[pkg-horde] [SCM] Debian Horde Packages repository: gollem package branch, debian-sid, updated. 18a786c0276a5ec4e8e09b677efb69a0035880ac

Gregory Colpart reg at foulademer.gcolpart.com
Sat May 2 11:58:36 UTC 2009


The following commit has been merged in the debian-sid branch:
commit e113e8dac10fa602a39c8ea5dacad4fbd8718eb4
Author: Gregory Colpart <reg at foulademer.gcolpart.com>
Date:   Sat May 2 13:07:29 2009 +0200

    merge from upstream

diff --git a/COPYING b/COPYING
index 5a965fb..a6b6756 100644
--- a/COPYING
+++ b/COPYING
@@ -1,7 +1,7 @@
 		    GNU GENERAL PUBLIC LICENSE
 		       Version 2, June 1991
 
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ Copyright 1989, 1991 Free Software Foundation, Inc.
      59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  Everyone is permitted to copy and distribute verbatim copies
  of this license document, but changing it is not allowed.
diff --git a/README b/README
index 5b10d35..a3f5253 100644
--- a/README
+++ b/README
@@ -1,8 +1,8 @@
 What is Gollem?
 ===============
 
-:Last update:   $Date: 2005/11/11 23:10:38 $
-:Revision:      $Revision: 1.6.2.1 $
+:Last update:   $Date: 2008/10/13 09:32:22 $
+:Revision:      $Revision: 1.6.2.2 $
 :Contact:       gollem at lists.horde.org
 
 .. contents:: Contents
diff --git a/clipboard.php b/clipboard.php
index 7698880..abdb6e0 100644
--- a/clipboard.php
+++ b/clipboard.php
@@ -1,22 +1,23 @@
 <?php
 /**
- * $Horde: gollem/clipboard.php,v 1.4.2.4 2007/01/02 13:54:52 jan Exp $
+ * $Horde: gollem/clipboard.php,v 1.4.2.6 2009/01/06 15:23:53 jan Exp $
  *
- * Copyright 2005-2007 Michael Slusarz <slusarz at horde.org>
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
  *
- * See the enclosed file COPYING for license information (GPL).  If you
- * did notcan receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Michael Slusarz <slusarz at horde.org>
  */
 
 @define('GOLLEM_BASE', dirname(__FILE__));
 require_once GOLLEM_BASE . '/lib/base.php';
-require_once 'Horde/Template.php';
 
 $dir = Util::getFormData('dir');
 
 $title = _("Clipboard");
-
-Horde::addScriptFile('stripe.js', 'gollem', true);
+Horde::addScriptFile('prototype.js', 'gollem', true);
+Horde::addScriptFile('tables.js', 'gollem', true);
 require GOLLEM_TEMPLATES . '/common-header.inc';
 Gollem::menu();
 Gollem::status();
@@ -32,7 +33,7 @@ foreach ($_SESSION['gollem']['clipboard'] as $key => $val) {
 }
 
 /* Set up the template object. */
-$template = &new Horde_Template();
+$template = new Gollem_Template();
 $template->setOption('gettext', true);
 $template->set('cancelbutton', _("Cancel"));
 $template->set('clearbutton', _("Clear"));
diff --git a/config/backends.php.dist b/config/backends.php.dist
index 6ad4ed2..89d3086 100644
--- a/config/backends.php.dist
+++ b/config/backends.php.dist
@@ -1,6 +1,6 @@
 <?php
 /**
- * $Horde: gollem/config/backends.php.dist,v 1.41.2.7 2006/11/27 03:32:40 slusarz Exp $
+ * $Horde: gollem/config/backends.php.dist,v 1.41.2.8 2008/10/09 20:54:40 jan Exp $
  *
  * This file is where you specify what backends people using your
  * installation of Gollem can log in to. There are a number of properties
@@ -13,7 +13,9 @@
  *         Valid options:
  *           'file'  --  Work with a local file system.
  *           'ftp'   --  Connect to a FTP server.
+ *           'smb'   --  Connect to a SMB fileshare.
  *           'sql'   --  Connect to VFS filesystem stored in SQL database.
+ *           'ssh2'  --  Connect to a remote server via SSH2.
  *
  * preferred: This is only useful if you want to use the same backend.php
  *            file for different machines: if the hostname of the Gollem
@@ -79,6 +81,7 @@
  *
  * attributes: The list of attributes that the driver supports. Available
  *             attributes:
+ *               'edit'
  *               'download'
  *               'group'
  *               'modified'
@@ -109,6 +112,8 @@ $backends['ftp'] = array(
         // IDs from the local password file.  This is useful only if the FTP
         // server is running on localhost or if the local user/group
         // IDs are identical to the remote FTP server.
+        // You must be running a version of Horde >= 3.1 for this parameter to
+        // have any effect.
         // 'maplocalids' => true,
         // The default permissions to set for newly created folders and files.
         // 'permissions' => '750'
@@ -125,7 +130,7 @@ $backends['ftp'] = array(
     // 'filter' => '^regex$',
     // 'quota' => false,
     'clipboard' => true,
-    'attributes' => array('type', 'name', 'download', 'modified', 'size', 'permission', 'owner', 'group')
+    'attributes' => array('type', 'name', 'edit', 'download', 'modified', 'size', 'permission', 'owner', 'group')
 );
 
 // This backend uses Horde credentials to automatically log in.
@@ -166,7 +171,7 @@ $backends['hordeftp'] = array(
     // 'filter' => '^regex$',
     // 'quota' => false,
     'clipboard' => true,
-    'attributes' => array('type', 'name', 'download', 'modified', 'size', 'permission', 'owner', 'group')
+    'attributes' => array('type', 'name', 'edit', 'download', 'modified', 'size', 'permission', 'owner', 'group')
 );
 
 // SQL Example.
@@ -178,7 +183,8 @@ $backends['sql'] = array(
 
     // The default connection details are pulled from the Horde-wide SQL
     // connection configuration.
-    'params' => array_merge($GLOBALS['conf']['sql'], array('table' => 'horde_vfs')),
+    'params' => array_merge($GLOBALS['conf']['sql'],
+                            array('table' => 'horde_vfs')),
 
     // If you need different connection details than from the Horde-wide SQL
     // connection configuration, uncomment and set the following lines.
@@ -202,7 +208,7 @@ $backends['sql'] = array(
     // 'filter' => '^regex$',
     // 'quota' => false,
     'clipboard' => false,
-    'attributes' => array('type', 'name', 'download', 'modified', 'size', 'permission', 'owner', 'group')
+    'attributes' => array('type', 'name', 'edit', 'download', 'modified', 'size')
 );
 
 // This backend specifies a home directory and root directory in a SQL vfs.
@@ -214,7 +220,8 @@ $backends['sqlhome'] = array(
 
     // The default connection details are pulled from the Horde-wide SQL
     // connection configuration.
-    'params' => array_merge($GLOBALS['conf']['sql'], array('table' => 'horde_vfs')),
+    'params' => array_merge($GLOBALS['conf']['sql'],
+                            array('table' => 'horde_vfs')),
 
     // If you need different connection details than from the Horde-wide SQL
     // connection configuration, uncomment and set the following lines.
@@ -238,7 +245,7 @@ $backends['sqlhome'] = array(
     // 'filter' => '^regex$',
     // 'quota' => false,
     'clipboard' => false,
-    'attributes' => array('type', 'name', 'download', 'modified', 'size', 'permission', 'owner', 'group'),
+    'attributes' => array('type', 'name', 'edit', 'download', 'modified', 'size', 'permission', 'owner', 'group'),
 );
 
 // NOTE: /exampledir/home and all subdirectories should be, for
@@ -263,7 +270,7 @@ $backends['file'] = array(
     // 'filter' => '^regex$',
     // 'quota' => false,
     'clipboard' => true,
-    'attributes' => array('type', 'name', 'download', 'modified', 'size', 'permission', 'owner', 'group')
+    'attributes' => array('type', 'name', 'edit', 'download', 'modified', 'size', 'permission', 'owner', 'group')
 );
 
 // SMB Example
@@ -302,5 +309,46 @@ $backends['file'] = array(
 //     // 'filter' => '^regex$',
 //     // 'quota' => false,
 //     'clipboard' => true,
-//     'attributes' => array('type', 'name', 'download', 'modified', 'size')
+//     'attributes' => array('type', 'name', 'edit', 'download', 'modified', 'size')
+// );
+
+// SSH2 Example
+// ** For the SSH2 backend to work, you must be using a version of Horde
+// ** that contains the SSH2 VFS driver. See the test.php script to determine
+// ** whether the SSH2 driver is present on your system.
+// $backends['ssh2'] = array(
+//     'name' => 'SSH2 Server',
+//     'driver' => 'ssh2',
+//     'preferred' => '',
+//     'hordeauth' => false,
+//     'params' => array(
+//         // The hostname/IP Address of the SSH server
+//         'hostspec' => 'ssh2.example.com',
+//         // The port number of the SSH server
+//         'port' => 22,
+//         // Set timeout (in seconds) for the SSH server. Default: 90 seconds
+//         // 'timeout' => 90,
+//         // If true and the POSIX extension is available the driver will map
+//         // the user and group IDs returned from the SSH server with the local
+//         // IDs from the local password file.  This is useful only if the SSH
+//         // server is running on localhost or if the local user/group
+//         // IDs are identical to the remote SSH server.
+//         // 'maplocalids' => true,
+//         // The default permissions to set for newly created folders and
+//         // files.
+//         // 'permissions' => '750'
+//     ),
+//     'loginparams' => array(
+//         // Allow the user to change the SSH server
+//         // 'hostspec' => 'Hostname',
+//         // Allow the user to change the SSH port
+//         // 'port' => 'Port'
+//     ),
+//     // 'root' => '',
+//     // 'home' => '',
+//     // 'createhome' => false,
+//     // 'filter' => '^regex$',
+//     // 'quota' => false,
+//     'clipboard' => true,
+//     'attributes' => array('type', 'name', 'edit', 'download', 'modified', 'size', 'permission', 'owner', 'group')
 // );
diff --git a/config/conf.xml b/config/conf.xml
index 28f4fbb..2c4ceea 100644
--- a/config/conf.xml
+++ b/config/conf.xml
@@ -1,29 +1,42 @@
 <?xml version="1.0"?>
-<!-- $Horde: gollem/config/conf.xml,v 1.5.2.2 2005/12/11 18:31:18 slusarz Exp $ -->
+<!-- $Horde: gollem/config/conf.xml,v 1.5.2.3 2008/10/09 20:54:40 jan Exp $ -->
 <configuration>
  <configtab name="manager" desc="File Manager Settings">
-  <configsection name="manager">
-   <configheader>File Manager settings</configheader>
-   <configstring name="date_format" desc="Date Format used in File List">%x</configstring>
-  </configsection>
-
   <configsection name="backend">
    <configheader>Backend Settings</configheader>
-   <configenum name="backend_list" desc="Should we display a list of backends (defined in config/backends.php) for users to choose from? The options are 'shown', 'hidden', and 'none'. If the backend list is hidden then you can use the 'preferred' mechanism to auto-select from it based on an HTTP virtualhost or another piece of data. If it is shown, the user will be able to pick from any of the options. If none, no server list will be shown and the defaults will be used unless another mechanism changes them.">
-    hidden
+   <configenum name="backend_list" desc="Should we display a list of backends
+   (defined in config/backends.php) for users to choose from? The options are
+   'none', 'hidden', and 'shown'. If 'none', no server list will be shown and
+   and the defaults will be used unless another mechanism changes them. If the
+   the backend list is 'hidden' then you can use the 'preferred' mechanism to
+   auto-select from it based on an HTTP virtualhost or another piece of data.
+   If it is 'shown', the user will be able to pick from any of the
+   options.">none
     <values>
      <value>none</value>
-     <value>shown</value>
      <value>hidden</value>
+     <value>shown</value>
     </values>
    </configenum>
   </configsection>
+  <configsection name="foldercache">
+   <configheader>Caching</configheader>
+   <configswitch name="use_cache" desc="Cache the folder list?">false
+    <case name="false" desc="No" />
+    <case name="true" desc="Yes">
+     <configinteger name="lifetime" desc="Cache lifetime, in seconds.  If
+     using the Memcache driver this will only have an effect in Horde 3.2 or
+     later.">120</configinteger>
+    </case>
+   </configswitch>
+  </configsection>
  </configtab>
 
  <configtab name="menu" desc="Menu Settings">
   <configsection name="menu">
    <configheader>Menu settings</configheader>
-   <configmultienum name="apps" desc="Select any applications that should be linked in Gollem's menu">
+   <configmultienum name="apps" desc="Select any applications that should be
+   linked in Gollem's menu">
    <values>
     <configspecial name="list-horde-apps" />
    </values>
diff --git a/config/credentials.php.dist b/config/credentials.php.dist
index 9db5979..f709c3d 100644
--- a/config/credentials.php.dist
+++ b/config/credentials.php.dist
@@ -10,7 +10,7 @@
  * File stores are defined in 'backends.php'.
  * Default user preferences are defined in 'prefs.php'.
  *
- * $Horde: gollem/config/credentials.php.dist,v 1.3 2003/02/22 17:13:23 chuck Exp $
+ * $Horde: gollem/config/credentials.php.dist,v 1.3.2.1 2008/10/09 20:54:40 jan Exp $
  */
 
 $credentials['username'] = array(
diff --git a/config/menu.php.dist b/config/menu.php.dist
index fe6029c..429d6c0 100644
--- a/config/menu.php.dist
+++ b/config/menu.php.dist
@@ -1,6 +1,6 @@
 <?php
 /**
- * $Horde: gollem/config/menu.php.dist,v 1.5 2005/08/15 05:36:27 slusarz Exp $
+ * $Horde: gollem/config/menu.php.dist,v 1.5.2.1 2008/10/09 20:54:40 jan Exp $
  *
  * This file lets you extend Gollem's menu with your own items.
  *
diff --git a/config/mime_drivers.php.dist b/config/mime_drivers.php.dist
index 204284e..b424082 100644
--- a/config/mime_drivers.php.dist
+++ b/config/mime_drivers.php.dist
@@ -1,6 +1,6 @@
 <?php
 /**
- * $Horde: gollem/config/mime_drivers.php.dist,v 1.8 2005/03/18 21:10:31 slusarz Exp $
+ * $Horde: gollem/config/mime_drivers.php.dist,v 1.8.2.1 2008/10/09 20:54:40 jan Exp $
  *
  * Decide which output drivers you want to activate for Gollem.
  * Settings in this file override settings in horde/config/mime_drivers.php.
@@ -27,6 +27,9 @@ $mime_drivers_map['gollem']['overrides'] = array();
 /**
  * Image driver settings
  */
-$mime_drivers['gollem']['images']['inline'] = true;
-$mime_drivers['gollem']['images']['handles'] = array(
-    'image/*');
+$mime_drivers['gollem']['images'] = array(
+    'inline' => true,
+    'handles' => array(
+        'image/*'
+    )
+);
diff --git a/config/prefs.php.dist b/config/prefs.php.dist
index 3e152f8..1663644 100644
--- a/config/prefs.php.dist
+++ b/config/prefs.php.dist
@@ -1,22 +1,19 @@
 <?php
 /**
- * $Horde: gollem/config/prefs.php.dist,v 1.32 2005/06/16 05:47:44 slusarz Exp $
+ * $Horde: gollem/config/prefs.php.dist,v 1.32.2.1 2008/10/09 20:54:40 jan Exp $
  *
  * See horde/config/prefs.php for documentation on the structure of this file.
  */
 
 // Make sure that constants are defined.
-// To respect Debian FHS policy, config/ is in /etc/ directory
-// Then we need use hardcoded parameter in Debian
- at define('GOLLEM_BASE', '/usr/share/horde3/gollem');
-require_once GOLLEM_BASE . '/lib/Gollem.php';
+require_once dirname(__FILE__) . '/../lib/Gollem.php';
 
 $prefGroups['display'] = array(
     'column' => _("User Interface"),
     'label' => _("File Display"),
     'desc' => _("Change your file sorting options."),
     'members' => array('show_dotfiles', 'sortdirsfirst', 'columnselect',
-                       'sortby', 'sortdir'));
+                       'sortby', 'sortdir', 'perpage'));
 
 $prefGroups['settings'] = array(
     'column' => _("User Interface"),
@@ -37,15 +34,15 @@ $_prefs['show_dotfiles'] = array(
 $_prefs['columnselect'] = array(
     'locked' => false,
     'type' => 'special'
-); 
- 
+);
+
 // columns to be displayed
 $_prefs['columns'] = array(
     'value' => "ftp\ttype\tname\tdownload\tmodified\tsize\tpermission\towner\tgroup",
     'locked' => false,
     'shared' => false,
     'type' => 'implicit'
-); 
+);
 
 // user preferred sorting column
 $_prefs['sortby'] = array(
@@ -84,7 +81,16 @@ $_prefs['sortdirsfirst'] = array(
     'desc' => _("List folders first?")
 );
 
-//user preferred recursive deletes
+// number of items per page
+$_prefs['perpage'] = array(
+    'value' => 20,
+    'locked' => false,
+    'shared' => true,
+    'type' => 'number',
+    'desc' => _("Items per page")
+);
+
+// user preferred recursive deletes
 $_prefs['recursive_deletes'] = array(
     'value' => 'disabled',
     'locked' => false,
diff --git a/docs/CHANGES b/docs/CHANGES
index 4bb8dc8..b8e0c6c 100644
--- a/docs/CHANGES
+++ b/docs/CHANGES
@@ -1,10 +1,55 @@
+----
+v1.1
+----
+
+[jan] Fix attachment dialog with only a single autologin backend (Bug #7096).
+[jan] Start with first page again when filter string is changed (Bug #7935).
+[mms] Navigation link now uses description from backends.php (Request #8037,
+      Thomas Reifferscheid <thomas at reifferscheid.org>).
+[mms] Fix autologin when switching backends on login page (Bug #7892,
+      horde at smartsector.hu).
+
+
+--------
+v1.1-RC1
+--------
+
+[mms] Add folder caching.
+[mms] Add pager navigation and filtering (Request #7160, joel at scopserv.com).
+[jan] Add Estonian translation (Alar Sing <alar.sing at err.ee>).
+[jan] Add Basque translation (Euskal Herriko Unibertsitatea EHU/UPV
+      <xabier.arrieta at ehu.es>).
+[cjh] Use the Horde date_format preference (Bug #6683).
+[cjh] Apply fix for http://dev.rubyonrails.org/ticket/11473 to prototype.js
+      (Request #6590).
+[jan] Add Turkish translation (METU <horde-tr at metu.edu.tr>).
+[cjh] If the VFS backend supports streams, use them to avoid reading entire
+      files into memory when downloading (Request #5913).
+[mms] Compress download data.
+[jan] Add Ukrainian translation (Andriy Kopystyansky <anri at polynet.lviv.ua>).
+[bak] Add browse API to allow browsing backends through WebDAV (Request #5337).
+[cjh] Inline PHP-generated javascript to avoid cross-domain leaking
+      (Bug #5307).
+[mms] Preserve URL anchor information when performing login.
+[mms] All javascript now contained in static, cacheable files.
+[cjh] Add javascript sortable tables to Gollem.
+[cjh] Use optimized Horde_Template code as Gollem_Template.
+[cjh] Fix javascript errors when filenames contain quotes (Bug #3824),
+      and make several links work without JavaScript.
+[jan] Add editing of text files.
+[mms] Add examples/checks for SSH2 VFS driver.
+[jan] Add Slovak translation (Martin Matuška <martin at matuska.org>).
+[cjh] Fix uploading files to non-empty directories when deletion is disabled
+      (Bug #5349).
+
+
 ------
 v1.0.3
 ------
 
 [jan] Add Catalan translation (Jordi Giralt <projecte.k2 at upcnet.es>).
 [jan] Add Slovenian translation (Duck <duck at obala.net>).
-[jan] Add Brazilian Portuguese translation (Fábio Gomes <flgoms at uol.com.br>).
+[jan] Add Brazilian Portuguese translation (Fábio Gomes <flgoms at uol.com.br>).
 [mms] Don't show quota link in menu if quota support for current backend is
       disabled.
 
@@ -181,7 +226,7 @@ v1.0-ALPHA
 [cjh] Use the new Notification system.
 [cjh] Use GOLLEM_TEMPLATES constant for all template paths.
 [cjh] Use $registry->get() for all Registry information.
-[jan] Add Swedish translation (Andreas Dahlén <andreas at dahlen.ws>).
+[jan] Add Swedish translation (Andreas Dahlén <andreas at dahlen.ws>).
 [jan] Remove the standard value for the language preference. The language to
       fall back to should be set Horde wide in lang.php instead.
 [jan] Add Korean translation (J.I Kim <aporie at netian.com>).
diff --git a/docs/CREDITS b/docs/CREDITS
index 898aa1d..db04958 100644
--- a/docs/CREDITS
+++ b/docs/CREDITS
@@ -6,20 +6,17 @@
 Core Developers
 ===============
 
-Max Kalika <max at horde.org>
-
-- initial implementation
-
-Michael Slusarz <slusarz at curecanti.org>
-
-- current maintainer
+- Michael Slusarz <slusarz at curecanti.org>
 
 
 Localization
 ============
 
 ======================  ===============================================
-Brazilian Portuguese    Fábio Gomes <flgoms at uol.com.br>
+Basque                  Euskal Herriko Unibertsitatea <xabier.arrieta at ehu.es>
+Brazilian Portuguese    Fábio Gomes <flgoms at uol.com.br>
+                        Luis Felipe Marzagao <duli at fedoraproject.org>
+                        Eduardo de Carli <carliedu at ig.com.br>
 Bulgarian               Miroslav Pendev <pendev at hotmail.com>
 Catalan                 Jordi Giralt <projecte.k2 at upcnet.es>
 Chinese (Traditional)   David Chang <david at tmv.gov.tw>
@@ -27,9 +24,10 @@ Czech                   Pavel Chytil <pchytil at asp.ogi.edu>
 Danish                  Brian Truelsen <horde+i18n at briantruelsen.dk>
 Dutch                   Jan Kuipers <jrkuipers at lauwerscollege.nl>
                         Peter Arien <peter.arien at cc.kuleuven.ac.be>
+Estonian                Alar Sing <alar.sing at err.ee>
 Finnish                 Leena Heino <leena.heino at uta.fi>
 French                  Remi Cohen-Scali <Remi at Cohen-Scali.com>
-                        Benoit St-André <ben at benoitst-andre.net>
+                        Benoit St-André <ben at benoitst-andre.net>
                         Pierre Lachance <pl at pierrelachance.net>
 German                  Jan Schneider <jan at horde.org>
 Hungarian               Laszlo L. Tornoci <torlasz at xenia.sote.hu>
@@ -40,11 +38,22 @@ Korean                  J.I Kim <aporie at netian.com>
 Latvian                 Janis Eisaks <jancs at dv.lv>
 Norwegian Nynorsk       Per-Stian Vatne <psv at orsta.org>
 Polish                  Krzysztof Kozlowski <kozik1 at o2.pl>
+                        Tadeusz Lesiecki <lesiecki at tmtsystem.pl>
+                        Piotr Tarnowski <drfugazi at drfugazi.eu.org>
 Romanian                Eugen Hoanca <eugenh at urban-grafx.ro>
-Slovenian               Duck <duck at obala.net>
-Spanish                 Raúl Alvarez Venegas <rav at tecoman.ucol.mx>
-                        Manuel Perez Ayala <mperaya at alcazaba.unex.es>
-Swedish                 Andreas Dahlén <andreas at dahlen.ws>
 Russian                 Anton Nekhoroshih <anton at valuehost.ru>
+Slovak                  Martin Matuška <martin at matuska.org>
                         Alexey Zakharov <baber at mosga.net>
+Slovenian               Duck <duck at obala.net>
+Spanish                 Raúl Alvarez Venegas <rav at tecoman.ucol.mx>
+                        Manuel Perez Ayala <mperaya at alcazaba.unex.es>
+Swedish                 Andreas Dahlén <andreas at dahlen.ws>
+Turkish                 Middle East Technical University <horde-tr at metu.edu.tr>
+Ukrainian               Andriy Kopystyansky <anri at polynet.lviv.ua>
 ======================  ===============================================
+
+
+Inactive Developers
+===================
+
+- Max Kalika <max at horde.org>
diff --git a/docs/INSTALL b/docs/INSTALL
index fc37e9e..a546b3a 100644
--- a/docs/INSTALL
+++ b/docs/INSTALL
@@ -2,8 +2,8 @@
  Installing Gollem 1.0
 =======================
 
-:Last update:   $Date: 2005/08/12 07:39:52 $
-:Revision:      $Revision: 1.14 $
+:Last update:   $Date: 2008/10/09 20:54:41 $
+:Revision:      $Revision: 1.14.2.1 $
 :Contact:       gollem at lists.horde.org
 
 .. contents:: Contents
@@ -87,6 +87,13 @@ To function properly, Gollem **REQUIRES** the following:
 3. If using a FTP backend, you **must** have at least one FTP server.
    If using a SQL backend, you **must** have at least one SQL server.
 
+4. If the ability to browse files using WebDAV is desired, the
+   HTTP_WebDAV_Server PEAR module is required.
+
+   To install, enter the following at the command prompt::
+
+       pear install HTTP_WebDAV_Server
+
 
 Installing Gollem
 =================
@@ -186,7 +193,7 @@ subscription information can be found at
 
   http://www.horde.org/mail/
 
-Lastly, Horde developers, contributors and users also make appearances on IRC,
+Lastly, Horde developers, contributors and users may also be found on IRC,
 on the channel #horde on the Freenode Network (irc.freenode.net).
 
 Please keep in mind that Gollem is free software written by volunteers.
diff --git a/docs/RELEASE_NOTES b/docs/RELEASE_NOTES
index 972d155..d154309 100644
--- a/docs/RELEASE_NOTES
+++ b/docs/RELEASE_NOTES
@@ -12,32 +12,37 @@
  * 8 - Minor security fixes
  * 9 - Major security fixes
  */
-$this->notes['fm']['focus'] = 6;
+$this->notes['fm']['focus'] = 5;
 
 /* Mailing list release notes. */
 $this->notes['ml']['changes'] = <<<ML
 The Horde Team is pleased to announce the final release of the Gollem File
-Manager version H3 (1.0.3).
+Manager version H3 (1.1).
 
 Gollem is a web-based file manager, providing the ability to fully manage a
 hierarchical file system stored in a variety of backends such as a SQL
-database, as part of a real filesystem, or on an FTP server.
+database, as part of a real filesystem, an FTP server, or an SSH2 server.
 
-The major changes compared to the Gollem H3 (1.0.2) version are:
-    * Don't show quota link in menu if quota support for current backend is
-      disabled.
-    * Added translations: Brazilian Portugese, Catalan, Slovanian.
-    * Updated translations: Finnish, German, Italian, Spanish, Traditional
-      Chinese.
+The major changes compared to Gollem version H3 (1.1-RC1) are:
+    * Use backend description instead of "Root" in the navigation links.
+    * Small bug fixes.
+
+Gollem version H3 (1.1) is a major upgrade in the 1.x release series,
+including these enhancements:
+    * Improved templating of output code to ease local customization.
+    * Javascript code now uses the prototype js library, resulting in more
+      robust code and more available functionality - for example, sortable
+      tables.
+    * Editing of text files in the browser.
+    * Browse API to allow browsing backends through WebDAV.
+    * And much more.
+
+Note that several of these new features require Horde 3.2+.
 ML;
 
 /* Freshmeat release notes. */
 $this->notes['fm']['changes'] = <<<FM
-Don't show quota link in menu if quota support for current backend is
-disabled.
-Brazilian Portugese, Catalan, and Slovanian translations have been added and
-Finnish, German, Italian, Spanish, Traditional Chinese translation have been
-updated.
+New in this release: Improved templating of output code to ease local customization. Javascript code uses prototype js library: more robust code and more available functionality including sortable tables. Editing of text files in the browser. Browse API to allow browsing backends through WebDAV.
 FM;
 
 $this->notes['name'] = 'Gollem';
diff --git a/docs/TODO b/docs/TODO
deleted file mode 100644
index 32cee00..0000000
--- a/docs/TODO
+++ /dev/null
@@ -1,17 +0,0 @@
-==============================
- Gollem Development TODO List
-==============================
-
-:Last update:   $Date: 2005/08/02 06:18:42 $
-:Revision:      $Revision: 1.5 $
-:Contact:       gollem at lists.horde.org
-
-- Manager: permissions
-
-- Manager: file typing
-
-- Manager: viewing/editing
-
-- Manager: cache directories
-
-- Manager: find files
diff --git a/edit.php b/edit.php
new file mode 100644
index 0000000..013926d
--- /dev/null
+++ b/edit.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * $Horde: gollem/edit.php,v 1.5.2.2 2009/01/06 15:23:53 jan Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Jan Schneider <jan at horde.org>
+ */
+
+ at define('GOLLEM_BASE', dirname(__FILE__));
+require_once GOLLEM_BASE . '/lib/base.php';
+
+$actionID = Util::getFormData('actionID');
+$driver = Util::getFormData('driver');
+$filedir = Util::getFormData('dir');
+$filename = Util::getFormData('file');
+$type = Util::getFormData('type');
+
+if ($driver != $GLOBALS['gollem_be']['driver']) {
+    Util::closeWindowJS();
+    exit;
+}
+
+/* Run through action handlers. */
+switch ($actionID) {
+case 'save_file':
+    $data = Util::getFormData('content');
+    $result = $gollem_vfs->writeData($filedir, $filename, $data);
+    if (is_a($result, 'PEAR_Error')) {
+        $message = sprintf(_("Access denied to %s"), $filename);
+    } else {
+        $message = sprintf(_("%s successfully saved."), $filename);
+    }
+    Util::closeWindowJS('alert(\'' . addslashes($message) . '\');');
+    exit;
+
+case 'edit_file':
+    $data = $gollem_vfs->read($filedir, $filename);
+    if (is_a($data, 'PEAR_Error')) {
+        Util::closeWindowJS('alert(\'' . addslashes(sprintf(_("Access denied to %s"), $filename)) . '\');');
+        exit;
+    }
+    require_once 'Horde/MIME/Magic.php';
+    $mime_type = MIME_Magic::extToMIME($type);
+    if (strpos($mime_type, 'text/') !== 0) {
+        Util::closeWindowJS();
+    }
+    if ($mime_type == 'text/html') {
+        require_once 'Horde/Editor.php';
+        $editor = &Horde_Editor::singleton('xinha', array('id' => 'content'));
+    }
+    require GOLLEM_TEMPLATES . '/common-header.inc';
+    Gollem::status();
+    require GOLLEM_TEMPLATES . '/edit/edit.inc';
+    require $registry->get('templates', 'horde') . '/common-footer.inc';
+    exit;
+}
+
+Util::closeWindowJS();
diff --git a/index.php b/index.php
index 25f8b2e..980bd59 100644
--- a/index.php
+++ b/index.php
@@ -1,20 +1,22 @@
 <?php
 /**
- * $Horde: gollem/index.php,v 1.20.2.2 2007/01/02 13:54:52 jan Exp $
+ * $Horde: gollem/index.php,v 1.20.2.4 2009/01/06 15:23:53 jan Exp $
  *
- * Copyright 1999-2007 Charles J. Hagenbuch <chuck at horde.org>
- * Copyright 1999-2007 Max Kalika <max at horde.org>
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
  *
- * See the enclosed file COPYING for license information (GPL).  If you
+ * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Max Kalika <max at horde.org>
+ * @author Chuck Hagenbuch <chuck at horde.org>
  */
 
-define('GOLLEM_BASE', dirname(__FILE__));
-$gollem_configured = (@is_readable(GOLLEM_BASE . '/config/backends.php') &&
-                      @is_readable(GOLLEM_BASE . '/config/conf.php') &&
-                      @is_readable(GOLLEM_BASE . '/config/credentials.php') &&
-                      @is_readable(GOLLEM_BASE . '/config/mime_drivers.php') &&
-                      @is_readable(GOLLEM_BASE . '/config/prefs.php'));
+ at define('GOLLEM_BASE', dirname(__FILE__));
+$gollem_configured = (is_readable(GOLLEM_BASE . '/config/backends.php') &&
+                      is_readable(GOLLEM_BASE . '/config/conf.php') &&
+                      is_readable(GOLLEM_BASE . '/config/credentials.php') &&
+                      is_readable(GOLLEM_BASE . '/config/mime_drivers.php') &&
+                      is_readable(GOLLEM_BASE . '/config/prefs.php'));
 
 if (!$gollem_configured) {
     require GOLLEM_BASE . '/../lib/Test.php';
diff --git a/js/columnselect.js b/js/columnselect.js
new file mode 100644
index 0000000..43d85c1
--- /dev/null
+++ b/js/columnselect.js
@@ -0,0 +1 @@
+var columns=[];function selectSource(){var F=document.prefs;var D="";while(F.unselected_columns.length>1){F.unselected_columns.options[F.unselected_columns.length-1]=null}while(F.selected_columns.length>1){F.selected_columns.options[F.selected_columns.length-1]=null}if(F.source.selectedIndex<1){return}var E=F.source.selectedIndex-1;var C=[];var B=[];for(var A=1;A<columns[E].length;A++){if(columns[E][A][2]){C[columns[E][A][3]]=[columns[E][A][1],columns[E][A][0]]}else{B[B.length]=[columns[E][A][1],columns[E][A][0]]}}for(A=0;A<C.length;A++){F.selected_columns.options[A+1]=new Option(C[A][0],C[A][1])}for(A=0;A<B.length;A++){F.unselected_columns.options[A+1]=new Option(B[A][0],B[A][1])}}function deselectHeaders(){document.prefs.unselected_columns[0].selected=false;document.prefs.selected_columns[0].selected=false}function resetHidden(){var C="";for(var B=0;B<columns.length;B++){if(B>0){C+="\n"}C+=columns[B][0];for(var A=1;A<columns[B].length;A++){if(columns[B][A][2]){C+="\t"+columns[B][A][0]}}}document.prefs.columns.value=C}function addColumn(){var C=document.prefs;var B=C.source.selectedIndex-1;for(i=1;i<C.unselected_columns.length;i++){if(C.unselected_columns[i].selected){for(var A=1;A<columns[B].length;A++){if(columns[B][A][0]==C.unselected_columns[i].value){columns[B][A][2]=true}}C.selected_columns[C.selected_columns.length]=new Option(C.unselected_columns[i].text,C.unselected_columns[i].value);C.unselected_columns[i]=null;i--}}resetHidden()}function removeColumn(){var C=document.prefs;var B=C.source.selectedIndex-1;for(i=1;i<C.selected_columns.length;i++){if(C.selected_columns[i].selected){for(var A=1;A<columns[B].length;A++){if(columns[B][A][0]==C.selected_columns[i].value){columns[B][A][2]=false}}C.unselected_columns[C.unselected_columns.length]=new Option(C.selected_columns[i].text,C.selected_columns[i].value);C.selected_columns[i]=null;i--}}resetHidden()}function moveColumnUp(){var D=document.prefs;var C=D.selected_columns.selectedIndex;var B=D.source.selectedIndex-1;if(C<=1||D.selected_columns.length<=2){return}D.selected_columns.selectedIndex=C;var A=D.selected_columns[C].value;tmp=[];for(i=1;i<D.selected_columns.length;i++){tmp[i-1]=new Option(D.selected_columns[i].text,D.selected_columns[i].value)}for(i=0;i<tmp.length;i++){if(i+1==C-1){D.selected_columns[i+1]=tmp[i+1]}else{if(i+1==C){D.selected_columns[i+1]=tmp[i-1]}else{D.selected_columns[i+1]=tmp[i]}}}D.selected_columns.selectedIndex=C-1;for(i=2;i<columns[B].length-1;i++){if(columns[B][i][0]==A){column=columns[B][i];columns[B][i]=columns[B][i-1];columns[B][i-1]=column}}resetHidden()}function moveColumnDown(){var C=document.prefs;var B=C.selected_columns.selectedIndex;var A=C.source.selectedIndex-1;if(B==-1||C.selected_columns.length<=2||B==C.selected_columns.length-1){return}C.selected_columns.selectedIndex=B;var D=C.selected_columns[B].value;tmp=[];for(i=1;i<C.selected_columns.length;i++){tmp[i-1]=new Option(C.selected_columns[i].text,C.selected_columns[i].value)}for(i=0;i<tmp.length;i++){if(i+1==B){C.selected_columns[i+1]=tmp[i+1]}else{if(i+1==B+1){C.selected_columns[i+1]=tmp[i-1]}else{C.selected_columns[i+1]=tmp[i]}}}C.selected_columns.selectedIndex=B+1;for(i=columns[A].length-2;i>0;i--){if(columns[A][i][0]==D||columns[A][i+1][0]==D){column=columns[A][i];columns[A][i]=columns[A][i+1];columns[A][i+1]=column}}resetHidden()};
\ No newline at end of file
diff --git a/js/login.js b/js/login.js
new file mode 100644
index 0000000..e523b32
--- /dev/null
+++ b/js/login.js
@@ -0,0 +1 @@
+function setFocus(){if(document.gollem_login.username){document.gollem_login.username.focus()}}function gollem_reload(){var A=reload_url+document.gollem_login.backend_key[document.gollem_login.backend_key.selectedIndex].value;if(document.gollem_login.url&&document.gollem_login.url.value){A+="&url="+document.gollem_login.url.value}window.location=A}function submit_login(){if(document.gollem_login.username&&document.gollem_login.username.value==""){alert(GollemText.login_username);document.gollem_login.username.focus();return false}else{if(document.gollem_login.password&&document.gollem_login.password.value==""){alert(GollemText.login_password);document.gollem_login.password.focus();return false}else{document.gollem_login.loginButton.disabled=true;if(ie_clientcaps){try{document.gollem_login.ie_version.value=objCCaps.getComponentVersion("{89820200-ECBD-11CF-8B85-00AA005B4383}","componentid")}catch(A){}}document.gollem_login.submit();return true}}}function selectLang(){var A="login.php?new_lang="+document.gollem_login.new_lang[document.gollem_login.new_lang.selectedIndex].value;if(lang_url!==null){A+="&url="+lang_url}self.location=A}function removeHash(A){if(A==null||A==undefined){return null}else{if(A.length&&A.charAt(0)=="#"){if(A.length==1){return""}else{return A.substring(1)}}}return A}Event.observe(window,"load",function(){if(gollem_auth&&parent.frames.horde_main){if(nomenu){parent.location=self.location}else{document.gollem_login.target="_parent"}}if(location.hash){$("anchor_string").value=removeHash(location.hash)}});
\ No newline at end of file
diff --git a/js/manager.js b/js/manager.js
new file mode 100644
index 0000000..76e8149
--- /dev/null
+++ b/js/manager.js
@@ -0,0 +1 @@
+var Gollem={toggleRow:function(){$$("table.striped tr").each(function(A){var B=A.select("TD");A.observe("mouseover",B.invoke.bind(B,"addClassName","selected"));A.observe("mouseout",B.invoke.bind(B,"removeClassName","selected"))})},getChecked:function(){return this.getElements().findAll(function(A){return A.checked})},getElements:function(){return $("manager").getInputs(null,"items[]")},getSelected:function(){return this.getChecked().pluck("value").join("\n")},getItemsArray:function(){var A=0,B=$("manager").getInputs(null,"itemTypes[]");return this.getElements().collect(function(C){return{c:C.checked,v:C.value,t:B[A++].value}})},getSelectedFoldersList:function(){return this.getItemsArray().collect(function(A){return(A.c&&A.t=="**dir")?A.v:null}).compact().join("\n")},chooseAction:function(A){var B=$F("action"+A);switch(B){case"paste_items":$("actionID").setValue("paste_items");$("manager").submit();break;default:if(!this.getChecked().size()){alert(GollemText.select_item);break}switch(B){case"rename_items":this.renameItems();break;case"delete_items":this.deleteItems();break;case"chmod_modify":$("attributes").show();break;case"cut_items":$("actionID").setValue("cut_items");$("manager").submit();break;case"copy_items":$("actionID").setValue("copy_items");$("manager").submit();break}break}},changeDirectory:function(A){this._prepPopup("changeDirectory",A.element());$("cdfrm_fname").focus();A.stop()},createFolder:function(A){this._prepPopup("createFolder",A.element());$("createfrm_fname").focus();A.stop()},_prepPopup:function(A,B){this.getChecked().each(function(C){C.checked=false});$(A).clonePosition(B,{setWidth:false,setHeight:false,offsetTop:B.getHeight()}).show()},renameItems:function(){var A=this.getChecked();if(A.size()){A[0].checked=false;$("rename").show();$("renamefrm_oldname").setValue(A[0].value);$("renamefrm_newname").setValue(A[0].value).focus()}},deleteItems:function(){var A=true,B;if(window.confirm(GollemText.delete_confirm_1+"\n"+this.getSelected()+"\n"+GollemText.delete_confirm_2)){if(warn_recursive){B=this.getSelectedFoldersList();if(!B.empty()&&!window.confirm(GollemText.delete_recurs_1+"\n"+B+"\n"+GollemText.delete_recurs_2)){A=false}}}else{A=false}if(A){$("actionID").setValue("delete_items");$("manager").submit()}},toggleSelection:function(){var B=this.getElements(),A=(this.getChecked().size()!=B.length);B.each(function(C){C.checked=A})},createFolderOK:function(){$("createFolder").hide();if($F("createfrm_fname")){$("new_folder").setValue($F("createfrm_fname"));$("actionID").setValue("create_folder");$("manager").submit()}},createFolderKeyCheck:function(A){switch(A.keyCode){case Event.KEY_ESC:this.createFolderCancel();return false;case EVENT.KEY_RETURN:this.createFolderOK();return false}return true},createFolderCancel:function(){$("createFolder").hide();$("createfrm").reset()},chmodCancel:function(){$("attributes").hide();$("chmodfrm").reset()},chmodSave:function(){var A=group=owner=0;$("chmodfrm").getElements().each(function(B){if(B.name=="owner[]"&&B.checked){owner|=B.value}else{if(B.name=="group[]"&&B.checked){group|=B.value}else{if(B.name=="all[]"&&B.checked){A|=B.value}}}});$("attributes").hide();$("chmod").setValue("0"+owner+""+group+""+A);$("actionID").setValue("chmod_modify");$("manager").submit()},renameOK:function(){var E=this.getChecked(),B=$F("renamefrm_newname"),D=$F("new_names"),C=$F("renamefrm_oldname"),A=$F("old_names");if(B&&B!=C){D+="|"+B;A+="|"+C}if(D.startsWith("|")){D=D.substring(1)}if(A.startsWith("|")){A=A.substring(1)}$("new_names").setValue(D);$("old_names").setValue(A);if(E.size()){E[0].checked=false;found=true;$("rename").show();$F(E[0]).focus()}else{$("actionID").setValue("rename_items");$("manager").submit()}return false},renameCancel:function(){$("new_names","old_names").invoke("setValue","");$("rename").hide()},renameKeyCheck:function(A){switch(A.keyCode){case Event.KEY_ESC:this.renameCancel();return false;case EVENT.KEY_RETURN:this.renameOK();return false}return true},changeDirectoryOK:function(){$("changeDirectory").hide();if($F("cdfrm_fname")){$("dir").setValue($F("cdfrm_fname"));$("manager").submit()}},changeDirectoryKeyCheck:function(A){switch(A.keyCode){case Event.KEY_ESC:this.changeDirectoryCancel();return false;case EVENT.KEY_RETURN:this.changeDirectoryOK();return false}return true},changeDirectoryCancel:function(){$("changeDirectory").hide();$("cdfrm").reset()},uploadFields:function(){return $("manager").getInputs("file").collect(function(A){return(A.name.substr(0,12)=="file_upload_")?A:null}).compact()},uploadFile:function(){if(this.uploadsExist()){$("actionID").setValue("upload_file");$("manager").submit()}},applyFilter:function(){$("manager").submit()},clearFilter:function(){$("filter").setValue("");this.applyFilter()},uploadsExist:function(){if(GollemVar.empty_input||this.uploadFields().find(function(A){return $F(A)})){return true}alert(GollemText.specify_upload);$("file_upload_1").focus();return false},uploadChanged:function(){if(GollemVar.empty_input){return}var B,D,A=this.uploadFields(),C=A.findAll(function(E){return $F(E).length}).length;if(C==A.length){D=$("upload_row_"+C);if(D){B=new Element("INPUT",{type:"file",name:"file_upload_"+(C+1),size:25});D.insert({after:new Element("DIV",{id:"upload_row_"+(C+1)}).insert(new Element("STRONG").insert(GollemText.file+" "+(C+1)+":")).insert(" ").insert(B)});B.observe("change",this.uploadChanged.bind(this))}}},doPrefsUpdate:function(B,A){try{new Ajax.Request(GollemVar.prefs_api,{parameters:{app:"gollem",pref:"sortby",value:B.substring(1)}});new Ajax.Request(GollemVar.prefs_api,{parameters:{app:"gollem",pref:"sortdir",value:A}})}catch(C){}}};function table_sortCallback(C,B,A){if(Gollem.prefs_update_timeout){window.clearTimeout(Gollem.prefs_update_timeout)}Gollem.prefs_update_timeout=Gollem.doPrefsUpdate.bind(this,B,A).delay(0.3)}document.observe("dom:loaded",function(){var A;Gollem.toggleRow();if(A=$("createfolder")){A.observe("click",Gollem.createFolder.bindAsEventListener(Gollem))}if(A=$("changefolder")){A.observe("click",Gollem.changeDirectory.bindAsEventListener(Gollem))}});
\ No newline at end of file
diff --git a/js/popup.js b/js/popup.js
new file mode 100644
index 0000000..6a3bceb
--- /dev/null
+++ b/js/popup.js
@@ -0,0 +1 @@
+function popup_gollem(F,G,B,E){if(!G){G=600}var A=screen.width;if(G>(A-75)){G=A-75}if(!B){B=500}var A=screen.width;if(G>(A-75)){G=A-75}var D=new Date();var C=D.getTime();if(F.indexOf("?")==-1){var H="?"}else{var H="&"}if(E!=""){F=F+H+unescape(E)+"&uniq="+C}else{F=F+H+"uniq="+C}param="toolbar=no,location=no,status=yes,scrollbars=yes,resizable=yes,width="+G+",height="+B+",left=0,top=0";win=window.open(F,C,param);if(!win){alert(GollemText.popup_block)}else{if(typeof win.name=="undefined"){win.name=C}if(typeof win.opener=="undefined"){win.opener=self}win.focus()}};
\ No newline at end of file
diff --git a/js/prototype.js b/js/prototype.js
new file mode 100644
index 0000000..3c2493f
--- /dev/null
+++ b/js/prototype.js
@@ -0,0 +1 @@
+var Prototype={Version:"1.6.0.3",Browser:{IE:!!(window.attachEvent&&navigator.userAgent.indexOf("Opera")===-1),Opera:navigator.userAgent.indexOf("Opera")>-1,WebKit:navigator.userAgent.indexOf("AppleWebKit/")>-1,Gecko:navigator.userAgent.indexOf("Gecko")>-1&&navigator.userAgent.indexOf("KHTML")===-1,MobileSafari:!!navigator.userAgent.match(/Apple.*Mobile.*Safari/)},BrowserFeatures:{XPath:!!document.evaluate,SelectorsAPI:!!document.querySelector,ElementExtensions:!!window.HTMLElement,SpecificElementExtensions:document.createElement("div")["__proto__"]&&document.createElement("div")["__proto__"]!==document.createElement("form")["__proto__"]},ScriptFragment:"<script[^>]*>([\\S\\s]*?)<\/script>",JSONFilter:/^\/\*-secure-([\s\S]*)\*\/\s*$/,emptyFunction:function(){},K:function(a){return a}};if(Prototype.Browser.MobileSafari){Prototype.BrowserFeatures.SpecificElementExtensions=false}var Class={create:function(){var e=null,d=$A(arguments);if(Object.isFunction(d[0])){e=d.shift()}function a(){this.initialize.apply(this,arguments)}Object.extend(a,Class.Methods);a.superclass=e;a.subclasses=[];if(e){var b=function(){};b.prototype=e.prototype;a.prototype=new b;e.subclasses.push(a)}for(var c=0;c<d.length;c++){a.addMethods(d[c])}if(!a.prototype.initialize){a.prototype.initialize=Prototype.emptyFunction}a.prototype.constructor=a;return a}};Class.Methods={addMethods:function(g){var c=this.superclass&&this.superclass.prototype;var b=Object.keys(g);if(!Object.keys({toString:true}).length){b.push("toString","valueOf")}for(var a=0,d=b.length;a<d;a++){var f=b[a],e=g[f];if(c&&Object.isFunction(e)&&e.argumentNames().first()=="$super"){var h=e;e=(function(i){return function(){return c[i].apply(this,arguments)}})(f).wrap(h);e.valueOf=h.valueOf.bind(h);e.toString=h.toString.bind(h)}this.prototype[f]=e}return this}};var Abstract={};Object.extend=function(a,c){for(var b in c){a[b]=c[b]}return a};Object.extend(Object,{inspect:function(a){try{if(Object.isUndefined(a)){return"undefined"}if(a===null){return"null"}return a.inspect?a.inspect():String(a)}catch(b){if(b instanceof RangeError){return"..."}throw b}},toJSON:function(a){var c=typeof a;switch(c){case"undefined":case"function":case"unknown":return;case"boolean":return a.toString()}if(a===null){return"null"}if(a.toJSON){return a.toJSON()}if(Object.isElement(a)){return}var b=[];for(var e in a){var d=Object.toJSON(a[e]);if(!Object.isUndefined(d)){b.push(e.toJSON()+": "+d)}}return"{"+b.join(", ")+"}"},toQueryString:function(a){return $H(a).toQueryString()},toHTML:function(a){return a&&a.toHTML?a.toHTML():String.interpret(a)},keys:function(a){var b=[];for(var c in a){b.push(c)}return b},values:function(b){var a=[];for(var c in b){a.push(b[c])}return a},clone:function(a){return Object.extend({},a)},isElement:function(a){return!!(a&&a.nodeType==1)},isArray:function(a){return a!=null&&typeof a=="object"&&"splice"in a&&"join"in a},isHash:function(a){return a instanceof Hash},isFunction:function(a){return typeof a=="function"},isString:function(a){return typeof a=="string"},isNumber:function(a){return typeof a=="number"},isUndefined:function(a){return typeof a=="undefined"}});Object.extend(Function.prototype,{argumentNames:function(){var a=this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g,"").split(",");return a.length==1&&!a[0]?[]:a},bind:function(){if(arguments.length<2&&Object.isUndefined(arguments[0])){return this}var a=this,c=$A(arguments),b=c.shift();return function(){return a.apply(b,c.concat($A(arguments)))}},bindAsEventListener:function(){var a=this,c=$A(arguments),b=c.shift();return function(d){return a.apply(b,[d||window.event].concat(c))}},curry:function(){if(!arguments.length){return this}var a=this,b=$A(arguments);return function(){return a.apply(this,b.concat($A(arguments)))}},delay:function(){var a=this,b=$A(arguments),c=b.shift()*1000;return window.setTimeout(function(){return a.apply(a,b)},c)},defer:function(){var a=[0.01].concat($A(arguments));return this.delay.apply(this,a)},wrap:function(b){var a=this;return function(){return b.apply(this,[a.bind(this)].concat($A(arguments)))}},methodize:function(){if(this._methodized){return this._methodized}var a=this;return this._methodized=function(){return a.apply(null,[this].concat($A(arguments)))}}});Date.prototype.toJSON=function(){return'"'+this.getUTCFullYear()+"-"+(this.getUTCMonth()+1).toPaddedString(2)+"-"+this.getUTCDate().toPaddedString(2)+"T"+this.getUTCHours().toPaddedString(2)+":"+this.getUTCMinutes().toPaddedString(2)+":"+this.getUTCSeconds().toPaddedString(2)+'Z"'};var Try={these:function(){var c;for(var b=0,d=arguments.length;b<d;b++){var a=arguments[b];try{c=a();break}catch(f){}}return c}};RegExp.prototype.match=RegExp.prototype.test;RegExp.escape=function(a){return String(a).replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")};var PeriodicalExecuter=Class.create({initialize:function(b,a){this.callback=b;this.frequency=a;this.currentlyExecuting=false;this.registerCallback()},registerCallback:function(){this.timer=setInterval(this.onTimerEvent.bind(this),this.frequency*1000)},execute:function(){this.callback(this)},stop:function(){if(!this.timer){return}clearInterval(this.timer);this.timer=null},onTimerEvent:function(){if(!this.currentlyExecuting){try{this.currentlyExecuting=true;this.execute()}finally{this.currentlyExecuting=false}}}});Object.extend(String,{interpret:function(a){return a==null?"":String(a)},specialChar:{"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r","\\":"\\\\"}});Object.extend(String.prototype,{gsub:function(e,c){var a="",d=this,b;c=arguments.callee.prepareReplacement(c);while(d.length>0){if(b=d.match(e)){a+=d.slice(0,b.index);a+=String.interpret(c(b));d=d.slice(b.index+b[0].length)}else{a+=d,d=""}}return a},sub:function(c,a,b){a=this.gsub.prepareReplacement(a);b=Object.isUndefined(b)?1:b;return this.gsub(c,function(d){if(--b<0){return d[0]}return a(d)})},scan:function(b,a){this.gsub(b,a);return String(this)},truncate:function(b,a){b=b||30;a=Object.isUndefined(a)?"...":a;return this.length>b?this.slice(0,b-a.length)+a:String(this)},strip:function(){return this.replace(/^\s+/,"").replace(/\s+$/,"")},stripTags:function(){return this.replace(/<\/?[^>]+>/gi,"")},stripScripts:function(){return this.replace(new RegExp(Prototype.ScriptFragment,"img"),"")},extractScripts:function(){var b=new RegExp(Prototype.ScriptFragment,"img");var a=new RegExp(Prototype.ScriptFragment,"im");return(this.match(b)||[]).map(function(c){return(c.match(a)||["",""])[1]})},evalScripts:function(){return this.extractScripts().map(function(script){return eval(script)})},escapeHTML:function(){var a=arguments.callee;a.text.data=this;return a.div.innerHTML},unescapeHTML:function(){var a=new Element("div");a.innerHTML=this.stripTags();return a.childNodes[0]?(a.childNodes.length>1?$A(a.childNodes).inject("",function(b,c){return b+c.nodeValue}):a.childNodes[0].nodeValue):""},toQueryParams:function(b){var a=this.strip().match(/([^?#]*)(#.*)?$/);if(!a){return{}}return a[1].split(b||"&").inject({},function(e,f){if((f=f.split("="))[0]){var c=decodeURIComponent(f.shift());var d=f.length>1?f.join("="):f[0];if(d!=undefined){d=decodeURIComponent(d)}if(c in e){if(!Object.isArray(e[c])){e[c]=[e[c]]}e[c].push(d)}else{e[c]=d}}return e})},toArray:function(){return this.split("")},succ:function(){return this.slice(0,this.length-1)+String.fromCharCode(this.charCodeAt(this.length-1)+1)},times:function(a){return a<1?"":new Array(a+1).join(this)},camelize:function(){var d=this.split("-"),a=d.length;if(a==1){return d[0]}var c=this.charAt(0)=="-"?d[0].charAt(0).toUpperCase()+d[0].substring(1):d[0];for(var b=1;b<a;b++){c+=d[b].charAt(0).toUpperCase()+d[b].substring(1)}return c},capitalize:function(){return this.charAt(0).toUpperCase()+this.substring(1).toLowerCase()},underscore:function(){return this.gsub(/::/,"/").gsub(/([A-Z]+)([A-Z][a-z])/,"#{1}_#{2}").gsub(/([a-z\d])([A-Z])/,"#{1}_#{2}").gsub(/-/,"_").toLowerCase()},dasherize:function(){return this.gsub(/_/,"-")},inspect:function(b){var a=this.gsub(/[\x00-\x1f\\]/,function(c){var d=String.specialChar[c[0]];return d?d:"\\u00"+c[0].charCodeAt().toPaddedString(2,16)});if(b){return'"'+a.replace(/"/g,'\\"')+'"'}return"'"+a.replace(/'/g,"\\'")+"'"},toJSON:function(){return this.inspect(true)},unfilterJSON:function(a){return this.sub(a||Prototype.JSONFilter,"#{1}")},isJSON:function(){var a=this;if(a.blank()){return false}a=this.replace(/\\./g,"@").replace(/"[^"\\\n\r]*"/g,"");return(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(a)},evalJSON:function(sanitize){var json=this.unfilterJSON();try{if(!sanitize||json.isJSON()){return eval("("+json+")")}}catch(e){}throw new SyntaxError("Badly formed JSON string: "+this.inspect())},include:function(a){return this.indexOf(a)>-1},startsWith:function(a){return this.indexOf(a)===0},endsWith:function(a){var b=this.length-a.length;return b>=0&&this.lastIndexOf(a)===b},empty:function(){return this==""},blank:function(){return/^\s*$/.test(this)},interpolate:function(a,b){return new Template(this,b).evaluate(a)}});if(Prototype.Browser.WebKit||Prototype.Browser.IE){Object.extend(String.prototype,{escapeHTML:function(){return this.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")},unescapeHTML:function(){return this.stripTags().replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}})}String.prototype.gsub.prepareReplacement=function(b){if(Object.isFunction(b)){return b}var a=new Template(b);return function(c){return a.evaluate(c)}};String.prototype.parseQuery=String.prototype.toQueryParams;Object.extend(String.prototype.escapeHTML,{div:document.createElement("div"),text:document.createTextNode("")});String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);var Template=Class.create({initialize:function(a,b){this.template=a.toString();this.pattern=b||Template.Pattern},evaluate:function(a){if(Object.isFunction(a.toTemplateReplacements)){a=a.toTemplateReplacements()}return this.template.gsub(this.pattern,function(d){if(a==null){return""}var f=d[1]||"";if(f=="\\"){return d[2]}var b=a,g=d[3];var e=/^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;d=e.exec(g);if(d==null){return f}while(d!=null){var c=d[1].startsWith("[")?d[2].gsub("\\\\]","]"):d[1];b=b[c];if(null==b||""==d[3]){break}g=g.substring("["==d[3]?d[1].length:d[0].length);d=e.exec(g)}return f+String.interpret(b)})}});Template.Pattern=/(^|.|\r|\n)(#\{(.*?)\})/;var $break={};var Enumerable={each:function(c,b){var a=0;try{this._each(function(e){c.call(b,e,a++)})}catch(d){if(d!=$break){throw d}}return this},eachSlice:function(d,c,b){var a=-d,e=[],f=this.toArray();if(d<1){return f}while((a+=d)<f.length){e.push(f.slice(a,a+d))}return e.collect(c,b)},all:function(c,b){c=c||Prototype.K;var a=true;this.each(function(e,d){a=a&&!!c.call(b,e,d);if(!a){throw $break}});return a},any:function(c,b){c=c||Prototype.K;var a=false;this.each(function(e,d){if(a=!!c.call(b,e,d)){throw $break}});return a},collect:function(c,b){c=c||Prototype.K;var a=[];this.each(function(e,d){a.push(c.call(b,e,d))});return a},detect:function(c,b){var a;this.each(function(e,d){if(c.call(b,e,d)){a=e;throw $break}});return a},findAll:function(c,b){var a=[];this.each(function(e,d){if(c.call(b,e,d)){a.push(e)}});return a},grep:function(d,c,b){c=c||Prototype.K;var a=[];if(Object.isString(d)){d=new RegExp(d)}this.each(function(f,e){if(d.match(f)){a.push(c.call(b,f,e))}});return a},include:function(a){if(Object.isFunction(this.indexOf)){if(this.indexOf(a)!=-1){return true}}var b=false;this.each(function(c){if(c==a){b=true;throw $break}});return b},inGroupsOf:function(b,a){a=Object.isUndefined(a)?null:a;return this.eachSlice(b,function(c){while(c.length<b){c.push(a)}return c})},inject:function(a,c,b){this.each(function(e,d){a=c.call(b,a,e,d)});return a},invoke:function(b){var a=$A(arguments).slice(1);return this.map(function(c){return c[b].apply(c,a)})},max:function(c,b){c=c||Prototype.K;var a;this.each(function(e,d){e=c.call(b,e,d);if(a==null||e>=a){a=e}});return a},min:function(c,b){c=c||Prototype.K;var a;this.each(function(e,d){e=c.call(b,e,d);if(a==null||e<a){a=e}});return a},partition:function(d,b){d=d||Prototype.K;var c=[],a=[];this.each(function(f,e){(d.call(b,f,e)?c:a).push(f)});return[c,a]},pluck:function(b){var a=[];this.each(function(c){a.push(c[b])});return a},reject:function(c,b){var a=[];this.each(function(e,d){if(!c.call(b,e,d)){a.push(e)}});return a},sortBy:function(b,a){return this.map(function(d,c){return{value:d,criteria:b.call(a,d,c)}}).sort(function(f,e){var d=f.criteria,c=e.criteria;return d<c?-1:d>c?1:0}).pluck("value")},toArray:function(){return this.map()},zip:function(){var b=Prototype.K,a=$A(arguments);if(Object.isFunction(a.last())){b=a.pop()}var c=[this].concat(a).map($A);return this.map(function(e,d){return b(c.pluck(d))})},size:function(){return this.toArray().length},inspect:function(){return"#<Enumerable:"+this.toArray().inspect()+">"}};Object.extend(Enumerable,{map:Enumerable.collect,find:Enumerable.detect,select:Enumerable.findAll,filter:Enumerable.findAll,member:Enumerable.include,entries:Enumerable.toArray,every:Enumerable.all,some:Enumerable.any});function $A(c){if(!c){return[]}if(c.toArray){return c.toArray()}var b=c.length||0,a=new Array(b);while(b--){a[b]=c[b]}return a}if(Prototype.Browser.WebKit){$A=function(c){if(!c){return[]}if(!(typeof c==="function"&&typeof c.length==="number"&&typeof c.item==="function")&&c.toArray){return c.toArray()}var b=c.length||0,a=new Array(b);while(b--){a[b]=c[b]}return a}}Array.from=$A;Object.extend(Array.prototype,Enumerable);if(!Array.prototype._reverse){Array.prototype._reverse=Array.prototype.reverse}Object.extend(Array.prototype,{_each:function(b){for(var a=0,c=this.length;a<c;a++){b(this[a])}},clear:function(){this.length=0;return this},first:function(){return this[0]},last:function(){return this[this.length-1]},compact:function(){return this.select(function(a){return a!=null})},flatten:function(){return this.inject([],function(b,a){return b.concat(Object.isArray(a)?a.flatten():[a])})},without:function(){var a=$A(arguments);return this.select(function(b){return!a.include(b)})},reverse:function(a){return(a!==false?this:this.toArray())._reverse()},reduce:function(){return this.length>1?this:this[0]},uniq:function(a){return this.inject([],function(d,c,b){if(0==b||(a?d.last()!=c:!d.include(c))){d.push(c)}return d})},intersect:function(a){return this.uniq().findAll(function(b){return a.detect(function(c){return b===c})})},clone:function(){return[].concat(this)},size:function(){return this.length},inspect:function(){return"["+this.map(Object.inspect).join(", ")+"]"},toJSON:function(){var a=[];this.each(function(b){var c=Object.toJSON(b);if(!Object.isUndefined(c)){a.push(c)}});return"["+a.join(", ")+"]"}});if(Object.isFunction(Array.prototype.forEach)){Array.prototype._each=Array.prototype.forEach}if(!Array.prototype.indexOf){Array.prototype.indexOf=function(c,a){a||(a=0);var b=this.length;if(a<0){a=b+a}for(;a<b;a++){if(this[a]===c){return a}}return-1}}if(!Array.prototype.lastIndexOf){Array.prototype.lastIndexOf=function(b,a){a=isNaN(a)?this.length:(a<0?this.length+a:a)+1;var c=this.slice(0,a).reverse().indexOf(b);return(c<0)?c:a-c-1}}Array.prototype.toArray=Array.prototype.clone;function $w(a){if(!Object.isString(a)){return[]}a=a.strip();return a?a.split(/\s+/):[]}if(Prototype.Browser.Opera){Array.prototype.concat=function(){var e=[];for(var b=0,c=this.length;b<c;b++){e.push(this[b])}for(var b=0,c=arguments.length;b<c;b++){if(Object.isArray(arguments[b])){for(var a=0,d=arguments[b].length;a<d;a++){e.push(arguments[b][a])}}else{e.push(arguments[b])}}return e}}Object.extend(Number.prototype,{toColorPart:function(){return this.toPaddedString(2,16)},succ:function(){return this+1},times:function(b,a){$R(0,this,true).each(b,a);return this},toPaddedString:function(c,b){var a=this.toString(b||10);return"0".times(c-a.length)+a},toJSON:function(){return isFinite(this)?this.toString():"null"}});$w("abs round ceil floor").each(function(a){Number.prototype[a]=Math[a].methodize()});function $H(a){return new Hash(a)}var Hash=Class.create(Enumerable,(function(){function a(b,c){if(Object.isUndefined(c)){return b}return b+"="+encodeURIComponent(String.interpret(c))}return{initialize:function(b){this._object=Object.isHash(b)?b.toObject():Object.clone(b)},_each:function(c){for(var b in this._object){var d=this._object[b],e=[b,d];e.key=b;e.value=d;c(e)}},set:function(b,c){return this._object[b]=c},get:function(b){if(this._object[b]!==Object.prototype[b]){return this._object[b]}},unset:function(b){var c=this._object[b];delete this._object[b];return c},toObject:function(){return Object.clone(this._object)},keys:function(){return this.pluck("key")},values:function(){return this.pluck("value")},index:function(c){var b=this.detect(function(d){return d.value===c});return b&&b.key},merge:function(b){return this.clone().update(b)},update:function(b){return new Hash(b).inject(this,function(c,d){c.set(d.key,d.value);return c})},toQueryString:function(){return this.inject([],function(d,e){var c=encodeURIComponent(e.key),b=e.value;if(b&&typeof b=="object"){if(Object.isArray(b)){return d.concat(b.map(a.curry(c)))}}else{d.push(a(c,b))}return d}).join("&")},inspect:function(){return"#<Hash:{"+this.map(function(b){return b.map(Object.inspect).join(": ")}).join(", ")+"}>"},toJSON:function(){return Object.toJSON(this.toObject())},clone:function(){return new Hash(this)}}})());Hash.prototype.toTemplateReplacements=Hash.prototype.toObject;Hash.from=$H;var ObjectRange=Class.create(Enumerable,{initialize:function(c,a,b){this.start=c;this.end=a;this.exclusive=b},_each:function(a){var b=this.start;while(this.include(b)){a(b);b=b.succ()}},include:function(a){if(a<this.start){return false}if(this.exclusive){return a<this.end}return a<=this.end}});var $R=function(c,a,b){return new ObjectRange(c,a,b)};var Ajax={getTransport:function(){return Try.these(function(){return new XMLHttpRequest()},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Microsoft.XMLHTTP")})||false},activeRequestCount:0};Ajax.Responders={responders:[],_each:function(a){this.responders._each(a)},register:function(a){if(!this.include(a)){this.responders.push(a)}},unregister:function(a){this.responders=this.responders.without(a)},dispatch:function(d,b,c,a){this.each(function(f){if(Object.isFunction(f[d])){try{f[d].apply(f,[b,c,a])}catch(g){}}})}};Object.extend(Ajax.Responders,Enumerable);Ajax.Responders.register({onCreate:function(){Ajax.activeRequestCount++},onComplete:function(){Ajax.activeRequestCount--}});Ajax.Base=Class.create({initialize:function(a){this.options={method:"post",asynchronous:true,contentType:"application/x-www-form-urlencoded",encoding:"UTF-8",parameters:"",evalJSON:true,evalJS:true};Object.extend(this.options,a||{});this.options.method=this.options.method.toLowerCase();if(Object.isString(this.options.parameters)){this.options.parameters=this.options.parameters.toQueryParams()}else{if(Object.isHash(this.options.parameters)){this.options.parameters=this.options.parameters.toObject()}}}});Ajax.Request=Class.create(Ajax.Base,{_complete:false,initialize:function($super,b,a){$super(a);this.transport=Ajax.getTransport();this.request(b)},request:function(b){this.url=b;this.method=this.options.method;var d=Object.clone(this.options.parameters);if(!["get","post"].include(this.method)){d._method=this.method;this.method="post"}this.parameters=d;if(d=Object.toQueryString(d)){if(this.method=="get"){this.url+=(this.url.include("?")?"&":"?")+d}else{if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)){d+="&_="}}}try{var a=new Ajax.Response(this);if(this.options.onCreate){this.options.onCreate(a)}Ajax.Responders.dispatch("onCreate",this,a);this.transport.open(this.method.toUpperCase(),this.url,this.options.asynchronous);if(this.options.asynchronous){this.respondToReadyState.bind(this).defer(1)}this.transport.onreadystatechange=this.onStateChange.bind(this);this.setRequestHeaders();this.body=this.method=="post"?(this.options.postBody||d):null;this.transport.send(this.body);if(!this.options.asynchronous&&this.transport.overrideMimeType){this.onStateChange()}}catch(c){this.dispatchException(c)}},onStateChange:function(){var a=this.transport.readyState;if(a>1&&!((a==4)&&this._complete)){this.respondToReadyState(this.transport.readyState)}},setRequestHeaders:function(){var e={"X-Requested-With":"XMLHttpRequest","X-Prototype-Version":Prototype.Version,Accept:"text/javascript, text/html, application/xml, text/xml, */*"};if(this.method=="post"){e["Content-type"]=this.options.contentType+(this.options.encoding?"; charset="+this.options.encoding:"");if(this.transport.overrideMimeType&&(navigator.userAgent.match(/Gecko\/(\d{4})/)||[0,2005])[1]<2005){e.Connection="close"}}if(typeof this.options.requestHeaders=="object"){var c=this.options.requestHeaders;if(Object.isFunction(c.push)){for(var b=0,d=c.length;b<d;b+=2){e[c[b]]=c[b+1]}}else{$H(c).each(function(f){e[f.key]=f.value})}}for(var a in e){this.transport.setRequestHeader(a,e[a])}},success:function(){var a=this.getStatus();return!a||(a>=200&&a<300)},getStatus:function(){try{return this.transport.status||0}catch(a){return 0}},respondToReadyState:function(a){var c=Ajax.Request.Events[a],b=new Ajax.Response(this);if(c=="Complete"){try{this._complete=true;(this.options["on"+b.status]||this.options["on"+(this.success()?"Success":"Failure")]||Prototype.emptyFunction)(b,b.headerJSON)}catch(d){this.dispatchException(d)}var f=b.getHeader("Content-type");if(this.options.evalJS=="force"||(this.options.evalJS&&this.isSameOrigin()&&f&&f.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))){this.evalResponse()}}try{(this.options["on"+c]||Prototype.emptyFunction)(b,b.headerJSON);Ajax.Responders.dispatch("on"+c,this,b,b.headerJSON)}catch(d){this.dispatchException(d)}if(c=="Complete"){this.transport.onreadystatechange=Prototype.emptyFunction}},isSameOrigin:function(){var a=this.url.match(/^\s*https?:\/\/[^\/]*/);return!a||(a[0]=="#{protocol}//#{domain}#{port}".interpolate({protocol:location.protocol,domain:document.domain,port:location.port?":"+location.port:""}))},getHeader:function(a){try{return this.transport.getResponseHeader(a)||null}catch(b){return null}},evalResponse:function(){try{return eval((this.transport.responseText||"").unfilterJSON())}catch(e){this.dispatchException(e)}},dispatchException:function(a){(this.options.onException||Prototype.emptyFunction)(this,a);Ajax.Responders.dispatch("onException",this,a)}});Ajax.Request.Events=["Uninitialized","Loading","Loaded","Interactive","Complete"];Ajax.Response=Class.create({initialize:function(c){this.request=c;var d=this.transport=c.transport,a=this.readyState=d.readyState;if((a>2&&!Prototype.Browser.IE)||a==4){this.status=this.getStatus();this.statusText=this.getStatusText();this.responseText=String.interpret(d.responseText);this.headerJSON=this._getHeaderJSON()}if(a==4){var b=d.responseXML;this.responseXML=Object.isUndefined(b)?null:b;this.responseJSON=this._getResponseJSON()}},status:0,statusText:"",getStatus:Ajax.Request.prototype.getStatus,getStatusText:function(){try{return this.transport.statusText||""}catch(a){return""}},getHeader:Ajax.Request.prototype.getHeader,getAllHeaders:function(){try{return this.getAllResponseHeaders()}catch(a){return null}},getResponseHeader:function(a){return this.transport.getResponseHeader(a)},getAllResponseHeaders:function(){return this.transport.getAllResponseHeaders()},_getHeaderJSON:function(){var a=this.getHeader("X-JSON");if(!a){return null}a=decodeURIComponent(escape(a));try{return a.evalJSON(this.request.options.sanitizeJSON||!this.request.isSameOrigin())}catch(b){this.request.dispatchException(b)}},_getResponseJSON:function(){var a=this.request.options;if(!a.evalJSON||(a.evalJSON!="force"&&!(this.getHeader("Content-type")||"").include("application/json"))||this.responseText.blank()){return null}try{return this.responseText.evalJSON(a.sanitizeJSON||!this.request.isSameOrigin())}catch(b){this.request.dispatchException(b)}}});Ajax.Updater=Class.create(Ajax.Request,{initialize:function($super,a,c,b){this.container={success:(a.success||a),failure:(a.failure||(a.success?null:a))};b=Object.clone(b);var d=b.onComplete;b.onComplete=(function(e,f){this.updateContent(e.responseText);if(Object.isFunction(d)){d(e,f)}}).bind(this);$super(c,b)},updateContent:function(d){var c=this.container[this.success()?"success":"failure"],a=this.options;if(!a.evalScripts){d=d.stripScripts()}if(c=$(c)){if(a.insertion){if(Object.isString(a.insertion)){var b={};b[a.insertion]=d;c.insert(b)}else{a.insertion(c,d)}}else{c.update(d)}}}});Ajax.PeriodicalUpdater=Class.create(Ajax.Base,{initialize:function($super,a,c,b){$super(b);this.onComplete=this.options.onComplete;this.frequency=(this.options.frequency||2);this.decay=(this.options.decay||1);this.updater={};this.container=a;this.url=c;this.start()},start:function(){this.options.onComplete=this.updateComplete.bind(this);this.onTimerEvent()},stop:function(){this.updater.options.onComplete=undefined;clearTimeout(this.timer);(this.onComplete||Prototype.emptyFunction).apply(this,arguments)},updateComplete:function(a){if(this.options.decay){this.decay=(a.responseText==this.lastText?this.decay*this.options.decay:1);this.lastText=a.responseText}this.timer=this.onTimerEvent.bind(this).delay(this.decay*this.frequency)},onTimerEvent:function(){this.updater=new Ajax.Updater(this.container,this.url,this.options)}});function $(b){if(arguments.length>1){for(var a=0,d=[],c=arguments.length;a<c;a++){d.push($(arguments[a]))}return d}if(Object.isString(b)){b=document.getElementById(b)}return Element.extend(b)}if(Prototype.BrowserFeatures.XPath){document._getElementsByXPath=function(f,a){var c=[];var e=document.evaluate(f,$(a)||document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);for(var b=0,d=e.snapshotLength;b<d;b++){c.push(Element.extend(e.snapshotItem(b)))}return c}}if(!window.Node){var Node={}}if(!Node.ELEMENT_NODE){Object.extend(Node,{ELEMENT_NODE:1,ATTRIBUTE_NODE:2,TEXT_NODE:3,CDATA_SECTION_NODE:4,ENTITY_REFERENCE_NODE:5,ENTITY_NODE:6,PROCESSING_INSTRUCTION_NODE:7,COMMENT_NODE:8,DOCUMENT_NODE:9,DOCUMENT_TYPE_NODE:10,DOCUMENT_FRAGMENT_NODE:11,NOTATION_NODE:12})}(function(){var a=this.Element;this.Element=function(d,c){c=c||{};d=d.toLowerCase();var b=Element.cache;if(Prototype.Browser.IE&&c.name){d="<"+d+' name="'+c.name+'">';delete c.name;return Element.writeAttribute(document.createElement(d),c)}if(!b[d]){b[d]=Element.extend(document.createElement(d))}return Element.writeAttribute(b[d].cloneNode(false),c)};Object.extend(this.Element,a||{});if(a){this.Element.prototype=a.prototype}}).call(window);Element.cache={};Element.Methods={visible:function(a){return $(a).style.display!="none"},toggle:function(a){a=$(a);Element[Element.visible(a)?"hide":"show"](a);return a},hide:function(a){a=$(a);a.style.display="none";return a},show:function(a){a=$(a);a.style.display="";return a},remove:function(a){a=$(a);a.parentNode.removeChild(a);return a},update:function(a,b){a=$(a);if(b&&b.toElement){b=b.toElement()}if(Object.isElement(b)){return a.update().insert(b)}b=Object.toHTML(b);a.innerHTML=b.stripScripts();b.evalScripts.bind(b).defer();return a},replace:function(b,c){b=$(b);if(c&&c.toElement){c=c.toElement()}else{if(!Object.isElement(c)){c=Object.toHTML(c);var a=b.ownerDocument.createRange();a.selectNode(b);c.evalScripts.bind(c).defer();c=a.createContextualFragment(c.stripScripts())}}b.parentNode.replaceChild(c,b);return b},insert:function(c,e){c=$(c);if(Object.isString(e)||Object.isNumber(e)||Object.isElement(e)||(e&&(e.toElement||e.toHTML))){e={bottom:e}}var d,f,b,g;for(var a in e){d=e[a];a=a.toLowerCase();f=Element._insertionTranslations[a];if(d&&d.toElement){d=d.toElement()}if(Object.isElement(d)){f(c,d);continue}d=Object.toHTML(d);b=((a=="before"||a=="after")?c.parentNode:c).tagName.toUpperCase();g=Element._getContentFromAnonymousElement(b,d.stripScripts());if(a=="top"||a=="after"){g.reverse()}g.each(f.curry(c));d.evalScripts.bind(d).defer()}return c},wrap:function(b,c,a){b=$(b);if(Object.isElement(c)){$(c).writeAttribute(a||{})}else{if(Object.isString(c)){c=new Element(c,a)}else{c=new Element("div",c)}}if(b.parentNode){b.parentNode.replaceChild(c,b)}c.appendChild(b);return c},inspect:function(b){b=$(b);var a="<"+b.tagName.toLowerCase();$H({id:"id",className:"class"}).each(function(f){var e=f.first(),c=f.last();var d=(b[e]||"").toString();if(d){a+=" "+c+"="+d.inspect(true)}});return a+">"},recursivelyCollect:function(a,c){a=$(a);var b=[];while(a=a[c]){if(a.nodeType==1){b.push(Element.extend(a))}}return b},ancestors:function(a){return $(a).recursivelyCollect("parentNode")},descendants:function(a){return $(a).select("*")},firstDescendant:function(a){a=$(a).firstChild;while(a&&a.nodeType!=1){a=a.nextSibling}return $(a)},immediateDescendants:function(a){if(!(a=$(a).firstChild)){return[]}while(a&&a.nodeType!=1){a=a.nextSibling}if(a){return[a].concat($(a).nextSiblings())}return[]},previousSiblings:function(a){return $(a).recursivelyCollect("previousSibling")},nextSiblings:function(a){return $(a).recursivelyCollect("nextSibling")},siblings:function(a){a=$(a);return a.previousSiblings().reverse().concat(a.nextSiblings())},match:function(b,a){if(Object.isString(a)){a=new Selector(a)}return a.match($(b))},up:function(b,d,a){b=$(b);if(arguments.length==1){return $(b.parentNode)}var c=b.ancestors();return Object.isNumber(d)?c[d]:Selector.findElement(c,d,a)},down:function(b,c,a){b=$(b);if(arguments.length==1){return b.firstDescendant()}return Object.isNumber(c)?b.descendants()[c]:Element.select(b,c)[a||0]},previous:function(b,d,a){b=$(b);if(arguments.length==1){return $(Selector.handlers.previousElementSibling(b))}var c=b.previousSiblings();return Object.isNumber(d)?c[d]:Selector.findElement(c,d,a)},next:function(c,d,b){c=$(c);if(arguments.length==1){return $(Selector.handlers.nextElementSibling(c))}var a=c.nextSiblings();return Object.isNumber(d)?a[d]:Selector.findElement(a,d,b)},select:function(){var a=$A(arguments),b=$(a.shift());return Selector.findChildElements(b,a)},adjacent:function(){var a=$A(arguments),b=$(a.shift());return Selector.findChildElements(b.parentNode,a).without(b)},identify:function(b){b=$(b);var c=b.readAttribute("id"),a=arguments.callee;if(c){return c}do{c="anonymous_element_"+a.counter++}while($(c));b.writeAttribute("id",c);return c},readAttribute:function(c,a){c=$(c);if(Prototype.Browser.IE){var b=Element._attributeTranslations.read;if(b.values[a]){return b.values[a](c,a)}if(b.names[a]){a=b.names[a]}if(a.include(":")){return(!c.attributes||!c.attributes[a])?null:c.attributes[a].value}}return c.getAttribute(a)},writeAttribute:function(e,c,f){e=$(e);var b={},d=Element._attributeTranslations.write;if(typeof c=="object"){b=c}else{b[c]=Object.isUndefined(f)?true:f}for(var a in b){c=d.names[a]||a;f=b[a];if(d.values[a]){c=d.values[a](e,f)}if(f===false||f===null){e.removeAttribute(c)}else{if(f===true){e.setAttribute(c,c)}else{e.setAttribute(c,f)}}}return e},getHeight:function(a){return $(a).getDimensions().height},getWidth:function(a){return $(a).getDimensions().width},classNames:function(a){return new Element.ClassNames(a)},hasClassName:function(a,b){if(!(a=$(a))){return}var c=a.className;return(c.length>0&&(c==b||new RegExp("(^|\\s)"+b+"(\\s|$)").test(c)))},addClassName:function(a,b){if(!(a=$(a))){return}if(!a.hasClassName(b)){a.className+=(a.className?" ":"")+b}return a},removeClassName:function(a,b){if(!(a=$(a))){return}a.className=a.className.replace(new RegExp("(^|\\s+)"+b+"(\\s+|$)")," ").strip();return a},toggleClassName:function(a,b){if(!(a=$(a))){return}return a[a.hasClassName(b)?"removeClassName":"addClassName"](b)},cleanWhitespace:function(b){b=$(b);var c=b.firstChild;while(c){var a=c.nextSibling;if(c.nodeType==3&&!/\S/.test(c.nodeValue)){b.removeChild(c)}c=a}return b},empty:function(a){return $(a).innerHTML.blank()},descendantOf:function(b,a){b=$(b),a=$(a);if(b.compareDocumentPosition){return(b.compareDocumentPosition(a)&8)===8}if(a.contains){return a.contains(b)&&a!==b}while(b=b.parentNode){if(b==a){return true}}return false},scrollTo:function(a){a=$(a);var b=a.cumulativeOffset();window.scrollTo(b[0],b[1]);return a},getStyle:function(b,c){b=$(b);c=c=="float"?"cssFloat":c.camelize();var d=b.style[c];if(!d||d=="auto"){var a=document.defaultView.getComputedStyle(b,null);d=a?a[c]:null}if(c=="opacity"){return d?parseFloat(d):1}return d=="auto"?null:d},getOpacity:function(a){return $(a).getStyle("opacity")},setStyle:function(b,c){b=$(b);var e=b.style,a;if(Object.isString(c)){b.style.cssText+=";"+c;return c.include("opacity")?b.setOpacity(c.match(/opacity:\s*(\d?\.?\d*)/)[1]):b}for(var d in c){if(d=="opacity"){b.setOpacity(c[d])}else{e[(d=="float"||d=="cssFloat")?(Object.isUndefined(e.styleFloat)?"cssFloat":"styleFloat"):d]=c[d]}}return b},setOpacity:function(a,b){a=$(a);a.style.opacity=(b==1||b==="")?"":(b<0.00001)?0:b;return a},getDimensions:function(c){c=$(c);var g=c.getStyle("display");if(g!="none"&&g!=null){return{width:c.offsetWidth,height:c.offsetHeight}}var b=c.style;var f=b.visibility;var d=b.position;var a=b.display;b.visibility="hidden";b.position="absolute";b.display="block";var h=c.clientWidth;var e=c.clientHeight;b.display=a;b.position=d;b.visibility=f;return{width:h,height:e}},makePositioned:function(a){a=$(a);var b=Element.getStyle(a,"position");if(b=="static"||!b){a._madePositioned=true;a.style.position="relative";if(Prototype.Browser.Opera){a.style.top=0;a.style.left=0}}return a},undoPositioned:function(a){a=$(a);if(a._madePositioned){a._madePositioned=undefined;a.style.position=a.style.top=a.style.left=a.style.bottom=a.style.right=""}return a},makeClipping:function(a){a=$(a);if(a._overflow){return a}a._overflow=Element.getStyle(a,"overflow")||"auto";if(a._overflow!=="hidden"){a.style.overflow="hidden"}return a},undoClipping:function(a){a=$(a);if(!a._overflow){return a}a.style.overflow=a._overflow=="auto"?"":a._overflow;a._overflow=null;return a},cumulativeOffset:function(b){var a=0,c=0;do{a+=b.offsetTop||0;c+=b.offsetLeft||0;b=b.offsetParent}while(b);return Element._returnOffset(c,a)},positionedOffset:function(b){var a=0,d=0;do{a+=b.offsetTop||0;d+=b.offsetLeft||0;b=b.offsetParent;if(b){if(b.tagName.toUpperCase()=="BODY"){break}var c=Element.getStyle(b,"position");if(c!=="static"){break}}}while(b);return Element._returnOffset(d,a)},absolutize:function(b){b=$(b);if(b.getStyle("position")=="absolute"){return b}var d=b.positionedOffset();var f=d[1];var e=d[0];var c=b.clientWidth;var a=b.clientHeight;b._originalLeft=e-parseFloat(b.style.left||0);b._originalTop=f-parseFloat(b.style.top||0);b._originalWidth=b.style.width;b._originalHeight=b.style.height;b.style.position="absolute";b.style.top=f+"px";b.style.left=e+"px";b.style.width=c+"px";b.style.height=a+"px";return b},relativize:function(a){a=$(a);if(a.getStyle("position")=="relative"){return a}a.style.position="relative";var c=parseFloat(a.style.top||0)-(a._originalTop||0);var b=parseFloat(a.style.left||0)-(a._originalLeft||0);a.style.top=c+"px";a.style.left=b+"px";a.style.height=a._originalHeight;a.style.width=a._originalWidth;return a},cumulativeScrollOffset:function(b){var a=0,c=0;do{a+=b.scrollTop||0;c+=b.scrollLeft||0;b=b.parentNode}while(b);return Element._returnOffset(c,a)},getOffsetParent:function(b){b=$(b);var d=b.offsetParent,a=document.body,c=document.documentElement;if(d&&d!==c){return $(d)}if(d===c||b===c||b===a){return $(a)}while((b=b.parentNode)&&b!==a){if(Element.getStyle(b,"position")!="static"){return $(b)}}return $(a)},viewportOffset:function(d){d=$(d);var b=d,a=0,c=0;do{a+=b.offsetTop||0;c+=b.offsetLeft||0}while((b=b.getOffsetParent())!=document.body);b=d;do{if(!Prototype.Browser.Opera||(b.tagName&&(b.tagName.toUpperCase()=="BODY"))){a-=b.scrollTop||0;c-=b.scrollLeft||0}}while(b=b.parentNode);return Element._returnOffset(c,a)},clonePosition:function(b,d){var a=Object.extend({setLeft:true,setTop:true,setWidth:true,setHeight:true,offsetTop:0,offsetLeft:0},arguments[2]||{});d=$(d);var e=d.viewportOffset();b=$(b);var f=[0,0];var c=null;if(Element.getStyle(b,"position")=="absolute"){c=b.getOffsetParent();f=c.viewportOffset()}if(c==document.body){f[0]-=document.body.offsetLeft;f[1]-=document.body.offsetTop}if(a.setLeft){b.style.left=(e[0]-f[0]+a.offsetLeft)+"px"}if(a.setTop){b.style.top=(e[1]-f[1]+a.offsetTop)+"px"}if(a.setWidth){b.style.width=d.offsetWidth+"px"}if(a.setHeight){b.style.height=d.offsetHeight+"px"}return b}};Element.Methods.identify.counter=1;Object.extend(Element.Methods,{getElementsBySelector:Element.Methods.select,childElements:Element.Methods.immediateDescendants});Element._attributeTranslations={write:{names:{className:"class",htmlFor:"for"},values:{}}};if(Prototype.Browser.Opera){Element.Methods.getStyle=Element.Methods.getStyle.wrap(function(d,b,c){switch(c){case"left":case"top":case"right":case"bottom":if(d(b,"position")==="static"){return null}case"height":case"width":if(!Element.visible(b)){return null}var e=parseInt(d(b,c),10);if(e!==b["offset"+c.capitalize()]){return e+"px"}var a;if(c==="height"){a=["border-top-width","padding-top","padding-bottom","border-bottom-width"]}else{a=["border-left-width","padding-left","padding-right","border-right-width"]}return a.inject(e,function(f,g){var h=d(b,g);return h===null?f:f-parseInt(h,10)})+"px";default:return d(b,c)}});Element.Methods.readAttribute=Element.Methods.readAttribute.wrap(function(c,a,b){if(b==="title"){return a.title}return c(a,b)})}else{if(Prototype.Browser.IE){Element.Methods.getOffsetParent=Element.Methods.getOffsetParent.wrap(function(c,b){b=$(b);try{b.offsetParent}catch(f){return $(document.body)}var a=b.getStyle("position");if(a!=="static"){return c(b)}b.setStyle({position:"relative"});var d=c(b);b.setStyle({position:a});return d});$w("positionedOffset viewportOffset").each(function(a){Element.Methods[a]=Element.Methods[a].wrap(function(f,c){c=$(c);try{c.offsetParent}catch(h){return Element._returnOffset(0,0)}var b=c.getStyle("position");if(b!=="static"){return f(c)}var d=c.getOffsetParent();if(d&&d.getStyle("position")==="fixed"){d.setStyle({zoom:1})}c.setStyle({position:"relative"});var g=f(c);c.setStyle({position:b});return g})});Element.Methods.cumulativeOffset=Element.Methods.cumulativeOffset.wrap(function(b,a){try{a.offsetParent}catch(c){return Element._returnOffset(0,0)}return b(a)});Element.Methods.getStyle=function(a,b){a=$(a);b=(b=="float"||b=="cssFloat")?"styleFloat":b.camelize();var c=a.style[b];if(!c&&a.currentStyle){c=a.currentStyle[b]}if(b=="opacity"){if(c=(a.getStyle("filter")||"").match(/alpha\(opacity=(.*)\)/)){if(c[1]){return parseFloat(c[1])/100}}return 1}if(c=="auto"){if((b=="width"||b=="height")&&(a.getStyle("display")!="none")){return a["offset"+b.capitalize()]+"px"}return null}return c};Element.Methods.setOpacity=function(b,e){function f(g){return g.replace(/alpha\([^\)]*\)/gi,"")}b=$(b);var a=b.currentStyle;if((a&&!a.hasLayout)||(!a&&b.style.zoom=="normal")){b.style.zoom=1}var d=b.getStyle("filter"),c=b.style;if(e==1||e===""){(d=f(d))?c.filter=d:c.removeAttribute("filter");return b}else{if(e<0.00001){e=0}}c.filter=f(d)+"alpha(opacity="+(e*100)+")";return b};Element._attributeTranslations={read:{names:{"class":"className","for":"htmlFor"},values:{_getAttr:function(a,b){return a.getAttribute(b,2)},_getAttrNode:function(a,c){var b=a.getAttributeNode(c);return b?b.value:""},_getEv:function(a,b){b=a.getAttribute(b);return b?b.toString().slice(23,-2):null},_flag:function(a,b){return $(a).hasAttribute(b)?b:null},style:function(a){return a.style.cssText.toLowerCase()},title:function(a){return a.title}}}};Element._attributeTranslations.write={names:Object.extend({cellpadding:"cellPadding",cellspacing:"cellSpacing"},Element._attributeTranslations.read.names),values:{checked:function(a,b){a.checked=!!b},style:function(a,b){a.style.cssText=b?b:""}}};Element._attributeTranslations.has={};$w("colSpan rowSpan vAlign dateTime accessKey tabIndex encType maxLength readOnly longDesc frameBorder").each(function(a){Element._attributeTranslations.write.names[a.toLowerCase()]=a;Element._attributeTranslations.has[a.toLowerCase()]=a});(function(a){Object.extend(a,{href:a._getAttr,src:a._getAttr,type:a._getAttr,action:a._getAttrNode,disabled:a._flag,checked:a._flag,readonly:a._flag,multiple:a._flag,onload:a._getEv,onunload:a._getEv,onclick:a._getEv,ondblclick:a._getEv,onmousedown:a._getEv,onmouseup:a._getEv,onmouseover:a._getEv,onmousemove:a._getEv,onmouseout:a._getEv,onfocus:a._getEv,onblur:a._getEv,onkeypress:a._getEv,onkeydown:a._getEv,onkeyup:a._getEv,onsubmit:a._getEv,onreset:a._getEv,onselect:a._getEv,onchange:a._getEv})})(Element._attributeTranslations.read.values)}else{if(Prototype.Browser.Gecko&&/rv:1\.8\.0/.test(navigator.userAgent)){Element.Methods.setOpacity=function(a,b){a=$(a);a.style.opacity=(b==1)?0.999999:(b==="")?"":(b<0.00001)?0:b;return a}}else{if(Prototype.Browser.WebKit){Element.Methods.setOpacity=function(a,b){a=$(a);a.style.opacity=(b==1||b==="")?"":(b<0.00001)?0:b;if(b==1){if(a.tagName.toUpperCase()=="IMG"&&a.width){a.width++;a.width--}else{try{var d=document.createTextNode(" ");a.appendChild(d);a.removeChild(d)}catch(c){}}}return a};Element.Methods.cumulativeOffset=function(b){var a=0,c=0;do{a+=b.offsetTop||0;c+=b.offsetLeft||0;if(b.offsetParent==document.body){if(Element.getStyle(b,"position")=="absolute"){break}}b=b.offsetParent}while(b);return Element._returnOffset(c,a)}}}}}if(Prototype.Browser.IE||Prototype.Browser.Opera){Element.Methods.update=function(b,c){b=$(b);if(c&&c.toElement){c=c.toElement()}if(Object.isElement(c)){return b.update().insert(c)}c=Object.toHTML(c);var a=b.tagName.toUpperCase();if(a in Element._insertionTranslations.tags){$A(b.childNodes).each(function(d){b.removeChild(d)});Element._getContentFromAnonymousElement(a,c.stripScripts()).each(function(d){b.appendChild(d)})}else{b.innerHTML=c.stripScripts()}c.evalScripts.bind(c).defer();return b}}if("outerHTML"in document.createElement("div")){Element.Methods.replace=function(c,e){c=$(c);if(e&&e.toElement){e=e.toElement()}if(Object.isElement(e)){c.parentNode.replaceChild(e,c);return c}e=Object.toHTML(e);var d=c.parentNode,b=d.tagName.toUpperCase();if(Element._insertionTranslations.tags[b]){var f=c.next();var a=Element._getContentFromAnonymousElement(b,e.stripScripts());d.removeChild(c);if(f){a.each(function(g){d.insertBefore(g,f)})}else{a.each(function(g){d.appendChild(g)})}}else{c.outerHTML=e.stripScripts()}e.evalScripts.bind(e).defer();return c}}Element._returnOffset=function(b,c){var a=[b,c];a.left=b;a.top=c;return a};Element._getContentFromAnonymousElement=function(c,b){var d=new Element("div"),a=Element._insertionTranslations.tags[c];if(a){d.innerHTML=a[0]+b+a[1];a[2].times(function(){d=d.firstChild})}else{d.innerHTML=b}return $A(d.childNodes)};Element._insertionTranslations={before:function(a,b){a.parentNode.insertBefore(b,a)},top:function(a,b){a.insertBefore(b,a.firstChild)},bottom:function(a,b){a.appendChild(b)},after:function(a,b){a.parentNode.insertBefore(b,a.nextSibling)},tags:{TABLE:["<table>","</table>",1],TBODY:["<table><tbody>","</tbody></table>",2],TR:["<table><tbody><tr>","</tr></tbody></table>",3],TD:["<table><tbody><tr><td>","</td></tr></tbody></table>",4],SELECT:["<select>","</select>",1]}};(function(){Object.extend(this.tags,{THEAD:this.tags.TBODY,TFOOT:this.tags.TBODY,TH:this.tags.TD})}).call(Element._insertionTranslations);Element.Methods.Simulated={hasAttribute:function(a,c){c=Element._attributeTranslations.has[c]||c;var b=$(a).getAttributeNode(c);return!!(b&&b.specified)}};Element.Methods.ByTag={};Object.extend(Element,Element.Methods);if(!Prototype.BrowserFeatures.ElementExtensions&&document.createElement("div")["__proto__"]){window.HTMLElement={};window.HTMLElement.prototype=document.createElement("div")["__proto__"];Prototype.BrowserFeatures.ElementExtensions=true}Element.extend=(function(){if(Prototype.BrowserFeatures.SpecificElementExtensions){return Prototype.K}var a={},b=Element.Methods.ByTag;var c=Object.extend(function(f){if(!f||f._extendedByPrototype||f.nodeType!=1||f==window){return f}var d=Object.clone(a),e=f.tagName.toUpperCase(),h,g;if(b[e]){Object.extend(d,b[e])}for(h in d){g=d[h];if(Object.isFunction(g)&&!(h in f)){f[h]=g.methodize()}}f._extendedByPrototype=Prototype.emptyFunction;return f},{refresh:function(){if(!Prototype.BrowserFeatures.ElementExtensions){Object.extend(a,Element.Methods);Object.extend(a,Element.Methods.Simulated)}}});c.refresh();return c})();Element.hasAttribute=function(a,b){if(a.hasAttribute){return a.hasAttribute(b)}return Element.Methods.Simulated.hasAttribute(a,b)};Element.addMethods=function(c){var h=Prototype.BrowserFeatures,d=Element.Methods.ByTag;if(!c){Object.extend(Form,Form.Methods);Object.extend(Form.Element,Form.Element.Methods);Object.extend(Element.Methods.ByTag,{FORM:Object.clone(Form.Methods),INPUT:Object.clone(Form.Element.Methods),SELECT:Object.clone(Form.Element.Methods),TEXTAREA:Object.clone(Form.Element.Methods)})}if(arguments.length==2){var b=c;c=arguments[1]}if(!b){Object.extend(Element.Methods,c||{})}else{if(Object.isArray(b)){b.each(g)}else{g(b)}}function g(j){j=j.toUpperCase();if(!Element.Methods.ByTag[j]){Element.Methods.ByTag[j]={}}Object.extend(Element.Methods.ByTag[j],c)}function a(l,k,j){j=j||false;for(var n in l){var m=l[n];if(!Object.isFunction(m)){continue}if(!j||!(n in k)){k[n]=m.methodize()}}}function e(l){var j;var k={OPTGROUP:"OptGroup",TEXTAREA:"TextArea",P:"Paragraph",FIELDSET:"FieldSet",UL:"UList",OL:"OList",DL:"DList",DIR:"Directory",H1:"Heading",H2:"Heading",H3:"Heading",H4:"Heading",H5:"Heading",H6:"Heading",Q:"Quote",INS:"Mod",DEL:"Mod",A:"Anchor",IMG:"Image",CAPTION:"TableCaption",COL:"TableCol",COLGROUP:"TableCol",THEAD:"TableSection",TFOOT:"TableSection",TBODY:"TableSection",TR:"TableRow",TH:"TableCell",TD:"TableCell",FRAMESET:"FrameSet",IFRAME:"IFrame"};if(k[l]){j="HTML"+k[l]+"Element"}if(window[j]){return window[j]}j="HTML"+l+"Element";if(window[j]){return window[j]}j="HTML"+l.capitalize()+"Element";if(window[j]){return window[j]}window[j]={};window[j].prototype=document.createElement(l)["__proto__"];return window[j]}if(h.ElementExtensions){a(Element.Methods,HTMLElement.prototype);a(Element.Methods.Simulated,HTMLElement.prototype,true)}if(h.SpecificElementExtensions){for(var i in Element.Methods.ByTag){var f=e(i);if(Object.isUndefined(f)){continue}a(d[i],f.prototype)}}Object.extend(Element,Element.Methods);delete Element.ByTag;if(Element.extend.refresh){Element.extend.refresh()}Element.cache={}};document.viewport={getDimensions:function(){var a={},b=Prototype.Browser;$w("width height").each(function(e){var c=e.capitalize();if(b.WebKit&&!document.evaluate){a[e]=self["inner"+c]}else{if(b.Opera&&parseFloat(window.opera.version())<9.5){a[e]=document.body["client"+c]}else{a[e]=document.documentElement["client"+c]}}});return a},getWidth:function(){return this.getDimensions().width},getHeight:function(){return this.getDimensions().height},getScrollOffsets:function(){return Element._returnOffset(window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft,window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop)}};var Selector=Class.create({initialize:function(a){this.expression=a.strip();if(this.shouldUseSelectorsAPI()){this.mode="selectorsAPI"}else{if(this.shouldUseXPath()){this.mode="xpath";this.compileXPathMatcher()}else{this.mode="normal";this.compileMatcher()}}},shouldUseXPath:function(){if(!Prototype.BrowserFeatures.XPath){return false}var a=this.expression;if(Prototype.Browser.WebKit&&(a.include("-of-type")||a.include(":empty"))){return false}if((/(\[[\w-]*?:|:checked)/).test(a)){return false}return true},shouldUseSelectorsAPI:function(){if(!Prototype.BrowserFeatures.SelectorsAPI){return false}if(!Selector._div){Selector._div=new Element("div")}try{Selector._div.querySelector(this.expression)}catch(a){return false}return true},compileMatcher:function(){var e=this.expression,ps=Selector.patterns,h=Selector.handlers,c=Selector.criteria,le,p,m;if(Selector._cache[e]){this.matcher=Selector._cache[e];return}this.matcher=["this.matcher = function(root) {","var r = root, h = Selector.handlers, c = false, n;"];while(e&&le!=e&&(/\S/).test(e)){le=e;for(var i in ps){p=ps[i];if(m=e.match(p)){this.matcher.push(Object.isFunction(c[i])?c[i](m):new Template(c[i]).evaluate(m));e=e.replace(m[0],"");break}}}this.matcher.push("return h.unique(n);\n}");eval(this.matcher.join("\n"));Selector._cache[this.expression]=this.matcher},compileXPathMatcher:function(){var f=this.expression,g=Selector.patterns,b=Selector.xpath,d,a;if(Selector._cache[f]){this.xpath=Selector._cache[f];return}this.matcher=[".//*"];while(f&&d!=f&&(/\S/).test(f)){d=f;for(var c in g){if(a=f.match(g[c])){this.matcher.push(Object.isFunction(b[c])?b[c](a):new Template(b[c]).evaluate(a));f=f.replace(a[0],"");break}}}this.xpath=this.matcher.join("");Selector._cache[this.expression]=this.xpath},findElements:function(a){a=a||document;var c=this.expression,b;switch(this.mode){case"selectorsAPI":if(a!==document){var d=a.id,f=$(a).identify();c="#"+f+" "+c}b=$A(a.querySelectorAll(c)).map(Element.extend);a.id=d;return b;case"xpath":return document._getElementsByXPath(this.xpath,a);default:return this.matcher(a)}},match:function(j){this.tokens=[];var o=this.expression,a=Selector.patterns,f=Selector.assertions;var b,d,g;while(o&&b!==o&&(/\S/).test(o)){b=o;for(var k in a){d=a[k];if(g=o.match(d)){if(f[k]){this.tokens.push([k,Object.clone(g)]);o=o.replace(g[0],"")}else{return this.findElements(document).include(j)}}}}var n=true,c,l;for(var k=0,h;h=this.tokens[k];k++){c=h[0],l=h[1];if(!Selector.assertions[c](j,l)){n=false;break}}return n},toString:function(){return this.expression},inspect:function(){return"#<Selector:"+this.expression.inspect()+">"}});Object.extend(Selector,{_cache:{},xpath:{descendant:"//*",child:"/*",adjacent:"/following-sibling::*[1]",laterSibling:"/following-sibling::*",tagName:function(a){if(a[1]=="*"){return""}return"[local-name()='"+a[1].toLowerCase()+"' or local-name()='"+a[1].toUpperCase()+"']"},className:"[contains(concat(' ', @class, ' '), ' #{1} ')]",id:"[@id='#{1}']",attrPresence:function(a){a[1]=a[1].toLowerCase();return new Template("[@#{1}]").evaluate(a)},attr:function(a){a[1]=a[1].toLowerCase();a[3]=a[5]||a[6];return new Template(Selector.xpath.operators[a[2]]).evaluate(a)},pseudo:function(a){var b=Selector.xpath.pseudos[a[1]];if(!b){return""}if(Object.isFunction(b)){return b(a)}return new Template(Selector.xpath.pseudos[a[1]]).evaluate(a)},operators:{"=":"[@#{1}='#{3}']","!=":"[@#{1}!='#{3}']","^=":"[starts-with(@#{1}, '#{3}')]","$=":"[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']","*=":"[contains(@#{1}, '#{3}')]","~=":"[contains(concat(' ', @#{1}, ' '), ' #{3} ')]","|=":"[contains(concat('-', @#{1}, '-'), '-#{3}-')]"},pseudos:{"first-child":"[not(preceding-sibling::*)]","last-child":"[not(following-sibling::*)]","only-child":"[not(preceding-sibling::* or following-sibling::*)]",empty:"[count(*) = 0 and (count(text()) = 0)]",checked:"[@checked]",disabled:"[(@disabled) and (@type!='hidden')]",enabled:"[not(@disabled) and (@type!='hidden')]",not:function(b){var j=b[6],h=Selector.patterns,a=Selector.xpath,f,c;var g=[];while(j&&f!=j&&(/\S/).test(j)){f=j;for(var d in h){if(b=j.match(h[d])){c=Object.isFunction(a[d])?a[d](b):new Template(a[d]).evaluate(b);g.push("("+c.substring(1,c.length-1)+")");j=j.replace(b[0],"");break}}}return"[not("+g.join(" and ")+")]"},"nth-child":function(a){return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ",a)},"nth-last-child":function(a){return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ",a)},"nth-of-type":function(a){return Selector.xpath.pseudos.nth("position() ",a)},"nth-last-of-type":function(a){return Selector.xpath.pseudos.nth("(last() + 1 - position()) ",a)},"first-of-type":function(a){a[6]="1";return Selector.xpath.pseudos["nth-of-type"](a)},"last-of-type":function(a){a[6]="1";return Selector.xpath.pseudos["nth-last-of-type"](a)},"only-of-type":function(a){var b=Selector.xpath.pseudos;return b["first-of-type"](a)+b["last-of-type"](a)},nth:function(g,e){var h,i=e[6],d;if(i=="even"){i="2n+0"}if(i=="odd"){i="2n+1"}if(h=i.match(/^(\d+)$/)){return"["+g+"= "+h[1]+"]"}if(h=i.match(/^(-?\d*)?n(([+-])(\d+))?/)){if(h[1]=="-"){h[1]=-1}var f=h[1]?Number(h[1]):1;var c=h[2]?Number(h[2]):0;d="[((#{fragment} - #{b}) mod #{a} = 0) and ((#{fragment} - #{b}) div #{a} >= 0)]";return new Template(d).evaluate({fragment:g,a:f,b:c})}}}},criteria:{tagName:'n = h.tagName(n, r, "#{1}", c);      c = false;',className:'n = h.className(n, r, "#{1}", c);    c = false;',id:'n = h.id(n, r, "#{1}", c);           c = false;',attrPresence:'n = h.attrPresence(n, r, "#{1}", c); c = false;',attr:function(a){a[3]=(a[5]||a[6]);return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(a)},pseudo:function(a){if(a[6]){a[6]=a[6].replace(/"/g,'\\"')}return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(a)},descendant:'c = "descendant";',child:'c = "child";',adjacent:'c = "adjacent";',laterSibling:'c = "laterSibling";'},patterns:{laterSibling:/^\s*~\s*/,child:/^\s*>\s*/,adjacent:/^\s*\+\s*/,descendant:/^\s/,tagName:/^\s*(\*|[\w\-]+)(\b|$)?/,id:/^#([\w\-\*]+)(\b|$)/,className:/^\.([\w\-\*]+)(\b|$)/,pseudo:/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,attrPresence:/^\[((?:[\w]+:)?[\w]+)\]/,attr:/\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/},assertions:{tagName:function(a,b){return b[1].toUpperCase()==a.tagName.toUpperCase()},className:function(a,b){return Element.hasClassName(a,b[1])},id:function(a,b){return a.id===b[1]},attrPresence:function(a,b){return Element.hasAttribute(a,b[1])},attr:function(b,c){var a=Element.readAttribute(b,c[1]);return a&&Selector.operators[c[2]](a,c[5]||c[6])}},handlers:{concat:function(d,c){for(var e=0,f;f=c[e];e++){d.push(f)}return d},mark:function(a){var d=Prototype.emptyFunction;for(var b=0,c;c=a[b];b++){c._countedByPrototype=d}return a},unmark:function(a){for(var b=0,c;c=a[b];b++){c._countedByPrototype=undefined}return a},index:function(a,d,g){a._countedByPrototype=Prototype.emptyFunction;if(d){for(var b=a.childNodes,e=b.length-1,c=1;e>=0;e--){var f=b[e];if(f.nodeType==1&&(!g||f._countedByPrototype)){f.nodeIndex=c++}}}else{for(var e=0,c=1,b=a.childNodes;f=b[e];e++){if(f.nodeType==1&&(!g||f._countedByPrototype)){f.nodeIndex=c++}}}},unique:function(b){if(b.length==0){return b}var d=[],e;for(var c=0,a=b.length;c<a;c++){if(!(e=b[c])._countedByPrototype){e._countedByPrototype=Prototype.emptyFunction;d.push(Element.extend(e))}}return Selector.handlers.unmark(d)},descendant:function(a){var d=Selector.handlers;for(var c=0,b=[],e;e=a[c];c++){d.concat(b,e.getElementsByTagName("*"))}return b},child:function(a){var e=Selector.handlers;for(var d=0,c=[],f;f=a[d];d++){for(var b=0,g;g=f.childNodes[b];b++){if(g.nodeType==1&&g.tagName!="!"){c.push(g)}}}return c},adjacent:function(a){for(var c=0,b=[],e;e=a[c];c++){var d=this.nextElementSibling(e);if(d){b.push(d)}}return b},laterSibling:function(a){var d=Selector.handlers;for(var c=0,b=[],e;e=a[c];c++){d.concat(b,Element.nextSiblings(e))}return b},nextElementSibling:function(a){while(a=a.nextSibling){if(a.nodeType==1){return a}}return null},previousElementSibling:function(a){while(a=a.previousSibling){if(a.nodeType==1){return a}}return null},tagName:function(a,j,c,b){var k=c.toUpperCase();var e=[],g=Selector.handlers;if(a){if(b){if(b=="descendant"){for(var f=0,d;d=a[f];f++){g.concat(e,d.getElementsByTagName(c))}return e}else{a=this[b](a)}if(c=="*"){return a}}for(var f=0,d;d=a[f];f++){if(d.tagName.toUpperCase()===k){e.push(d)}}return e}else{return j.getElementsByTagName(c)}},id:function(b,a,j,f){var g=$(j),d=Selector.handlers;if(!g){return[]}if(!b&&a==document){return[g]}if(b){if(f){if(f=="child"){for(var c=0,e;e=b[c];c++){if(g.parentNode==e){return[g]}}}else{if(f=="descendant"){for(var c=0,e;e=b[c];c++){if(Element.descendantOf(g,e)){return[g]}}}else{if(f=="adjacent"){for(var c=0,e;e=b[c];c++){if(Selector.handlers.previousElementSibling(g)==e){return[g]}}}else{b=d[f](b)}}}}for(var c=0,e;e=b[c];c++){if(e==g){return[g]}}return[]}return(g&&Element.descendantOf(g,a))?[g]:[]},className:function(b,a,c,d){if(b&&d){b=this[d](b)}return Selector.handlers.byClassName(b,a,c)},byClassName:function(c,b,f){if(!c){c=Selector.handlers.descendant([b])}var h=" "+f+" ";for(var e=0,d=[],g,a;g=c[e];e++){a=g.className;if(a.length==0){continue}if(a==f||(" "+a+" ").include(h)){d.push(g)}}return d},attrPresence:function(c,b,a,g){if(!c){c=b.getElementsByTagName("*")}if(c&&g){c=this[g](c)}var e=[];for(var d=0,f;f=c[d];d++){if(Element.hasAttribute(f,a)){e.push(f)}}return e},attr:function(a,j,h,k,c,b){if(!a){a=j.getElementsByTagName("*")}if(a&&b){a=this[b](a)}var l=Selector.operators[c],f=[];for(var e=0,d;d=a[e];e++){var g=Element.readAttribute(d,h);if(g===null){continue}if(l(g,k)){f.push(d)}}return f},pseudo:function(b,c,e,a,d){if(b&&d){b=this[d](b)}if(!b){b=a.getElementsByTagName("*")}return Selector.pseudos[c](b,e,a)}},pseudos:{"first-child":function(b,f,a){for(var d=0,c=[],e;e=b[d];d++){if(Selector.handlers.previousElementSibling(e)){continue}c.push(e)}return c},"last-child":function(b,f,a){for(var d=0,c=[],e;e=b[d];d++){if(Selector.handlers.nextElementSibling(e)){continue}c.push(e)}return c},"only-child":function(b,g,a){var e=Selector.handlers;for(var d=0,c=[],f;f=b[d];d++){if(!e.previousElementSibling(f)&&!e.nextElementSibling(f)){c.push(f)}}return c},"nth-child":function(b,c,a){return Selector.pseudos.nth(b,c,a)},"nth-last-child":function(b,c,a){return Selector.pseudos.nth(b,c,a,true)},"nth-of-type":function(b,c,a){return Selector.pseudos.nth(b,c,a,false,true)},"nth-last-of-type":function(b,c,a){return Selector.pseudos.nth(b,c,a,true,true)},"first-of-type":function(b,c,a){return Selector.pseudos.nth(b,"1",a,false,true)},"last-of-type":function(b,c,a){return Selector.pseudos.nth(b,"1",a,true,true)},"only-of-type":function(b,d,a){var c=Selector.pseudos;return c["last-of-type"](c["first-of-type"](b,d,a),d,a)},getIndices:function(d,c,e){if(d==0){return c>0?[c]:[]}return $R(1,e).inject([],function(a,b){if(0==(b-c)%d&&(b-c)/d>=0){a.push(b)}return a})},nth:function(c,s,u,r,e){if(c.length==0){return[]}if(s=="even"){s="2n+0"}if(s=="odd"){s="2n+1"}var q=Selector.handlers,p=[],d=[],g;q.mark(c);for(var o=0,f;f=c[o];o++){if(!f.parentNode._countedByPrototype){q.index(f.parentNode,r,e);d.push(f.parentNode)}}if(s.match(/^\d+$/)){s=Number(s);for(var o=0,f;f=c[o];o++){if(f.nodeIndex==s){p.push(f)}}}else{if(g=s.match(/^(-?\d*)?n(([+-])(\d+))?/)){if(g[1]=="-"){g[1]=-1}var v=g[1]?Number(g[1]):1;var t=g[2]?Number(g[2]):0;var w=Selector.pseudos.getIndices(v,t,c.length);for(var o=0,f,k=w.length;f=c[o];o++){for(var n=0;n<k;n++){if(f.nodeIndex==w[n]){p.push(f)}}}}}q.unmark(c);q.unmark(d);return p},empty:function(b,f,a){for(var d=0,c=[],e;e=b[d];d++){if(e.tagName=="!"||e.firstChild){continue}c.push(e)}return c},not:function(a,d,k){var g=Selector.handlers,l,c;var j=new Selector(d).findElements(k);g.mark(j);for(var f=0,e=[],b;b=a[f];f++){if(!b._countedByPrototype){e.push(b)}}g.unmark(j);return e},enabled:function(b,f,a){for(var d=0,c=[],e;e=b[d];d++){if(!e.disabled&&(!e.type||e.type!=="hidden")){c.push(e)}}return c},disabled:function(b,f,a){for(var d=0,c=[],e;e=b[d];d++){if(e.disabled){c.push(e)}}return c},checked:function(b,f,a){for(var d=0,c=[],e;e=b[d];d++){if(e.checked){c.push(e)}}return c}},operators:{"=":function(b,a){return b==a},"!=":function(b,a){return b!=a},"^=":function(b,a){return b==a||b&&b.startsWith(a)},"$=":function(b,a){return b==a||b&&b.endsWith(a)},"*=":function(b,a){return b==a||b&&b.include(a)},"$=":function(b,a){return b.endsWith(a)},"*=":function(b,a){return b.include(a)},"~=":function(b,a){return(" "+b+" ").include(" "+a+" ")},"|=":function(b,a){return("-"+(b||"").toUpperCase()+"-").include("-"+(a||"").toUpperCase()+"-")}},split:function(b){var a=[];b.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/,function(c){a.push(c[1].strip())});return a},matchElements:function(f,g){var e=$$(g),d=Selector.handlers;d.mark(e);for(var c=0,b=[],a;a=f[c];c++){if(a._countedByPrototype){b.push(a)}}d.unmark(e);return b},findElement:function(b,c,a){if(Object.isNumber(c)){a=c;c=false}return Selector.matchElements(b,c||"*")[a||0]},findChildElements:function(e,g){g=Selector.split(g.join(","));var d=[],f=Selector.handlers;for(var c=0,b=g.length,a;c<b;c++){a=new Selector(g[c].strip());f.concat(d,a.findElements(e))}return(b>1)?f.unique(d):d}});if(Prototype.Browser.IE){Object.extend(Selector.handlers,{concat:function(d,c){for(var e=0,f;f=c[e];e++){if(f.tagName!=="!"){d.push(f)}}return d},unmark:function(a){for(var b=0,c;c=a[b];b++){c.removeAttribute("_countedByPrototype")}return a}})}function $$(){return Selector.findChildElements(document,$A(arguments))}var Form={reset:function(a){$(a).reset();return a},serializeElements:function(g,b){if(typeof b!="object"){b={hash:!!b}}else{if(Object.isUndefined(b.hash)){b.hash=true}}var c,f,a=false,e=b.submit;var d=g.inject({},function(h,i){if(!i.disabled&&i.name){c=i.name;f=$(i).getValue();if(f!=null&&i.type!="file"&&(i.type!="submit"||(!a&&e!==false&&(!e||c==e)&&(a=true)))){if(c in h){if(!Object.isArray(h[c])){h[c]=[h[c]]}h[c].push(f)}else{h[c]=f}}}return h});return b.hash?d:Object.toQueryString(d)}};Form.Methods={serialize:function(b,a){return Form.serializeElements(Form.getElements(b),a)},getElements:function(a){return $A($(a).getElementsByTagName("*")).inject([],function(b,c){if(Form.Element.Serializers[c.tagName.toLowerCase()]){b.push(Element.extend(c))}return b})},getInputs:function(g,c,d){g=$(g);var a=g.getElementsByTagName("input");if(!c&&!d){return $A(a).map(Element.extend)}for(var e=0,h=[],f=a.length;e<f;e++){var b=a[e];if((c&&b.type!=c)||(d&&b.name!=d)){continue}h.push(Element.extend(b))}return h},disable:function(a){a=$(a);Form.getElements(a).invoke("disable");return a},enable:function(a){a=$(a);Form.getElements(a).invoke("enable");return a},findFirstElement:function(b){var c=$(b).getElements().findAll(function(d){return"hidden"!=d.type&&!d.disabled});var a=c.findAll(function(d){return d.hasAttribute("tabIndex")&&d.tabIndex>=0}).sortBy(function(d){return d.tabIndex}).first();return a?a:c.find(function(d){return["input","select","textarea"].include(d.tagName.toLowerCase())})},focusFirstElement:function(a){a=$(a);a.findFirstElement().activate();return a},request:function(b,a){b=$(b),a=Object.clone(a||{});var d=a.parameters,c=b.readAttribute("action")||"";if(c.blank()){c=window.location.href}a.parameters=b.serialize(true);if(d){if(Object.isString(d)){d=d.toQueryParams()}Object.extend(a.parameters,d)}if(b.hasAttribute("method")&&!a.method){a.method=b.method}return new Ajax.Request(c,a)}};Form.Element={focus:function(a){$(a).focus();return a},select:function(a){$(a).select();return a}};Form.Element.Methods={serialize:function(a){a=$(a);if(!a.disabled&&a.name){var b=a.getValue();if(b!=undefined){var c={};c[a.name]=b;return Object.toQueryString(c)}}return""},getValue:function(a){a=$(a);var b=a.tagName.toLowerCase();return Form.Element.Serializers[b](a)},setValue:function(a,b){a=$(a);var c=a.tagName.toLowerCase();Form.Element.Serializers[c](a,b);return a},clear:function(a){$(a).value="";return a},present:function(a){return $(a).value!=""},activate:function(a){a=$(a);try{a.focus();if(a.select&&(a.tagName.toLowerCase()!="input"||!["button","reset","submit"].include(a.type))){a.select()}}catch(b){}return a},disable:function(a){a=$(a);a.disabled=true;return a},enable:function(a){a=$(a);a.disabled=false;return a}};var Field=Form.Element;var $F=Form.Element.Methods.getValue;Form.Element.Serializers={input:function(a,b){switch(a.type.toLowerCase()){case"checkbox":case"radio":return Form.Element.Serializers.inputSelector(a,b);default:return Form.Element.Serializers.textarea(a,b)}},inputSelector:function(a,b){if(Object.isUndefined(b)){return a.checked?a.value:null}else{a.checked=!!b}},textarea:function(a,b){if(Object.isUndefined(b)){return a.value}else{a.value=b}},select:function(c,f){if(Object.isUndefined(f)){return this[c.type=="select-one"?"selectOne":"selectMany"](c)}else{var b,d,g=!Object.isArray(f);for(var a=0,e=c.length;a<e;a++){b=c.options[a];d=this.optionValue(b);if(g){if(d==f){b.selected=true;return}}else{b.selected=f.include(d)}}}},selectOne:function(b){var a=b.selectedIndex;return a>=0?this.optionValue(b.options[a]):null},selectMany:function(d){var a,e=d.length;if(!e){return null}for(var c=0,a=[];c<e;c++){var b=d.options[c];if(b.selected){a.push(this.optionValue(b))}}return a},optionValue:function(a){return Element.extend(a).hasAttribute("value")?a.value:a.text}};Abstract.TimedObserver=Class.create(PeriodicalExecuter,{initialize:function($super,a,b,c){$super(c,b);this.element=$(a);this.lastValue=this.getValue()},execute:function(){var a=this.getValue();if(Object.isString(this.lastValue)&&Object.isString(a)?this.lastValue!=a:String(this.lastValue)!=String(a)){this.callback(this.element,a);this.lastValue=a}}});Form.Element.Observer=Class.create(Abstract.TimedObserver,{getValue:function(){return Form.Element.getValue(this.element)}});Form.Observer=Class.create(Abstract.TimedObserver,{getValue:function(){return Form.serialize(this.element)}});Abstract.EventObserver=Class.create({initialize:function(a,b){this.element=$(a);this.callback=b;this.lastValue=this.getValue();if(this.element.tagName.toLowerCase()=="form"){this.registerFormCallbacks()}else{this.registerCallback(this.element)}},onElementEvent:function(){var a=this.getValue();if(this.lastValue!=a){this.callback(this.element,a);this.lastValue=a}},registerFormCallbacks:function(){Form.getElements(this.element).each(this.registerCallback,this)},registerCallback:function(a){if(a.type){switch(a.type.toLowerCase()){case"checkbox":case"radio":Event.observe(a,"click",this.onElementEvent.bind(this));break;default:Event.observe(a,"change",this.onElementEvent.bind(this));break}}}});Form.Element.EventObserver=Class.create(Abstract.EventObserver,{getValue:function(){return Form.Element.getValue(this.element)}});Form.EventObserver=Class.create(Abstract.EventObserver,{getValue:function(){return Form.serialize(this.element)}});if(!window.Event){var Event={}}Object.extend(Event,{KEY_BACKSPACE:8,KEY_TAB:9,KEY_RETURN:13,KEY_ESC:27,KEY_LEFT:37,KEY_UP:38,KEY_RIGHT:39,KEY_DOWN:40,KEY_DELETE:46,KEY_HOME:36,KEY_END:35,KEY_PAGEUP:33,KEY_PAGEDOWN:34,KEY_INSERT:45,cache:{},relatedTarget:function(b){var a;switch(b.type){case"mouseover":a=b.fromElement;break;case"mouseout":a=b.toElement;break;default:return null}return Element.extend(a)}});Event.Methods=(function(){var a;if(Prototype.Browser.IE){var b={0:1,1:4,2:2};a=function(d,c){return d.button==b[c]}}else{if(Prototype.Browser.WebKit){a=function(d,c){switch(c){case 0:return d.which==1&&!d.metaKey;case 1:return d.which==1&&d.metaKey;default:return false}}}else{a=function(d,c){return d.which?(d.which===c+1):(d.button===c)}}}return{isLeftClick:function(c){return a(c,0)},isMiddleClick:function(c){return a(c,1)},isRightClick:function(c){return a(c,2)},element:function(e){e=Event.extend(e);var d=e.target,c=e.type,f=e.currentTarget;if(f&&f.tagName){if(c==="load"||c==="error"||(c==="click"&&f.tagName.toLowerCase()==="input"&&f.type==="radio")){d=f}}if(d.nodeType==Node.TEXT_NODE){d=d.parentNode}return Element.extend(d)},findElement:function(d,f){var c=Event.element(d);if(!f){return c}var e=[c].concat(c.ancestors());return Selector.findElement(e,f,0)},pointer:function(e){var d=document.documentElement,c=document.body||{scrollLeft:0,scrollTop:0};return{x:e.pageX||(e.clientX+(d.scrollLeft||c.scrollLeft)-(d.clientLeft||0)),y:e.pageY||(e.clientY+(d.scrollTop||c.scrollTop)-(d.clientTop||0))}},pointerX:function(c){return Event.pointer(c).x},pointerY:function(c){return Event.pointer(c).y},stop:function(c){Event.extend(c);c.preventDefault();c.stopPropagation();c.stopped=true}}})();Event.extend=(function(){var a=Object.keys(Event.Methods).inject({},function(b,c){b[c]=Event.Methods[c].methodize();return b});if(Prototype.Browser.IE){Object.extend(a,{stopPropagation:function(){this.cancelBubble=true},preventDefault:function(){this.returnValue=false},inspect:function(){return"[object Event]"}});return function(b){if(!b){return false}if(b._extendedByPrototype){return b}b._extendedByPrototype=Prototype.emptyFunction;var c=Event.pointer(b);Object.extend(b,{target:b.srcElement,relatedTarget:Event.relatedTarget(b),pageX:c.x,pageY:c.y});return Object.extend(b,a)}}else{Event.prototype=Event.prototype||document.createEvent("HTMLEvents")["__proto__"];Object.extend(Event.prototype,a);return Prototype.K}})();Object.extend(Event,(function(){var b=Event.cache;function c(j){if(j._prototypeEventID){return j._prototypeEventID[0]}arguments.callee.id=arguments.callee.id||1;return j._prototypeEventID=[++arguments.callee.id]}function g(j){if(j&&j.include(":")){return"dataavailable"}return j}function a(j){return b[j]=b[j]||{}}function f(l,j){var k=a(l);return k[j]=k[j]||[]}function h(k,j,l){var o=c(k);var n=f(o,j);if(n.pluck("handler").include(l)){return false}var m=function(p){if(!Event||!Event.extend||(p.eventName&&p.eventName!=j)){return false}Event.extend(p);l.call(k,p)};m.handler=l;n.push(m);return m}function i(m,j,k){var l=f(m,j);return l.find(function(n){return n.handler==k})}function d(m,j,k){var l=a(m);if(!l[j]){return false}l[j]=l[j].without(i(m,j,k))}function e(){for(var k in b){for(var j in b[k]){b[k][j]=null}}}if(window.attachEvent){window.attachEvent("onunload",e)}if(Prototype.Browser.WebKit){window.addEventListener("unload",Prototype.emptyFunction,false)}return{observe:function(l,j,m){l=$(l);var k=g(j);var n=h(l,j,m);if(!n){return l}if(l.addEventListener){l.addEventListener(k,n,false)}else{l.attachEvent("on"+k,n)}return l},stopObserving:function(l,j,m){l=$(l);var o=c(l),k=g(j);if(!m&&j){f(o,j).each(function(p){l.stopObserving(j,p.handler)});return l}else{if(!j){Object.keys(a(o)).each(function(p){l.stopObserving(p)});return l}}var n=i(o,j,m);if(!n){return l}if(l.removeEventListener){l.removeEventListener(k,n,false)}else{l.detachEvent("on"+k,n)}d(o,j,m);return l},fire:function(l,k,j){l=$(l);if(l==document&&document.createEvent&&!l.dispatchEvent){l=document.documentElement}var m;if(document.createEvent){m=document.createEvent("HTMLEvents");m.initEvent("dataavailable",true,true)}else{m=document.createEventObject();m.eventType="ondataavailable"}m.eventName=k;m.memo=j||{};if(document.createEvent){l.dispatchEvent(m)}else{l.fireEvent(m.eventType,m)}return Event.extend(m)}}})());Object.extend(Event,Event.Methods);Element.addMethods({fire:Event.fire,observe:Event.observe,stopObserving:Event.stopObserving});Object.extend(document,{fire:Element.Methods.fire.methodize(),observe:Element.Methods.observe.methodize(),stopObserving:Element.Methods.stopObserving.methodize(),loaded:false});(function(){var b;function a(){if(document.loaded){return}if(b){window.clearInterval(b)}document.fire("dom:loaded");document.loaded=true}if(document.addEventListener){if(Prototype.Browser.WebKit){b=window.setInterval(function(){if(/loaded|complete/.test(document.readyState)){a()}},0);Event.observe(window,"load",a)}else{document.addEventListener("DOMContentLoaded",a,false)}}else{document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");$("__onDOMContentLoaded").onreadystatechange=function(){if(this.readyState=="complete"){this.onreadystatechange=null;a()}}}})();Hash.toQueryString=Object.toQueryString;var Toggle={display:Element.toggle};Element.Methods.childOf=Element.Methods.descendantOf;var Insertion={Before:function(a,b){return Element.insert(a,{before:b})},Top:function(a,b){return Element.insert(a,{top:b})},Bottom:function(a,b){return Element.insert(a,{bottom:b})},After:function(a,b){return Element.insert(a,{after:b})}};var $continue=new Error('"throw $continue" is deprecated, use "return" instead');var Position={includeScrollOffsets:false,prepare:function(){this.deltaX=window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0;this.deltaY=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0},within:function(b,a,c){if(this.includeScrollOffsets){return this.withinIncludingScrolloffsets(b,a,c)}this.xcomp=a;this.ycomp=c;this.offset=Element.cumulativeOffset(b);return(c>=this.offset[1]&&c<this.offset[1]+b.offsetHeight&&a>=this.offset[0]&&a<this.offset[0]+b.offsetWidth)},withinIncludingScrolloffsets:function(b,a,d){var c=Element.cumulativeScrollOffset(b);this.xcomp=a+c[0]-this.deltaX;this.ycomp=d+c[1]-this.deltaY;this.offset=Element.cumulativeOffset(b);return(this.ycomp>=this.offset[1]&&this.ycomp<this.offset[1]+b.offsetHeight&&this.xcomp>=this.offset[0]&&this.xcomp<this.offset[0]+b.offsetWidth)},overlap:function(b,a){if(!b){return 0}if(b=="vertical"){return((this.offset[1]+a.offsetHeight)-this.ycomp)/a.offsetHeight}if(b=="horizontal"){return((this.offset[0]+a.offsetWidth)-this.xcomp)/a.offsetWidth}},cumulativeOffset:Element.Methods.cumulativeOffset,positionedOffset:Element.Methods.positionedOffset,absolutize:function(a){Position.prepare();return Element.absolutize(a)},relativize:function(a){Position.prepare();return Element.relativize(a)},realOffset:Element.Methods.cumulativeScrollOffset,offsetParent:Element.Methods.getOffsetParent,page:Element.Methods.viewportOffset,clone:function(b,c,a){a=a||{};return Element.clonePosition(c,b,a)}};if(!document.getElementsByClassName){document.getElementsByClassName=function(b){function a(c){return c.blank()?null:"[contains(concat(' ', @class, ' '), ' "+c+" ')]"}b.getElementsByClassName=Prototype.BrowserFeatures.XPath?function(c,e){e=e.toString().strip();var d=/\s/.test(e)?$w(e).map(a).join(""):a(e);return d?document._getElementsByXPath(".//*"+d,c):[]}:function(e,f){f=f.toString().strip();var g=[],h=(/\s/.test(f)?$w(f):null);if(!h&&!f){return g}var c=$(e).getElementsByTagName("*");f=" "+f+" ";for(var d=0,k,j;k=c[d];d++){if(k.className&&(j=" "+k.className+" ")&&(j.include(f)||(h&&h.all(function(i){return!i.toString().blank()&&j.include(" "+i+" ")})))){g.push(Element.extend(k))}}return g};return function(d,c){return $(c||document.body).getElementsByClassName(d)}}(Element.Methods)}Element.ClassNames=Class.create();Element.ClassNames.prototype={initialize:function(a){this.element=$(a)},_each:function(a){this.element.className.split(/\s+/).select(function(b){return b.length>0})._each(a)},set:function(a){this.element.className=a},add:function(a){if(this.include(a)){return}this.set($A(this).concat(a).join(" "))},remove:function(a){if(!this.include(a)){return}this.set($A(this).without(a).join(" "))},toString:function(){return $A(this).join(" ")}};Object.extend(Element.ClassNames.prototype,Enumerable);Element.addMethods();
\ No newline at end of file
diff --git a/js/selectlist.js b/js/selectlist.js
new file mode 100644
index 0000000..39f29be
--- /dev/null
+++ b/js/selectlist.js
@@ -0,0 +1 @@
+function returnID(){var A=parent.opener.document[formid].selectlist_selectid,B=parent.opener.document[formid].actionID;if(parent.opener.closed||!A||!B){alert(GollemText.opener_window);window.close();return}A.value=cacheid;B.value="selectlist_process";parent.opener.document[formid].submit();window.close()};
\ No newline at end of file
diff --git a/templates/prefs/columnselect.js b/js/src/columnselect.js
similarity index 89%
rename from templates/prefs/columnselect.js
rename to js/src/columnselect.js
index 7de61b6..c110aa1 100644
--- a/templates/prefs/columnselect.js
+++ b/js/src/columnselect.js
@@ -1,7 +1,13 @@
-<script type="text/javascript">
-<!--
-var columns = new Array();
-<?php echo $js_columns; ?>
+/**
+ * Provides the javascript for the gollem column select preferences page.
+ *
+ * $Horde: gollem/js/src/columnselect.js,v 1.2.2.1 2008/10/09 20:54:41 jan Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+
+var columns = [];
 
 function selectSource()
 {
@@ -20,13 +26,13 @@ function selectSource()
     }
     var source = f.source.selectedIndex - 1;
 
-    var selected = new Array();
-    var unselected = new Array();
+    var selected = [];
+    var unselected = [];
     for (var i = 1; i < columns[source].length; i++) {
         if (columns[source][i][2]) {
-            selected[columns[source][i][3]] = new Array(columns[source][i][1], columns[source][i][0]);
+            selected[columns[source][i][3]] = [columns[source][i][1], columns[source][i][0]];
         } else {
-            unselected[unselected.length] = new Array(columns[source][i][1], columns[source][i][0]);
+            unselected[unselected.length] = [columns[source][i][1], columns[source][i][0]];
         }
     }
     for (i = 0; i < selected.length; i++) {
@@ -114,8 +120,7 @@ function moveColumnUp()
     f.selected_columns.selectedIndex = sel;
     var up = f.selected_columns[sel].value;
 
-    tmp = new Array();
-
+    tmp = [];
     for (i = 1; i < f.selected_columns.length; i++) {
         tmp[i - 1] = new Option(f.selected_columns[i].text, f.selected_columns[i].value)
     }
@@ -155,8 +160,7 @@ function moveColumnDown()
     f.selected_columns.selectedIndex = sel;
     var down = f.selected_columns[sel].value;
 
-    tmp = new Array();
-
+    tmp = [];
     for (i = 1; i < f.selected_columns.length; i++) {
         tmp[i - 1] = new Option(f.selected_columns[i].text, f.selected_columns[i].value)
     }
@@ -183,6 +187,3 @@ function moveColumnDown()
 
     resetHidden();
 }
-
-// -->
-</script>
diff --git a/js/src/login.js b/js/src/login.js
new file mode 100644
index 0000000..85ad0d2
--- /dev/null
+++ b/js/src/login.js
@@ -0,0 +1,89 @@
+/**
+ * Provides the javascript for the login.php script.
+ *
+ * $Horde: gollem/js/src/login.js,v 1.3.2.1 2008/10/09 20:54:41 jan Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+
+function setFocus()
+{
+    if (document.gollem_login.username) {
+        document.gollem_login.username.focus();
+    }
+}
+
+function gollem_reload()
+{
+    var url = reload_url + document.gollem_login.backend_key[document.gollem_login.backend_key.selectedIndex].value;
+    if (document.gollem_login.url &&
+        document.gollem_login.url.value) {
+        url += '&url=' + document.gollem_login.url.value;
+    }
+    window.location = url;
+}
+
+function submit_login()
+{
+    if (document.gollem_login.username &&
+        document.gollem_login.username.value == "") {
+        alert(GollemText.login_username);
+        document.gollem_login.username.focus();
+        return false;
+    } else if (document.gollem_login.password &&
+               document.gollem_login.password.value == "") {
+        alert(GollemText.login_password);
+        document.gollem_login.password.focus();
+        return false;
+    } else {
+        document.gollem_login.loginButton.disabled = true;
+        if (ie_clientcaps) {
+            try {
+                document.gollem_login.ie_version.value = objCCaps.getComponentVersion("{89820200-ECBD-11CF-8B85-00AA005B4383}","componentid");
+            } catch (e) { }
+        }
+        document.gollem_login.submit();
+        return true;
+    }
+}
+
+function selectLang()
+{
+    // We need to reload the login page here, but only if the user hasn't
+    // already entered a username and password.
+    var lang_page = 'login.php?new_lang=' + document.gollem_login.new_lang[document.gollem_login.new_lang.selectedIndex].value;
+    if (lang_url !== null) {
+        lang_page += '&url=' + lang_url;
+    }
+    self.location = lang_page;
+}
+
+/* Removes any leading hash that might be on a location string. */
+function removeHash(h) {
+    if (h == null || h == undefined) {
+        return null;
+    } else if (h.length && h.charAt(0) == '#') {
+        if (h.length == 1) {
+            return "";
+        } else {
+            return h.substring(1);
+        }
+    }
+    return h;
+}
+
+Event.observe(window, 'load', function() {
+    if (gollem_auth && parent.frames.horde_main) {
+        if (nomenu) {
+            parent.location = self.location;
+        } else {
+            document.gollem_login.target = '_parent';
+        }
+    }
+
+    // Need to capture hash information if it exists in URL
+    if (location.hash) {
+        $('anchor_string').value = removeHash(location.hash);
+    }
+});
diff --git a/js/src/manager.js b/js/src/manager.js
new file mode 100644
index 0000000..6bcb2f7
--- /dev/null
+++ b/js/src/manager.js
@@ -0,0 +1,390 @@
+/**
+ * Provides the javascript for the manager.php script.
+ *
+ * $Horde: gollem/js/src/manager.js,v 1.11.2.1 2008/10/09 20:54:41 jan Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+
+var Gollem = {
+    toggleRow: function()
+    {
+        $$('table.striped tr').each(function(tr) {
+            var td = tr.select('TD');
+            tr.observe('mouseover', td.invoke.bind(td, 'addClassName', 'selected'));
+            tr.observe('mouseout', td.invoke.bind(td, 'removeClassName', 'selected'));
+        });
+    },
+
+    getChecked: function()
+    {
+        return this.getElements().findAll(function(e) {
+            return e.checked;
+        });
+    },
+
+    getElements: function()
+    {
+        return $('manager').getInputs(null, 'items[]');
+    },
+
+    getSelected: function()
+    {
+        return this.getChecked().pluck('value').join("\n");
+    },
+
+    getItemsArray: function()
+    {
+        var i = 0,
+            it = $('manager').getInputs(null, 'itemTypes[]');
+
+        return this.getElements().collect(function(m) {
+            return { c: m.checked, v: m.value, t: it[i++].value };
+        });
+    },
+
+    getSelectedFoldersList: function()
+    {
+        return this.getItemsArray().collect(function(i) {
+            return (i.c && i.t == '**dir') ? i.v : null;
+        }).compact().join("\n");
+    },
+
+    chooseAction: function(i)
+    {
+        var action = $F('action' + i);
+
+        switch (action) {
+        case 'paste_items':
+            $('actionID').setValue('paste_items');
+            $('manager').submit();
+            break;
+
+        default:
+            if (!this.getChecked().size()) {
+                alert(GollemText.select_item);
+                break;
+            }
+            switch (action) {
+            case 'rename_items':
+                this.renameItems();
+                break;
+
+            case 'delete_items':
+                this.deleteItems();
+                break;
+
+            case 'chmod_modify':
+                $('attributes').show();
+                break;
+
+            case 'cut_items':
+                $('actionID').setValue('cut_items');
+                $('manager').submit();
+                break;
+
+            case 'copy_items':
+                $('actionID').setValue('copy_items');
+                $('manager').submit();
+                break;
+            }
+            break;
+        }
+    },
+
+    changeDirectory: function(e)
+    {
+        this._prepPopup('changeDirectory', e.element());
+        $('cdfrm_fname').focus();
+        e.stop();
+    },
+
+    createFolder: function(e)
+    {
+        this._prepPopup('createFolder', e.element());
+        $('createfrm_fname').focus();
+        e.stop();
+    },
+
+    _prepPopup: function(elt, elt2)
+    {
+        this.getChecked().each(function(e) {
+            e.checked = false;
+        });
+
+        $(elt).clonePosition(elt2, { setWidth: false, setHeight: false, offsetTop: elt2.getHeight() }).show();
+    },
+
+    renameItems: function()
+    {
+        var c = this.getChecked();
+        if (c.size()) {
+            c[0].checked = false;
+            $('rename').show();
+            $('renamefrm_oldname').setValue(c[0].value);
+            $('renamefrm_newname').setValue(c[0].value).focus();
+        }
+    },
+
+    deleteItems: function()
+    {
+        var cont = true, sf;
+
+        if (window.confirm(GollemText.delete_confirm_1 + '\n' + this.getSelected() + '\n' + GollemText.delete_confirm_2)) {
+            if (warn_recursive) {
+                sf = this.getSelectedFoldersList();
+                if (!sf.empty() &&
+                    !window.confirm(GollemText.delete_recurs_1 + '\n' + sf + '\n' + GollemText.delete_recurs_2)) {
+                    cont = false;
+                }
+            }
+        } else {
+            cont = false;
+        }
+
+        if (cont) {
+            $('actionID').setValue('delete_items');
+            $('manager').submit();
+        }
+    },
+
+    toggleSelection: function()
+    {
+        var e = this.getElements(),
+            checked = (this.getChecked().size() != e.length);
+        e.each(function(f) {
+            f.checked = checked;
+        });
+    },
+
+    createFolderOK: function()
+    {
+        $('createFolder').hide();
+        if ($F('createfrm_fname')) {
+            $('new_folder').setValue($F('createfrm_fname'));
+            $('actionID').setValue('create_folder');
+            $('manager').submit();
+        }
+    },
+
+    createFolderKeyCheck: function(e)
+    {
+        switch (e.keyCode) {
+        case Event.KEY_ESC:
+            this.createFolderCancel();
+            return false;
+
+        case EVENT.KEY_RETURN:
+            this.createFolderOK();
+            return false;
+        }
+        return true;
+    },
+
+    createFolderCancel: function()
+    {
+        $('createFolder').hide();
+        $('createfrm').reset();
+    },
+
+    chmodCancel: function()
+    {
+        $('attributes').hide();
+        $('chmodfrm').reset();
+    },
+
+    chmodSave: function()
+    {
+        var all = group = owner = 0;
+
+        $('chmodfrm').getElements().each(function(e) {
+            if (e.name == "owner[]" && e.checked) {
+                owner |= e.value;
+            } else if (e.name == "group[]" && e.checked) {
+                group |= e.value;
+            } else if (e.name == "all[]" && e.checked) {
+                all |= e.value;
+            }
+        });
+
+        $('attributes').hide();
+
+        $('chmod').setValue("0" + owner + "" + group + "" + all);
+        $('actionID').setValue('chmod_modify');
+        $('manager').submit();
+    },
+
+    renameOK: function()
+    {
+        var c = this.getChecked(),
+            newname = $F('renamefrm_newname'),
+            newNames = $F('new_names'),
+            oldname = $F('renamefrm_oldname'),
+            oldNames = $F('old_names');
+
+        if (newname && newname != oldname) {
+            newNames += "|" + newname;
+            oldNames += "|" + oldname;
+        }
+
+        if (newNames.startsWith("|")) {
+            newNames = newNames.substring(1);
+        }
+        if (oldNames.startsWith("|")) {
+            oldNames = oldNames.substring(1);
+        }
+
+        $('new_names').setValue(newNames);
+        $('old_names').setValue(oldNames);
+
+        if (c.size()) {
+            c[0].checked = false;
+            found = true;
+            $('rename').show();
+            $F(c[0]).focus();
+        } else {
+            $('actionID').setValue('rename_items');
+            $('manager').submit();
+        }
+
+        return false;
+    },
+
+    renameCancel: function()
+    {
+        $('new_names', 'old_names').invoke('setValue', '');
+        $('rename').hide();
+    },
+
+    renameKeyCheck: function(e)
+    {
+        switch (e.keyCode) {
+        case Event.KEY_ESC:
+            this.renameCancel();
+            return false;
+
+        case EVENT.KEY_RETURN:
+            this.renameOK();
+            return false;
+        }
+        return true;
+    },
+
+    changeDirectoryOK: function()
+    {
+        $('changeDirectory').hide();
+        if ($F('cdfrm_fname')) {
+            $('dir').setValue($F('cdfrm_fname'));
+            $('manager').submit();
+        }
+    },
+
+    changeDirectoryKeyCheck: function(e)
+    {
+        switch (e.keyCode) {
+        case Event.KEY_ESC:
+            this.changeDirectoryCancel();
+            return false;
+
+        case EVENT.KEY_RETURN:
+            this.changeDirectoryOK();
+            return false;
+        }
+        return true;
+    },
+
+    changeDirectoryCancel: function()
+    {
+        $('changeDirectory').hide();
+        $('cdfrm').reset();
+    },
+
+    uploadFields: function()
+    {
+        return $('manager').getInputs('file').collect(function(m) {
+            return (m.name.substr(0, 12) == 'file_upload_') ? m : null;
+        }).compact();
+    },
+
+    uploadFile: function()
+    {
+        if (this.uploadsExist()) {
+            $('actionID').setValue('upload_file');
+            $('manager').submit();
+        }
+    },
+
+    applyFilter: function()
+    {
+        $('manager').submit();
+    },
+
+    clearFilter: function()
+    {
+        $('filter').setValue('');
+        this.applyFilter();
+    },
+
+    uploadsExist: function()
+    {
+        if (GollemVar.empty_input ||
+            this.uploadFields().find(function(f) { return $F(f); })) {
+            return true;
+        }
+        alert(GollemText.specify_upload);
+        $('file_upload_1').focus();
+        return false;
+    },
+
+    uploadChanged: function()
+    {
+        if (GollemVar.empty_input) {
+            return;
+        }
+
+        var file, lastRow,
+            fields = this.uploadFields(),
+            usedFields = fields.findAll(function(f) { return $F(f).length; }).length;
+
+        if (usedFields == fields.length) {
+            lastRow = $('upload_row_' + usedFields);
+            if (lastRow) {
+                file = new Element('INPUT', { type: 'file', name: 'file_upload_' + (usedFields + 1), size: 25 });
+                lastRow.insert({ after:
+                    new Element('DIV', { id: 'upload_row_' + (usedFields + 1) }).insert(
+                        new Element('STRONG').insert(GollemText.file + ' ' + (usedFields + 1) + ':')
+                    ).insert(' ').insert(file)
+                });
+                file.observe('change', this.uploadChanged.bind(this));
+            }
+        }
+    },
+
+    doPrefsUpdate: function(column, sortDown)
+    {
+        try {
+            new Ajax.Request(GollemVar.prefs_api, { parameters: { app: 'gollem', pref: 'sortby', value: column.substring(1) } });
+            new Ajax.Request(GollemVar.prefs_api, { parameters: { app: 'gollem', pref: 'sortdir', value: sortDown } });
+        } catch (e) {}
+    }
+};
+
+function table_sortCallback(tableId, column, sortDown)
+{
+    if (Gollem.prefs_update_timeout) {
+        window.clearTimeout(Gollem.prefs_update_timeout);
+    }
+    Gollem.prefs_update_timeout = Gollem.doPrefsUpdate.bind(this, column, sortDown).delay(0.3);
+}
+
+document.observe('dom:loaded', function() {
+    var tmp;
+    Gollem.toggleRow()
+    if (tmp = $('createfolder')) {
+        tmp.observe('click', Gollem.createFolder.bindAsEventListener(Gollem));
+    }
+    if (tmp = $('changefolder')) {
+        tmp.observe('click', Gollem.changeDirectory.bindAsEventListener(Gollem));
+    }
+});
diff --git a/js/src/popup.js b/js/src/popup.js
new file mode 100644
index 0000000..3877c6f
--- /dev/null
+++ b/js/src/popup.js
@@ -0,0 +1,68 @@
+/**
+ * Gollem Popup JavaScript.
+ *
+ * Provides the javascript to open popup windows.
+ * This file should be included via Horde::addScriptFile().
+ *
+ * $Horde: gollem/js/src/popup.js,v 1.1.2.1 2008/10/09 20:54:41 jan Exp $
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+/**
+ * Open a popup window.
+ *
+ * @param string $url      The URL to open in the popup window.
+ * @param integer $width   The width of the popup window. (Default: 600 px)
+ * @param integer $height  The height of the popup window. (Default: 500 px)
+ * @param string $args     Any additional args to pass to the script.
+ *                         (Default: no args)
+ */
+function popup_gollem(url, width, height, args)
+{
+    if (!width) {
+        width = 600;
+    }
+    var screen_width = screen.width;
+    if (width > (screen_width - 75)) {
+        width = screen_width - 75;
+    }
+
+    if (!height) {
+        height = 500;
+    }
+    var screen_width = screen.width;
+    if (width > (screen_width - 75)) {
+        width = screen_width - 75;
+    }
+
+    var now = new Date();
+    var name = now.getTime();
+
+    if (url.indexOf('?') == -1) {
+        var glue = '?';
+    } else {
+        var glue = '&';
+    }
+
+    if (args != '') {
+        url = url + glue + unescape(args) + '&uniq=' + name;
+    } else {
+        url = url + glue + 'uniq=' + name;
+    }
+
+    param = 'toolbar=no,location=no,status=yes,scrollbars=yes,resizable=yes,width=' + width + ',height=' + height + ',left=0,top=0';
+    win = window.open(url, name, param);
+    if (!win) {
+        alert(GollemText.popup_block);
+    } else {
+        if (typeof win.name == 'undefined') {
+            win.name = name;
+        }
+        if (typeof win.opener == 'undefined') {
+            win.opener = self;
+        }
+        win.focus();
+    }
+}
diff --git a/js/src/prototype.js b/js/src/prototype.js
new file mode 100644
index 0000000..a7fd383
--- /dev/null
+++ b/js/src/prototype.js
@@ -0,0 +1,4320 @@
+/*  Prototype JavaScript framework, version 1.6.0.3
+ *  (c) 2005-2008 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.6.0.3',
+
+  Browser: {
+    IE:     !!(window.attachEvent &&
+      navigator.userAgent.indexOf('Opera') === -1),
+    Opera:  navigator.userAgent.indexOf('Opera') > -1,
+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 &&
+      navigator.userAgent.indexOf('KHTML') === -1,
+    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+  },
+
+  BrowserFeatures: {
+    XPath: !!document.evaluate,
+    SelectorsAPI: !!document.querySelector,
+    ElementExtensions: !!window.HTMLElement,
+    SpecificElementExtensions:
+      document.createElement('div')['__proto__'] &&
+      document.createElement('div')['__proto__'] !==
+        document.createElement('form')['__proto__']
+  },
+
+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  emptyFunction: function() { },
+  K: function(x) { return x }
+};
+
+if (Prototype.Browser.MobileSafari)
+  Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+
+/* Based on Alex Arnell's inheritance implementation. */
+var Class = {
+  create: function() {
+    var parent = null, properties = $A(arguments);
+    if (Object.isFunction(properties[0]))
+      parent = properties.shift();
+
+    function klass() {
+      this.initialize.apply(this, arguments);
+    }
+
+    Object.extend(klass, Class.Methods);
+    klass.superclass = parent;
+    klass.subclasses = [];
+
+    if (parent) {
+      var subclass = function() { };
+      subclass.prototype = parent.prototype;
+      klass.prototype = new subclass;
+      parent.subclasses.push(klass);
+    }
+
+    for (var i = 0; i < properties.length; i++)
+      klass.addMethods(properties[i]);
+
+    if (!klass.prototype.initialize)
+      klass.prototype.initialize = Prototype.emptyFunction;
+
+    klass.prototype.constructor = klass;
+
+    return klass;
+  }
+};
+
+Class.Methods = {
+  addMethods: function(source) {
+    var ancestor   = this.superclass && this.superclass.prototype;
+    var properties = Object.keys(source);
+
+    if (!Object.keys({ toString: true }).length)
+      properties.push("toString", "valueOf");
+
+    for (var i = 0, length = properties.length; i < length; i++) {
+      var property = properties[i], value = source[property];
+      if (ancestor && Object.isFunction(value) &&
+          value.argumentNames().first() == "$super") {
+        var method = value;
+        value = (function(m) {
+          return function() { return ancestor[m].apply(this, arguments) };
+        })(property).wrap(method);
+
+        value.valueOf = method.valueOf.bind(method);
+        value.toString = method.toString.bind(method);
+      }
+      this.prototype[property] = value;
+    }
+
+    return this;
+  }
+};
+
+var Abstract = { };
+
+Object.extend = function(destination, source) {
+  for (var property in source)
+    destination[property] = source[property];
+  return destination;
+};
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (Object.isUndefined(object)) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : String(object);
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  toJSON: function(object) {
+    var type = typeof object;
+    switch (type) {
+      case 'undefined':
+      case 'function':
+      case 'unknown': return;
+      case 'boolean': return object.toString();
+    }
+
+    if (object === null) return 'null';
+    if (object.toJSON) return object.toJSON();
+    if (Object.isElement(object)) return;
+
+    var results = [];
+    for (var property in object) {
+      var value = Object.toJSON(object[property]);
+      if (!Object.isUndefined(value))
+        results.push(property.toJSON() + ': ' + value);
+    }
+
+    return '{' + results.join(', ') + '}';
+  },
+
+  toQueryString: function(object) {
+    return $H(object).toQueryString();
+  },
+
+  toHTML: function(object) {
+    return object && object.toHTML ? object.toHTML() : String.interpret(object);
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({ }, object);
+  },
+
+  isElement: function(object) {
+    return !!(object && object.nodeType == 1);
+  },
+
+  isArray: function(object) {
+    return object != null && typeof object == "object" &&
+      'splice' in object && 'join' in object;
+  },
+
+  isHash: function(object) {
+    return object instanceof Hash;
+  },
+
+  isFunction: function(object) {
+    return typeof object == "function";
+  },
+
+  isString: function(object) {
+    return typeof object == "string";
+  },
+
+  isNumber: function(object) {
+    return typeof object == "number";
+  },
+
+  isUndefined: function(object) {
+    return typeof object == "undefined";
+  }
+});
+
+Object.extend(Function.prototype, {
+  argumentNames: function() {
+    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
+      .replace(/\s+/g, '').split(',');
+    return names.length == 1 && !names[0] ? [] : names;
+  },
+
+  bind: function() {
+    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function() {
+      return __method.apply(object, args.concat($A(arguments)));
+    }
+  },
+
+  bindAsEventListener: function() {
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function(event) {
+      return __method.apply(object, [event || window.event].concat(args));
+    }
+  },
+
+  curry: function() {
+    if (!arguments.length) return this;
+    var __method = this, args = $A(arguments);
+    return function() {
+      return __method.apply(this, args.concat($A(arguments)));
+    }
+  },
+
+  delay: function() {
+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+    return window.setTimeout(function() {
+      return __method.apply(__method, args);
+    }, timeout);
+  },
+
+  defer: function() {
+    var args = [0.01].concat($A(arguments));
+    return this.delay.apply(this, args);
+  },
+
+  wrap: function(wrapper) {
+    var __method = this;
+    return function() {
+      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+    }
+  },
+
+  methodize: function() {
+    if (this._methodized) return this._methodized;
+    var __method = this;
+    return this._methodized = function() {
+      return __method.apply(null, [this].concat($A(arguments)));
+    };
+  }
+});
+
+Date.prototype.toJSON = function() {
+  return '"' + this.getUTCFullYear() + '-' +
+    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+    this.getUTCDate().toPaddedString(2) + 'T' +
+    this.getUTCHours().toPaddedString(2) + ':' +
+    this.getUTCMinutes().toPaddedString(2) + ':' +
+    this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) { }
+    }
+
+    return returnValue;
+  }
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create({
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  execute: function() {
+    this.callback(this);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.execute();
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+});
+Object.extend(String, {
+  interpret: function(value) {
+    return value == null ? '' : String(value);
+  },
+  specialChar: {
+    '\b': '\\b',
+    '\t': '\\t',
+    '\n': '\\n',
+    '\f': '\\f',
+    '\r': '\\r',
+    '\\': '\\\\'
+  }
+});
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = Object.isUndefined(count) ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return String(this);
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = Object.isUndefined(truncation) ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : String(this);
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var self = arguments.callee;
+    self.text.data = this;
+    return self.div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = new Element('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return { };
+
+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var key = decodeURIComponent(pair.shift());
+        var value = pair.length > 1 ? pair.join('=') : pair[0];
+        if (value != undefined) value = decodeURIComponent(value);
+
+        if (key in hash) {
+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+          hash[key].push(value);
+        }
+        else hash[key] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  times: function(count) {
+    return count < 1 ? '' : new Array(count + 1).join(this);
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function() {
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+      var character = String.specialChar[match[0]];
+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+    });
+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  },
+
+  toJSON: function() {
+    return this.inspect(true);
+  },
+
+  unfilterJSON: function(filter) {
+    return this.sub(filter || Prototype.JSONFilter, '#{1}');
+  },
+
+  isJSON: function() {
+    var str = this;
+    if (str.blank()) return false;
+    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+  },
+
+  evalJSON: function(sanitize) {
+    var json = this.unfilterJSON();
+    try {
+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+    } catch (e) { }
+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+  },
+
+  include: function(pattern) {
+    return this.indexOf(pattern) > -1;
+  },
+
+  startsWith: function(pattern) {
+    return this.indexOf(pattern) === 0;
+  },
+
+  endsWith: function(pattern) {
+    var d = this.length - pattern.length;
+    return d >= 0 && this.lastIndexOf(pattern) === d;
+  },
+
+  empty: function() {
+    return this == '';
+  },
+
+  blank: function() {
+    return /^\s*$/.test(this);
+  },
+
+  interpolate: function(object, pattern) {
+    return new Template(this, pattern).evaluate(object);
+  }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+  escapeHTML: function() {
+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+  },
+  unescapeHTML: function() {
+    return this.stripTags().replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (Object.isFunction(replacement)) return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+};
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+  div:  document.createElement('div'),
+  text: document.createTextNode('')
+});
+
+String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);
+
+var Template = Class.create({
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    if (Object.isFunction(object.toTemplateReplacements))
+      object = object.toTemplateReplacements();
+
+    return this.template.gsub(this.pattern, function(match) {
+      if (object == null) return '';
+
+      var before = match[1] || '';
+      if (before == '\\') return match[2];
+
+      var ctx = object, expr = match[3];
+      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+      match = pattern.exec(expr);
+      if (match == null) return before;
+
+      while (match != null) {
+        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+        ctx = ctx[comp];
+        if (null == ctx || '' == match[3]) break;
+        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+        match = pattern.exec(expr);
+      }
+
+      return before + String.interpret(ctx);
+    });
+  }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
+
+var Enumerable = {
+  each: function(iterator, context) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        iterator.call(context, value, index++);
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator, context) {
+    var index = -number, slices = [], array = this.toArray();
+    if (number < 1) return array;
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.collect(iterator, context);
+  },
+
+  all: function(iterator, context) {
+    iterator = iterator || Prototype.K;
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!iterator.call(context, value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator, context) {
+    iterator = iterator || Prototype.K;
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!iterator.call(context, value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator, context) {
+    iterator = iterator || Prototype.K;
+    var results = [];
+    this.each(function(value, index) {
+      results.push(iterator.call(context, value, index));
+    });
+    return results;
+  },
+
+  detect: function(iterator, context) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator.call(context, value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator, context) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator.call(context, value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(filter, iterator, context) {
+    iterator = iterator || Prototype.K;
+    var results = [];
+
+    if (Object.isString(filter))
+      filter = new RegExp(filter);
+
+    this.each(function(value, index) {
+      if (filter.match(value))
+        results.push(iterator.call(context, value, index));
+    });
+    return results;
+  },
+
+  include: function(object) {
+    if (Object.isFunction(this.indexOf))
+      if (this.indexOf(object) != -1) return true;
+
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator, context) {
+    this.each(function(value, index) {
+      memo = iterator.call(context, memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.map(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator, context) {
+    iterator = iterator || Prototype.K;
+    var result;
+    this.each(function(value, index) {
+      value = iterator.call(context, value, index);
+      if (result == null || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator, context) {
+    iterator = iterator || Prototype.K;
+    var result;
+    this.each(function(value, index) {
+      value = iterator.call(context, value, index);
+      if (result == null || value < result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator, context) {
+    iterator = iterator || Prototype.K;
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      (iterator.call(context, value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator, context) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator.call(context, value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator, context) {
+    return this.map(function(value, index) {
+      return {
+        value: value,
+        criteria: iterator.call(context, value, index)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.map();
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (Object.isFunction(args.last()))
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+};
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  filter:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray,
+  every:   Enumerable.all,
+  some:    Enumerable.any
+});
+function $A(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) return iterable.toArray();
+  var length = iterable.length || 0, results = new Array(length);
+  while (length--) results[length] = iterable[length];
+  return results;
+}
+
+if (Prototype.Browser.WebKit) {
+  $A = function(iterable) {
+    if (!iterable) return [];
+    // In Safari, only use the `toArray` method if it's not a NodeList.
+    // A NodeList is a function, has an function `item` property, and a numeric
+    // `length` property. Adapted from Google Doctype.
+    if (!(typeof iterable === 'function' && typeof iterable.length ===
+        'number' && typeof iterable.item === 'function') && iterable.toArray)
+      return iterable.toArray();
+    var length = iterable.length || 0, results = new Array(length);
+    while (length--) results[length] = iterable[length];
+    return results;
+  };
+}
+
+Array.from = $A;
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i < length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(Object.isArray(value) ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function(sorted) {
+    return this.inject([], function(array, value, index) {
+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+        array.push(value);
+      return array;
+    });
+  },
+
+  intersect: function(array) {
+    return this.uniq().findAll(function(item) {
+      return array.detect(function(value) { return item === value });
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  },
+
+  toJSON: function() {
+    var results = [];
+    this.each(function(object) {
+      var value = Object.toJSON(object);
+      if (!Object.isUndefined(value)) results.push(value);
+    });
+    return '[' + results.join(', ') + ']';
+  }
+});
+
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+  Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+  i || (i = 0);
+  var length = this.length;
+  if (i < 0) i = length + i;
+  for (; i < length; i++)
+    if (this[i] === item) return i;
+  return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+  var n = this.slice(0, i).reverse().indexOf(item);
+  return (n < 0) ? n : i - n - 1;
+};
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+  if (!Object.isString(string)) return [];
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+  Array.prototype.concat = function() {
+    var array = [];
+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      if (Object.isArray(arguments[i])) {
+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  };
+}
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator, context) {
+    $R(0, this, true).each(iterator, context);
+    return this;
+  },
+
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
+  },
+
+  toJSON: function() {
+    return isFinite(this) ? this.toString() : 'null';
+  }
+});
+
+$w('abs round ceil floor').each(function(method){
+  Number.prototype[method] = Math[method].methodize();
+});
+function $H(object) {
+  return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+
+  function toQueryPair(key, value) {
+    if (Object.isUndefined(value)) return key;
+    return key + '=' + encodeURIComponent(String.interpret(value));
+  }
+
+  return {
+    initialize: function(object) {
+      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+    },
+
+    _each: function(iterator) {
+      for (var key in this._object) {
+        var value = this._object[key], pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    },
+
+    set: function(key, value) {
+      return this._object[key] = value;
+    },
+
+    get: function(key) {
+      // simulating poorly supported hasOwnProperty
+      if (this._object[key] !== Object.prototype[key])
+        return this._object[key];
+    },
+
+    unset: function(key) {
+      var value = this._object[key];
+      delete this._object[key];
+      return value;
+    },
+
+    toObject: function() {
+      return Object.clone(this._object);
+    },
+
+    keys: function() {
+      return this.pluck('key');
+    },
+
+    values: function() {
+      return this.pluck('value');
+    },
+
+    index: function(value) {
+      var match = this.detect(function(pair) {
+        return pair.value === value;
+      });
+      return match && match.key;
+    },
+
+    merge: function(object) {
+      return this.clone().update(object);
+    },
+
+    update: function(object) {
+      return new Hash(object).inject(this, function(result, pair) {
+        result.set(pair.key, pair.value);
+        return result;
+      });
+    },
+
+    toQueryString: function() {
+      return this.inject([], function(results, pair) {
+        var key = encodeURIComponent(pair.key), values = pair.value;
+
+        if (values && typeof values == 'object') {
+          if (Object.isArray(values))
+            return results.concat(values.map(toQueryPair.curry(key)));
+        } else results.push(toQueryPair(key, values));
+        return results;
+      }).join('&');
+    },
+
+    inspect: function() {
+      return '#<Hash:{' + this.map(function(pair) {
+        return pair.map(Object.inspect).join(': ');
+      }).join(', ') + '}>';
+    },
+
+    toJSON: function() {
+      return Object.toJSON(this.toObject());
+    },
+
+    clone: function() {
+      return new Hash(this);
+    }
+  }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+};
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+};
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (Object.isFunction(responder[callback])) {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) { }
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate:   function() { Ajax.activeRequestCount++ },
+  onComplete: function() { Ajax.activeRequestCount-- }
+});
+
+Ajax.Base = Class.create({
+  initialize: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   '',
+      evalJSON:     true,
+      evalJS:       true
+    };
+    Object.extend(this.options, options || { });
+
+    this.options.method = this.options.method.toLowerCase();
+
+    if (Object.isString(this.options.parameters))
+      this.options.parameters = this.options.parameters.toQueryParams();
+    else if (Object.isHash(this.options.parameters))
+      this.options.parameters = this.options.parameters.toObject();
+  }
+});
+
+Ajax.Request = Class.create(Ajax.Base, {
+  _complete: false,
+
+  initialize: function($super, url, options) {
+    $super(options);
+    this.transport = Ajax.getTransport();
+    this.request(url);
+  },
+
+  request: function(url) {
+    this.url = url;
+    this.method = this.options.method;
+    var params = Object.clone(this.options.parameters);
+
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    this.parameters = params;
+
+    if (params = Object.toQueryString(params)) {
+      // when GET, append parameters to URL
+      if (this.method == 'get')
+        this.url += (this.url.include('?') ? '&' : '?') + params;
+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+        params += '&_=';
+    }
+
+    try {
+      var response = new Ajax.Response(this);
+      if (this.options.onCreate) this.options.onCreate(response);
+      Ajax.Responders.dispatch('onCreate', this, response);
+
+      this.transport.open(this.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+      this.transport.send(this.body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+      headers['Content-type'] = this.options.contentType +
+        (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+            headers['Connection'] = 'close';
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (Object.isFunction(extras.push))
+        for (var i = 0, length = extras.length; i < length; i += 2)
+          headers[extras[i]] = extras[i+1];
+      else
+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+    }
+
+    for (var name in headers)
+      this.transport.setRequestHeader(name, headers[name]);
+  },
+
+  success: function() {
+    var status = this.getStatus();
+    return !status || (status >= 200 && status < 300);
+  },
+
+  getStatus: function() {
+    try {
+      return this.transport.status || 0;
+    } catch (e) { return 0 }
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + response.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(response, response.headerJSON);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      var contentType = response.getHeader('Content-type');
+      if (this.options.evalJS == 'force'
+          || (this.options.evalJS && this.isSameOrigin() && contentType
+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+        this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  isSameOrigin: function() {
+    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+      protocol: location.protocol,
+      domain: document.domain,
+      port: location.port ? ':' + location.port : ''
+    }));
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name) || null;
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval((this.transport.responseText || '').unfilterJSON());
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+  initialize: function(request){
+    this.request = request;
+    var transport  = this.transport  = request.transport,
+        readyState = this.readyState = transport.readyState;
+
+    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+      this.status       = this.getStatus();
+      this.statusText   = this.getStatusText();
+      this.responseText = String.interpret(transport.responseText);
+      this.headerJSON   = this._getHeaderJSON();
+    }
+
+    if(readyState == 4) {
+      var xml = transport.responseXML;
+      this.responseXML  = Object.isUndefined(xml) ? null : xml;
+      this.responseJSON = this._getResponseJSON();
+    }
+  },
+
+  status:      0,
+  statusText: '',
+
+  getStatus: Ajax.Request.prototype.getStatus,
+
+  getStatusText: function() {
+    try {
+      return this.transport.statusText || '';
+    } catch (e) { return '' }
+  },
+
+  getHeader: Ajax.Request.prototype.getHeader,
+
+  getAllHeaders: function() {
+    try {
+      return this.getAllResponseHeaders();
+    } catch (e) { return null }
+  },
+
+  getResponseHeader: function(name) {
+    return this.transport.getResponseHeader(name);
+  },
+
+  getAllResponseHeaders: function() {
+    return this.transport.getAllResponseHeaders();
+  },
+
+  _getHeaderJSON: function() {
+    var json = this.getHeader('X-JSON');
+    if (!json) return null;
+    json = decodeURIComponent(escape(json));
+    try {
+      return json.evalJSON(this.request.options.sanitizeJSON ||
+        !this.request.isSameOrigin());
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  },
+
+  _getResponseJSON: function() {
+    var options = this.request.options;
+    if (!options.evalJSON || (options.evalJSON != 'force' &&
+      !(this.getHeader('Content-type') || '').include('application/json')) ||
+        this.responseText.blank())
+          return null;
+    try {
+      return this.responseText.evalJSON(options.sanitizeJSON ||
+        !this.request.isSameOrigin());
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+  initialize: function($super, container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    };
+
+    options = Object.clone(options);
+    var onComplete = options.onComplete;
+    options.onComplete = (function(response, json) {
+      this.updateContent(response.responseText);
+      if (Object.isFunction(onComplete)) onComplete(response, json);
+    }).bind(this);
+
+    $super(url, options);
+  },
+
+  updateContent: function(responseText) {
+    var receiver = this.container[this.success() ? 'success' : 'failure'],
+        options = this.options;
+
+    if (!options.evalScripts) responseText = responseText.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (options.insertion) {
+        if (Object.isString(options.insertion)) {
+          var insertion = { }; insertion[options.insertion] = responseText;
+          receiver.insert(insertion);
+        }
+        else options.insertion(receiver, responseText);
+      }
+      else receiver.update(responseText);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+  initialize: function($super, container, url, options) {
+    $super(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = { };
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(response) {
+    if (this.options.decay) {
+      this.decay = (response.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = response.responseText;
+    }
+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (Object.isString(element))
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(Element.extend(query.snapshotItem(i)));
+    return results;
+  };
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+  // DOM level 2 ECMAScript Language Binding
+  Object.extend(Node, {
+    ELEMENT_NODE: 1,
+    ATTRIBUTE_NODE: 2,
+    TEXT_NODE: 3,
+    CDATA_SECTION_NODE: 4,
+    ENTITY_REFERENCE_NODE: 5,
+    ENTITY_NODE: 6,
+    PROCESSING_INSTRUCTION_NODE: 7,
+    COMMENT_NODE: 8,
+    DOCUMENT_NODE: 9,
+    DOCUMENT_TYPE_NODE: 10,
+    DOCUMENT_FRAGMENT_NODE: 11,
+    NOTATION_NODE: 12
+  });
+}
+
+(function() {
+  var element = this.Element;
+  this.Element = function(tagName, attributes) {
+    attributes = attributes || { };
+    tagName = tagName.toLowerCase();
+    var cache = Element.cache;
+    if (Prototype.Browser.IE && attributes.name) {
+      tagName = '<' + tagName + ' name="' + attributes.name + '">';
+      delete attributes.name;
+      return Element.writeAttribute(document.createElement(tagName), attributes);
+    }
+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+  };
+  Object.extend(this.Element, element || { });
+  if (element) this.Element.prototype = element.prototype;
+}).call(window);
+
+Element.cache = { };
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    element = $(element);
+    element.style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    element = $(element);
+    element.style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+    content = Object.toHTML(content);
+    element.innerHTML = content.stripScripts();
+    content.evalScripts.bind(content).defer();
+    return element;
+  },
+
+  replace: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    else if (!Object.isElement(content)) {
+      content = Object.toHTML(content);
+      var range = element.ownerDocument.createRange();
+      range.selectNode(element);
+      content.evalScripts.bind(content).defer();
+      content = range.createContextualFragment(content.stripScripts());
+    }
+    element.parentNode.replaceChild(content, element);
+    return element;
+  },
+
+  insert: function(element, insertions) {
+    element = $(element);
+
+    if (Object.isString(insertions) || Object.isNumber(insertions) ||
+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+          insertions = {bottom:insertions};
+
+    var content, insert, tagName, childNodes;
+
+    for (var position in insertions) {
+      content  = insertions[position];
+      position = position.toLowerCase();
+      insert = Element._insertionTranslations[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+
+      tagName = ((position == 'before' || position == 'after')
+        ? element.parentNode : element).tagName.toUpperCase();
+
+      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+      if (position == 'top' || position == 'after') childNodes.reverse();
+      childNodes.each(insert.curry(element));
+
+      content.evalScripts.bind(content).defer();
+    }
+
+    return element;
+  },
+
+  wrap: function(element, wrapper, attributes) {
+    element = $(element);
+    if (Object.isElement(wrapper))
+      $(wrapper).writeAttribute(attributes || { });
+    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+    else wrapper = new Element('div', wrapper);
+    if (element.parentNode)
+      element.parentNode.replaceChild(wrapper, element);
+    wrapper.appendChild(element);
+    return wrapper;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $(element).select("*");
+  },
+
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (Object.isString(selector))
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return Object.isNumber(expression) ? ancestors[expression] :
+      Selector.findElement(ancestors, expression, index);
+  },
+
+  down: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return element.firstDescendant();
+    return Object.isNumber(expression) ? element.descendants()[expression] :
+      Element.select(element, expression)[index || 0];
+  },
+
+  previous: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return Object.isNumber(expression) ? previousSiblings[expression] :
+      Selector.findElement(previousSiblings, expression, index);
+  },
+
+  next: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return Object.isNumber(expression) ? nextSiblings[expression] :
+      Selector.findElement(nextSiblings, expression, index);
+  },
+
+  select: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  adjacent: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element.parentNode, args).without(element);
+  },
+
+  identify: function(element) {
+    element = $(element);
+    var id = element.readAttribute('id'), self = arguments.callee;
+    if (id) return id;
+    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+    element.writeAttribute('id', id);
+    return id;
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (Prototype.Browser.IE) {
+      var t = Element._attributeTranslations.read;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name]) name = t.names[name];
+      if (name.include(':')) {
+        return (!element.attributes || !element.attributes[name]) ? null :
+         element.attributes[name].value;
+      }
+    }
+    return element.getAttribute(name);
+  },
+
+  writeAttribute: function(element, name, value) {
+    element = $(element);
+    var attributes = { }, t = Element._attributeTranslations.write;
+
+    if (typeof name == 'object') attributes = name;
+    else attributes[name] = Object.isUndefined(value) ? true : value;
+
+    for (var attr in attributes) {
+      name = t.names[attr] || attr;
+      value = attributes[attr];
+      if (t.values[attr]) name = t.values[attr](element, value);
+      if (value === false || value === null)
+        element.removeAttribute(name);
+      else if (value === true)
+        element.setAttribute(name, name);
+      else element.setAttribute(name, value);
+    }
+    return element;
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    return (elementClassName.length > 0 && (elementClassName == className ||
+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    if (!element.hasClassName(className))
+      element.className += (element.className ? ' ' : '') + className;
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    element.className = element.className.replace(
+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return element[element.hasClassName(className) ?
+      'removeClassName' : 'addClassName'](className);
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.blank();
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+
+    if (element.compareDocumentPosition)
+      return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+    if (ancestor.contains)
+      return ancestor.contains(element) && ancestor !== element;
+
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = element.cumulativeOffset();
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    style = style == 'float' ? 'cssFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value || value == 'auto') {
+      var css = document.defaultView.getComputedStyle(element, null);
+      value = css ? css[style] : null;
+    }
+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+    return value == 'auto' ? null : value;
+  },
+
+  getOpacity: function(element) {
+    return $(element).getStyle('opacity');
+  },
+
+  setStyle: function(element, styles) {
+    element = $(element);
+    var elementStyle = element.style, match;
+    if (Object.isString(styles)) {
+      element.style.cssText += ';' + styles;
+      return styles.include('opacity') ?
+        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+    }
+    for (var property in styles)
+      if (property == 'opacity') element.setOpacity(styles[property]);
+      else
+        elementStyle[(property == 'float' || property == 'cssFloat') ?
+          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+            property] = styles[property];
+
+    return element;
+  },
+
+  setOpacity: function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = element.getStyle('display');
+    if (display != 'none' && display != null) // Safari bug
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (Prototype.Browser.Opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+    if (element._overflow !== 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if (element.tagName.toUpperCase() == 'BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p !== 'static') break;
+      }
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'absolute') return element;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    var offsets = element.positionedOffset();
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.width  = width + 'px';
+    element.style.height = height + 'px';
+    return element;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'relative') return element;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+    return element;
+  },
+
+  cumulativeScrollOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  getOffsetParent: function(element) {
+    element = $(element);
+    var op = element.offsetParent, body = document.body, docEl = document.documentElement;
+
+    /* IE with strict doctype may try to return documentElement as offsetParent
+       on relatively positioned elements, we will return body instead */
+    if (op && op !== docEl) return $(op);
+    if (op === docEl || element === docEl || element === body) return $(body);
+
+    while ((element = element.parentNode) && element !== body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return $(element);
+
+    return $(body);
+  },
+
+  viewportOffset: function(forElement) {
+    forElement = $(forElement);
+
+    var element = forElement, valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+    } while ((element = element.getOffsetParent()) != document.body);
+
+    element = forElement;
+    do {
+      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  clonePosition: function(element, source) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || { });
+
+    // find page position of source
+    source = $(source);
+    var p = source.viewportOffset();
+
+    // find coordinate system to use
+    element = $(element);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(element, 'position') == 'absolute') {
+      parent = element.getOffsetParent();
+      delta = parent.viewportOffset();
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
+    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+    return element;
+  }
+};
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+  getElementsBySelector: Element.Methods.select,
+  childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+  write: {
+    names: {
+      className: 'class',
+      htmlFor:   'for'
+    },
+    values: { }
+  }
+};
+
+if (Prototype.Browser.Opera) {
+  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+    function(proceed, element, style) {
+      switch (style) {
+        case 'left': case 'top': case 'right': case 'bottom':
+          if (proceed(element, 'position') === 'static') return null;
+        case 'height': case 'width':
+          // returns '0px' for hidden elements; we want it to return null
+          if (!Element.visible(element)) return null;
+
+          // returns the border-box dimensions rather than the content-box
+          // dimensions, so we subtract padding and borders from the value
+          var dim = parseInt(proceed(element, style), 10);
+
+          if (dim !== element['offset' + style.capitalize()])
+            return dim + 'px';
+
+          var properties;
+          if (style === 'height') {
+            properties = ['border-top-width', 'padding-top',
+             'padding-bottom', 'border-bottom-width'];
+          }
+          else {
+            properties = ['border-left-width', 'padding-left',
+             'padding-right', 'border-right-width'];
+          }
+          return properties.inject(dim, function(memo, property) {
+            var val = proceed(element, property);
+            return val === null ? memo : memo - parseInt(val, 10);
+          }) + 'px';
+        default: return proceed(element, style);
+      }
+    }
+  );
+
+  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+    function(proceed, element, attribute) {
+      if (attribute === 'title') return element.title;
+      return proceed(element, attribute);
+    }
+  );
+}
+
+else if (Prototype.Browser.IE) {
+  // IE doesn't report offsets correctly for static elements, so we change them
+  // to "relative" to get the values, then change them back.
+  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+    function(proceed, element) {
+      element = $(element);
+      // IE throws an error if element is not in document
+      try { element.offsetParent }
+      catch(e) { return $(document.body) }
+      var position = element.getStyle('position');
+      if (position !== 'static') return proceed(element);
+      element.setStyle({ position: 'relative' });
+      var value = proceed(element);
+      element.setStyle({ position: position });
+      return value;
+    }
+  );
+
+  $w('positionedOffset viewportOffset').each(function(method) {
+    Element.Methods[method] = Element.Methods[method].wrap(
+      function(proceed, element) {
+        element = $(element);
+        try { element.offsetParent }
+        catch(e) { return Element._returnOffset(0,0) }
+        var position = element.getStyle('position');
+        if (position !== 'static') return proceed(element);
+        // Trigger hasLayout on the offset parent so that IE6 reports
+        // accurate offsetTop and offsetLeft values for position: fixed.
+        var offsetParent = element.getOffsetParent();
+        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+          offsetParent.setStyle({ zoom: 1 });
+        element.setStyle({ position: 'relative' });
+        var value = proceed(element);
+        element.setStyle({ position: position });
+        return value;
+      }
+    );
+  });
+
+  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
+    function(proceed, element) {
+      try { element.offsetParent }
+      catch(e) { return Element._returnOffset(0,0) }
+      return proceed(element);
+    }
+  );
+
+  Element.Methods.getStyle = function(element, style) {
+    element = $(element);
+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value && element.currentStyle) value = element.currentStyle[style];
+
+    if (style == 'opacity') {
+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if (value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+
+    if (value == 'auto') {
+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+        return element['offset' + style.capitalize()] + 'px';
+      return null;
+    }
+    return value;
+  };
+
+  Element.Methods.setOpacity = function(element, value) {
+    function stripAlpha(filter){
+      return filter.replace(/alpha\([^\)]*\)/gi,'');
+    }
+    element = $(element);
+    var currentStyle = element.currentStyle;
+    if ((currentStyle && !currentStyle.hasLayout) ||
+      (!currentStyle && element.style.zoom == 'normal'))
+        element.style.zoom = 1;
+
+    var filter = element.getStyle('filter'), style = element.style;
+    if (value == 1 || value === '') {
+      (filter = stripAlpha(filter)) ?
+        style.filter = filter : style.removeAttribute('filter');
+      return element;
+    } else if (value < 0.00001) value = 0;
+    style.filter = stripAlpha(filter) +
+      'alpha(opacity=' + (value * 100) + ')';
+    return element;
+  };
+
+  Element._attributeTranslations = {
+    read: {
+      names: {
+        'class': 'className',
+        'for':   'htmlFor'
+      },
+      values: {
+        _getAttr: function(element, attribute) {
+          return element.getAttribute(attribute, 2);
+        },
+        _getAttrNode: function(element, attribute) {
+          var node = element.getAttributeNode(attribute);
+          return node ? node.value : "";
+        },
+        _getEv: function(element, attribute) {
+          attribute = element.getAttribute(attribute);
+          return attribute ? attribute.toString().slice(23, -2) : null;
+        },
+        _flag: function(element, attribute) {
+          return $(element).hasAttribute(attribute) ? attribute : null;
+        },
+        style: function(element) {
+          return element.style.cssText.toLowerCase();
+        },
+        title: function(element) {
+          return element.title;
+        }
+      }
+    }
+  };
+
+  Element._attributeTranslations.write = {
+    names: Object.extend({
+      cellpadding: 'cellPadding',
+      cellspacing: 'cellSpacing'
+    }, Element._attributeTranslations.read.names),
+    values: {
+      checked: function(element, value) {
+        element.checked = !!value;
+      },
+
+      style: function(element, value) {
+        element.style.cssText = value ? value : '';
+      }
+    }
+  };
+
+  Element._attributeTranslations.has = {};
+
+  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
+    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+  });
+
+  (function(v) {
+    Object.extend(v, {
+      href:        v._getAttr,
+      src:         v._getAttr,
+      type:        v._getAttr,
+      action:      v._getAttrNode,
+      disabled:    v._flag,
+      checked:     v._flag,
+      readonly:    v._flag,
+      multiple:    v._flag,
+      onload:      v._getEv,
+      onunload:    v._getEv,
+      onclick:     v._getEv,
+      ondblclick:  v._getEv,
+      onmousedown: v._getEv,
+      onmouseup:   v._getEv,
+      onmouseover: v._getEv,
+      onmousemove: v._getEv,
+      onmouseout:  v._getEv,
+      onfocus:     v._getEv,
+      onblur:      v._getEv,
+      onkeypress:  v._getEv,
+      onkeydown:   v._getEv,
+      onkeyup:     v._getEv,
+      onsubmit:    v._getEv,
+      onreset:     v._getEv,
+      onselect:    v._getEv,
+      onchange:    v._getEv
+    });
+  })(Element._attributeTranslations.read.values);
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1) ? 0.999999 :
+      (value === '') ? '' : (value < 0.00001) ? 0 : value;
+    return element;
+  };
+}
+
+else if (Prototype.Browser.WebKit) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+
+    if (value == 1)
+      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
+        element.width++; element.width--;
+      } else try {
+        var n = document.createTextNode(' ');
+        element.appendChild(n);
+        element.removeChild(n);
+      } catch (e) { }
+
+    return element;
+  };
+
+  // Safari returns margins on body which is incorrect if the child is absolutely
+  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
+  // KHTML/WebKit only.
+  Element.Methods.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return Element._returnOffset(valueL, valueT);
+  };
+}
+
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+  Element.Methods.update = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+
+    content = Object.toHTML(content);
+    var tagName = element.tagName.toUpperCase();
+
+    if (tagName in Element._insertionTranslations.tags) {
+      $A(element.childNodes).each(function(node) { element.removeChild(node) });
+      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+        .each(function(node) { element.appendChild(node) });
+    }
+    else element.innerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+if ('outerHTML' in document.createElement('div')) {
+  Element.Methods.replace = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) {
+      element.parentNode.replaceChild(content, element);
+      return element;
+    }
+
+    content = Object.toHTML(content);
+    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+    if (Element._insertionTranslations.tags[tagName]) {
+      var nextSibling = element.next();
+      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+      parent.removeChild(element);
+      if (nextSibling)
+        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+      else
+        fragments.each(function(node) { parent.appendChild(node) });
+    }
+    else element.outerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+Element._returnOffset = function(l, t) {
+  var result = [l, t];
+  result.left = l;
+  result.top = t;
+  return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+  if (t) {
+    div.innerHTML = t[0] + html + t[1];
+    t[2].times(function() { div = div.firstChild });
+  } else div.innerHTML = html;
+  return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+  before: function(element, node) {
+    element.parentNode.insertBefore(node, element);
+  },
+  top: function(element, node) {
+    element.insertBefore(node, element.firstChild);
+  },
+  bottom: function(element, node) {
+    element.appendChild(node);
+  },
+  after: function(element, node) {
+    element.parentNode.insertBefore(node, element.nextSibling);
+  },
+  tags: {
+    TABLE:  ['<table>',                '</table>',                   1],
+    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
+    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
+    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+    SELECT: ['<select>',               '</select>',                  1]
+  }
+};
+
+(function() {
+  Object.extend(this.tags, {
+    THEAD: this.tags.TBODY,
+    TFOOT: this.tags.TBODY,
+    TH:    this.tags.TD
+  });
+}).call(Element._insertionTranslations);
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    attribute = Element._attributeTranslations.has[attribute] || attribute;
+    var node = $(element).getAttributeNode(attribute);
+    return !!(node && node.specified);
+  }
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+    document.createElement('div')['__proto__']) {
+  window.HTMLElement = { };
+  window.HTMLElement.prototype = document.createElement('div')['__proto__'];
+  Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.extend = (function() {
+  if (Prototype.BrowserFeatures.SpecificElementExtensions)
+    return Prototype.K;
+
+  var Methods = { }, ByTag = Element.Methods.ByTag;
+
+  var extend = Object.extend(function(element) {
+    if (!element || element._extendedByPrototype ||
+        element.nodeType != 1 || element == window) return element;
+
+    var methods = Object.clone(Methods),
+      tagName = element.tagName.toUpperCase(), property, value;
+
+    // extend methods for specific tags
+    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+    for (property in methods) {
+      value = methods[property];
+      if (Object.isFunction(value) && !(property in element))
+        element[property] = value.methodize();
+    }
+
+    element._extendedByPrototype = Prototype.emptyFunction;
+    return element;
+
+  }, {
+    refresh: function() {
+      // extend methods for all tags (Safari doesn't need this)
+      if (!Prototype.BrowserFeatures.ElementExtensions) {
+        Object.extend(Methods, Element.Methods);
+        Object.extend(Methods, Element.Methods.Simulated);
+      }
+    }
+  });
+
+  extend.refresh();
+  return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+  if (element.hasAttribute) return element.hasAttribute(attribute);
+  return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+  if (!methods) {
+    Object.extend(Form, Form.Methods);
+    Object.extend(Form.Element, Form.Element.Methods);
+    Object.extend(Element.Methods.ByTag, {
+      "FORM":     Object.clone(Form.Methods),
+      "INPUT":    Object.clone(Form.Element.Methods),
+      "SELECT":   Object.clone(Form.Element.Methods),
+      "TEXTAREA": Object.clone(Form.Element.Methods)
+    });
+  }
+
+  if (arguments.length == 2) {
+    var tagName = methods;
+    methods = arguments[1];
+  }
+
+  if (!tagName) Object.extend(Element.Methods, methods || { });
+  else {
+    if (Object.isArray(tagName)) tagName.each(extend);
+    else extend(tagName);
+  }
+
+  function extend(tagName) {
+    tagName = tagName.toUpperCase();
+    if (!Element.Methods.ByTag[tagName])
+      Element.Methods.ByTag[tagName] = { };
+    Object.extend(Element.Methods.ByTag[tagName], methods);
+  }
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!Object.isFunction(value)) continue;
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = value.methodize();
+    }
+  }
+
+  function findDOMClass(tagName) {
+    var klass;
+    var trans = {
+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+      "FrameSet", "IFRAME": "IFrame"
+    };
+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName.capitalize() + 'Element';
+    if (window[klass]) return window[klass];
+
+    window[klass] = { };
+    window[klass].prototype = document.createElement(tagName)['__proto__'];
+    return window[klass];
+  }
+
+  if (F.ElementExtensions) {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+  }
+
+  if (F.SpecificElementExtensions) {
+    for (var tag in Element.Methods.ByTag) {
+      var klass = findDOMClass(tag);
+      if (Object.isUndefined(klass)) continue;
+      copy(T[tag], klass.prototype);
+    }
+  }
+
+  Object.extend(Element, Element.Methods);
+  delete Element.ByTag;
+
+  if (Element.extend.refresh) Element.extend.refresh();
+  Element.cache = { };
+};
+
+document.viewport = {
+  getDimensions: function() {
+    var dimensions = { }, B = Prototype.Browser;
+    $w('width height').each(function(d) {
+      var D = d.capitalize();
+      if (B.WebKit && !document.evaluate) {
+        // Safari <3.0 needs self.innerWidth/Height
+        dimensions[d] = self['inner' + D];
+      } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
+        // Opera <9.5 needs document.body.clientWidth/Height
+        dimensions[d] = document.body['client' + D]
+      } else {
+        dimensions[d] = document.documentElement['client' + D];
+      }
+    });
+    return dimensions;
+  },
+
+  getWidth: function() {
+    return this.getDimensions().width;
+  },
+
+  getHeight: function() {
+    return this.getDimensions().height;
+  },
+
+  getScrollOffsets: function() {
+    return Element._returnOffset(
+      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+  }
+};
+/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license.  Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create({
+  initialize: function(expression) {
+    this.expression = expression.strip();
+
+    if (this.shouldUseSelectorsAPI()) {
+      this.mode = 'selectorsAPI';
+    } else if (this.shouldUseXPath()) {
+      this.mode = 'xpath';
+      this.compileXPathMatcher();
+    } else {
+      this.mode = "normal";
+      this.compileMatcher();
+    }
+
+  },
+
+  shouldUseXPath: function() {
+    if (!Prototype.BrowserFeatures.XPath) return false;
+
+    var e = this.expression;
+
+    // Safari 3 chokes on :*-of-type and :empty
+    if (Prototype.Browser.WebKit &&
+     (e.include("-of-type") || e.include(":empty")))
+      return false;
+
+    // XPath can't do namespaced attributes, nor can it read
+    // the "checked" property from DOM nodes
+    if ((/(\[[\w-]*?:|:checked)/).test(e))
+      return false;
+
+    return true;
+  },
+
+  shouldUseSelectorsAPI: function() {
+    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;
+
+    if (!Selector._div) Selector._div = new Element('div');
+
+    // Make sure the browser treats the selector as valid. Test on an
+    // isolated element to minimize cost of this check.
+    try {
+      Selector._div.querySelector(this.expression);
+    } catch(e) {
+      return false;
+    }
+
+    return true;
+  },
+
+  compileMatcher: function() {
+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+        c = Selector.criteria, le, p, m;
+
+    if (Selector._cache[e]) {
+      this.matcher = Selector._cache[e];
+      return;
+    }
+
+    this.matcher = ["this.matcher = function(root) {",
+                    "var r = root, h = Selector.handlers, c = false, n;"];
+
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+            new Template(c[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.matcher.push("return h.unique(n);\n}");
+    eval(this.matcher.join('\n'));
+    Selector._cache[this.expression] = this.matcher;
+  },
+
+  compileXPathMatcher: function() {
+    var e = this.expression, ps = Selector.patterns,
+        x = Selector.xpath, le, m;
+
+    if (Selector._cache[e]) {
+      this.xpath = Selector._cache[e]; return;
+    }
+
+    this.matcher = ['.//*'];
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        if (m = e.match(ps[i])) {
+          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+            new Template(x[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.xpath = this.matcher.join('');
+    Selector._cache[this.expression] = this.xpath;
+  },
+
+  findElements: function(root) {
+    root = root || document;
+    var e = this.expression, results;
+
+    switch (this.mode) {
+      case 'selectorsAPI':
+        // querySelectorAll queries document-wide, then filters to descendants
+        // of the context element. That's not what we want.
+        // Add an explicit context to the selector if necessary.
+        if (root !== document) {
+          var oldId = root.id, id = $(root).identify();
+          e = "#" + id + " " + e;
+        }
+
+        results = $A(root.querySelectorAll(e)).map(Element.extend);
+        root.id = oldId;
+
+        return results;
+      case 'xpath':
+        return document._getElementsByXPath(this.xpath, root);
+      default:
+       return this.matcher(root);
+    }
+  },
+
+  match: function(element) {
+    this.tokens = [];
+
+    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+    var le, p, m;
+
+    while (e && le !== e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          // use the Selector.assertions methods unless the selector
+          // is too complex.
+          if (as[i]) {
+            this.tokens.push([i, Object.clone(m)]);
+            e = e.replace(m[0], '');
+          } else {
+            // reluctantly do a document-wide search
+            // and look for a match in the array
+            return this.findElements(document).include(element);
+          }
+        }
+      }
+    }
+
+    var match = true, name, matches;
+    for (var i = 0, token; token = this.tokens[i]; i++) {
+      name = token[0], matches = token[1];
+      if (!Selector.assertions[name](element, matches)) {
+        match = false; break;
+      }
+    }
+
+    return match;
+  },
+
+  toString: function() {
+    return this.expression;
+  },
+
+  inspect: function() {
+    return "#<Selector:" + this.expression.inspect() + ">";
+  }
+});
+
+Object.extend(Selector, {
+  _cache: { },
+
+  xpath: {
+    descendant:   "//*",
+    child:        "/*",
+    adjacent:     "/following-sibling::*[1]",
+    laterSibling: '/following-sibling::*',
+    tagName:      function(m) {
+      if (m[1] == '*') return '';
+      return "[local-name()='" + m[1].toLowerCase() +
+             "' or local-name()='" + m[1].toUpperCase() + "']";
+    },
+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+    id:           "[@id='#{1}']",
+    attrPresence: function(m) {
+      m[1] = m[1].toLowerCase();
+      return new Template("[@#{1}]").evaluate(m);
+    },
+    attr: function(m) {
+      m[1] = m[1].toLowerCase();
+      m[3] = m[5] || m[6];
+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+    },
+    pseudo: function(m) {
+      var h = Selector.xpath.pseudos[m[1]];
+      if (!h) return '';
+      if (Object.isFunction(h)) return h(m);
+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+    },
+    operators: {
+      '=':  "[@#{1}='#{3}']",
+      '!=': "[@#{1}!='#{3}']",
+      '^=': "[starts-with(@#{1}, '#{3}')]",
+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+      '*=': "[contains(@#{1}, '#{3}')]",
+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+    },
+    pseudos: {
+      'first-child': '[not(preceding-sibling::*)]',
+      'last-child':  '[not(following-sibling::*)]',
+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
+      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
+      'checked':     "[@checked]",
+      'disabled':    "[(@disabled) and (@type!='hidden')]",
+      'enabled':     "[not(@disabled) and (@type!='hidden')]",
+      'not': function(m) {
+        var e = m[6], p = Selector.patterns,
+            x = Selector.xpath, le, v;
+
+        var exclusion = [];
+        while (e && le != e && (/\S/).test(e)) {
+          le = e;
+          for (var i in p) {
+            if (m = e.match(p[i])) {
+              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+              e = e.replace(m[0], '');
+              break;
+            }
+          }
+        }
+        return "[not(" + exclusion.join(" and ") + ")]";
+      },
+      'nth-child':      function(m) {
+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+      },
+      'nth-last-child': function(m) {
+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+      },
+      'nth-of-type':    function(m) {
+        return Selector.xpath.pseudos.nth("position() ", m);
+      },
+      'nth-last-of-type': function(m) {
+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+      },
+      'first-of-type':  function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+      },
+      'last-of-type':   function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+      },
+      'only-of-type':   function(m) {
+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+      },
+      nth: function(fragment, m) {
+        var mm, formula = m[6], predicate;
+        if (formula == 'even') formula = '2n+0';
+        if (formula == 'odd')  formula = '2n+1';
+        if (mm = formula.match(/^(\d+)$/)) // digit only
+          return '[' + fragment + "= " + mm[1] + ']';
+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+          if (mm[1] == "-") mm[1] = -1;
+          var a = mm[1] ? Number(mm[1]) : 1;
+          var b = mm[2] ? Number(mm[2]) : 0;
+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+          "((#{fragment} - #{b}) div #{a} >= 0)]";
+          return new Template(predicate).evaluate({
+            fragment: fragment, a: a, b: b });
+        }
+      }
+    }
+  },
+
+  criteria: {
+    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
+    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
+    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
+    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
+    attr: function(m) {
+      m[3] = (m[5] || m[6]);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
+    },
+    pseudo: function(m) {
+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+    },
+    descendant:   'c = "descendant";',
+    child:        'c = "child";',
+    adjacent:     'c = "adjacent";',
+    laterSibling: 'c = "laterSibling";'
+  },
+
+  patterns: {
+    // combinators must be listed first
+    // (and descendant needs to be last combinator)
+    laterSibling: /^\s*~\s*/,
+    child:        /^\s*>\s*/,
+    adjacent:     /^\s*\+\s*/,
+    descendant:   /^\s/,
+
+    // selectors follow
+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
+    id:           /^#([\w\-\*]+)(\b|$)/,
+    className:    /^\.([\w\-\*]+)(\b|$)/,
+    pseudo:
+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
+    attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+  },
+
+  // for Selector.match and Element#match
+  assertions: {
+    tagName: function(element, matches) {
+      return matches[1].toUpperCase() == element.tagName.toUpperCase();
+    },
+
+    className: function(element, matches) {
+      return Element.hasClassName(element, matches[1]);
+    },
+
+    id: function(element, matches) {
+      return element.id === matches[1];
+    },
+
+    attrPresence: function(element, matches) {
+      return Element.hasAttribute(element, matches[1]);
+    },
+
+    attr: function(element, matches) {
+      var nodeValue = Element.readAttribute(element, matches[1]);
+      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
+    }
+  },
+
+  handlers: {
+    // UTILITY FUNCTIONS
+    // joins two collections
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        a.push(node);
+      return a;
+    },
+
+    // marks an array of nodes for counting
+    mark: function(nodes) {
+      var _true = Prototype.emptyFunction;
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._countedByPrototype = _true;
+      return nodes;
+    },
+
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._countedByPrototype = undefined;
+      return nodes;
+    },
+
+    // mark each child node with its position (for nth calls)
+    // "ofType" flag indicates whether we're indexing for nth-of-type
+    // rather than nth-child
+    index: function(parentNode, reverse, ofType) {
+      parentNode._countedByPrototype = Prototype.emptyFunction;
+      if (reverse) {
+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+          var node = nodes[i];
+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+        }
+      } else {
+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+      }
+    },
+
+    // filters out duplicates and extends all nodes
+    unique: function(nodes) {
+      if (nodes.length == 0) return nodes;
+      var results = [], n;
+      for (var i = 0, l = nodes.length; i < l; i++)
+        if (!(n = nodes[i])._countedByPrototype) {
+          n._countedByPrototype = Prototype.emptyFunction;
+          results.push(Element.extend(n));
+        }
+      return Selector.handlers.unmark(results);
+    },
+
+    // COMBINATOR FUNCTIONS
+    descendant: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, node.getElementsByTagName('*'));
+      return results;
+    },
+
+    child: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        for (var j = 0, child; child = node.childNodes[j]; j++)
+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+      }
+      return results;
+    },
+
+    adjacent: function(nodes) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        var next = this.nextElementSibling(node);
+        if (next) results.push(next);
+      }
+      return results;
+    },
+
+    laterSibling: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, Element.nextSiblings(node));
+      return results;
+    },
+
+    nextElementSibling: function(node) {
+      while (node = node.nextSibling)
+        if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    previousElementSibling: function(node) {
+      while (node = node.previousSibling)
+        if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    // TOKEN FUNCTIONS
+    tagName: function(nodes, root, tagName, combinator) {
+      var uTagName = tagName.toUpperCase();
+      var results = [], h = Selector.handlers;
+      if (nodes) {
+        if (combinator) {
+          // fastlane for ordinary descendant combinators
+          if (combinator == "descendant") {
+            for (var i = 0, node; node = nodes[i]; i++)
+              h.concat(results, node.getElementsByTagName(tagName));
+            return results;
+          } else nodes = this[combinator](nodes);
+          if (tagName == "*") return nodes;
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.tagName.toUpperCase() === uTagName) results.push(node);
+        return results;
+      } else return root.getElementsByTagName(tagName);
+    },
+
+    id: function(nodes, root, id, combinator) {
+      var targetNode = $(id), h = Selector.handlers;
+      if (!targetNode) return [];
+      if (!nodes && root == document) return [targetNode];
+      if (nodes) {
+        if (combinator) {
+          if (combinator == 'child') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (targetNode.parentNode == node) return [targetNode];
+          } else if (combinator == 'descendant') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Element.descendantOf(targetNode, node)) return [targetNode];
+          } else if (combinator == 'adjacent') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Selector.handlers.previousElementSibling(targetNode) == node)
+                return [targetNode];
+          } else nodes = h[combinator](nodes);
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node == targetNode) return [targetNode];
+        return [];
+      }
+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+    },
+
+    className: function(nodes, root, className, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      return Selector.handlers.byClassName(nodes, root, className);
+    },
+
+    byClassName: function(nodes, root, className) {
+      if (!nodes) nodes = Selector.handlers.descendant([root]);
+      var needle = ' ' + className + ' ';
+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+        nodeClassName = node.className;
+        if (nodeClassName.length == 0) continue;
+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+          results.push(node);
+      }
+      return results;
+    },
+
+    attrPresence: function(nodes, root, attr, combinator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      var results = [];
+      for (var i = 0, node; node = nodes[i]; i++)
+        if (Element.hasAttribute(node, attr)) results.push(node);
+      return results;
+    },
+
+    attr: function(nodes, root, attr, value, operator, combinator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      var handler = Selector.operators[operator], results = [];
+      for (var i = 0, node; node = nodes[i]; i++) {
+        var nodeValue = Element.readAttribute(node, attr);
+        if (nodeValue === null) continue;
+        if (handler(nodeValue, value)) results.push(node);
+      }
+      return results;
+    },
+
+    pseudo: function(nodes, name, value, root, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      return Selector.pseudos[name](nodes, value, root);
+    }
+  },
+
+  pseudos: {
+    'first-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.previousElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'last-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.nextElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'only-child': function(nodes, value, root) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+          results.push(node);
+      return results;
+    },
+    'nth-child':        function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root);
+    },
+    'nth-last-child':   function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true);
+    },
+    'nth-of-type':      function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, false, true);
+    },
+    'nth-last-of-type': function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true, true);
+    },
+    'first-of-type':    function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, false, true);
+    },
+    'last-of-type':     function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, true, true);
+    },
+    'only-of-type':     function(nodes, formula, root) {
+      var p = Selector.pseudos;
+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+    },
+
+    // handles the an+b logic
+    getIndices: function(a, b, total) {
+      if (a == 0) return b > 0 ? [b] : [];
+      return $R(1, total).inject([], function(memo, i) {
+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+        return memo;
+      });
+    },
+
+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+    nth: function(nodes, formula, root, reverse, ofType) {
+      if (nodes.length == 0) return [];
+      if (formula == 'even') formula = '2n+0';
+      if (formula == 'odd')  formula = '2n+1';
+      var h = Selector.handlers, results = [], indexed = [], m;
+      h.mark(nodes);
+      for (var i = 0, node; node = nodes[i]; i++) {
+        if (!node.parentNode._countedByPrototype) {
+          h.index(node.parentNode, reverse, ofType);
+          indexed.push(node.parentNode);
+        }
+      }
+      if (formula.match(/^\d+$/)) { // just a number
+        formula = Number(formula);
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.nodeIndex == formula) results.push(node);
+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+        if (m[1] == "-") m[1] = -1;
+        var a = m[1] ? Number(m[1]) : 1;
+        var b = m[2] ? Number(m[2]) : 0;
+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+          for (var j = 0; j < l; j++)
+            if (node.nodeIndex == indices[j]) results.push(node);
+        }
+      }
+      h.unmark(nodes);
+      h.unmark(indexed);
+      return results;
+    },
+
+    'empty': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        // IE treats comments as element nodes
+        if (node.tagName == '!' || node.firstChild) continue;
+        results.push(node);
+      }
+      return results;
+    },
+
+    'not': function(nodes, selector, root) {
+      var h = Selector.handlers, selectorType, m;
+      var exclusions = new Selector(selector).findElements(root);
+      h.mark(exclusions);
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node._countedByPrototype) results.push(node);
+      h.unmark(exclusions);
+      return results;
+    },
+
+    'enabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node.disabled && (!node.type || node.type !== 'hidden'))
+          results.push(node);
+      return results;
+    },
+
+    'disabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.disabled) results.push(node);
+      return results;
+    },
+
+    'checked': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.checked) results.push(node);
+      return results;
+    }
+  },
+
+  operators: {
+    '=':  function(nv, v) { return nv == v; },
+    '!=': function(nv, v) { return nv != v; },
+    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
+    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
+    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
+    '$=': function(nv, v) { return nv.endsWith(v); },
+    '*=': function(nv, v) { return nv.include(v); },
+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
+     '-').include('-' + (v || "").toUpperCase() + '-'); }
+  },
+
+  split: function(expression) {
+    var expressions = [];
+    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    return expressions;
+  },
+
+  matchElements: function(elements, expression) {
+    var matches = $$(expression), h = Selector.handlers;
+    h.mark(matches);
+    for (var i = 0, results = [], element; element = elements[i]; i++)
+      if (element._countedByPrototype) results.push(element);
+    h.unmark(matches);
+    return results;
+  },
+
+  findElement: function(elements, expression, index) {
+    if (Object.isNumber(expression)) {
+      index = expression; expression = false;
+    }
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    expressions = Selector.split(expressions.join(','));
+    var results = [], h = Selector.handlers;
+    for (var i = 0, l = expressions.length, selector; i < l; i++) {
+      selector = new Selector(expressions[i].strip());
+      h.concat(results, selector.findElements(element));
+    }
+    return (l > 1) ? h.unique(results) : results;
+  }
+});
+
+if (Prototype.Browser.IE) {
+  Object.extend(Selector.handlers, {
+    // IE returns comment nodes on getElementsByTagName("*").
+    // Filter them out.
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        if (node.tagName !== "!") a.push(node);
+      return a;
+    },
+
+    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node.removeAttribute('_countedByPrototype');
+      return nodes;
+    }
+  });
+}
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements, options) {
+    if (typeof options != 'object') options = { hash: !!options };
+    else if (Object.isUndefined(options.hash)) options.hash = true;
+    var key, value, submitted = false, submit = options.submit;
+
+    var data = elements.inject({ }, function(result, element) {
+      if (!element.disabled && element.name) {
+        key = element.name; value = $(element).getValue();
+        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
+            submit !== false && (!submit || key == submit) && (submitted = true)))) {
+          if (key in result) {
+            // a key is already present; construct an array of values
+            if (!Object.isArray(result[key])) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return options.hash ? data : Object.toQueryString(data);
+  }
+};
+
+Form.Methods = {
+  serialize: function(form, options) {
+    return Form.serializeElements(Form.getElements(form), options);
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) || (name && input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('disable');
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('enable');
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    var elements = $(form).getElements().findAll(function(element) {
+      return 'hidden' != element.type && !element.disabled;
+    });
+    var firstByIndex = elements.findAll(function(element) {
+      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+    }).sortBy(function(element) { return element.tabIndex }).first();
+
+    return firstByIndex ? firstByIndex : elements.find(function(element) {
+      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  },
+
+  request: function(form, options) {
+    form = $(form), options = Object.clone(options || { });
+
+    var params = options.parameters, action = form.readAttribute('action') || '';
+    if (action.blank()) action = window.location.href;
+    options.parameters = form.serialize(true);
+
+    if (params) {
+      if (Object.isString(params)) params = params.toQueryParams();
+      Object.extend(options.parameters, params);
+    }
+
+    if (form.hasAttribute('method') && !options.method)
+      options.method = form.method;
+
+    return new Ajax.Request(action, options);
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+};
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        var pair = { };
+        pair[element.name] = value;
+        return Object.toQueryString(pair);
+      }
+    }
+    return '';
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    return Form.Element.Serializers[method](element);
+  },
+
+  setValue: function(element, value) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    Form.Element.Serializers[method](element, value);
+    return element;
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    try {
+      element.focus();
+      if (element.select && (element.tagName.toLowerCase() != 'input' ||
+          !['button', 'reset', 'submit'].include(element.type)))
+        element.select();
+    } catch (e) { }
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.disabled = false;
+    return element;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element, value) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element, value);
+      default:
+        return Form.Element.Serializers.textarea(element, value);
+    }
+  },
+
+  inputSelector: function(element, value) {
+    if (Object.isUndefined(value)) return element.checked ? element.value : null;
+    else element.checked = !!value;
+  },
+
+  textarea: function(element, value) {
+    if (Object.isUndefined(value)) return element.value;
+    else element.value = value;
+  },
+
+  select: function(element, value) {
+    if (Object.isUndefined(value))
+      return this[element.type == 'select-one' ?
+        'selectOne' : 'selectMany'](element);
+    else {
+      var opt, currentValue, single = !Object.isArray(value);
+      for (var i = 0, length = element.length; i < length; i++) {
+        opt = element.options[i];
+        currentValue = this.optionValue(opt);
+        if (single) {
+          if (currentValue == value) {
+            opt.selected = true;
+            return;
+          }
+        }
+        else opt.selected = value.include(currentValue);
+      }
+    }
+  },
+
+  selectOne: function(element) {
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
+  },
+
+  selectMany: function(element) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) values.push(this.optionValue(opt));
+    }
+    return values;
+  },
+
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+  initialize: function($super, element, frequency, callback) {
+    $super(callback, frequency);
+    this.element   = $(element);
+    this.lastValue = this.getValue();
+  },
+
+  execute: function() {
+    var value = this.getValue();
+    if (Object.isString(this.lastValue) && Object.isString(value) ?
+        this.lastValue != value : String(this.lastValue) != String(value)) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create(Abstract.TimedObserver, {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = Class.create({
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    Form.getElements(this.element).each(this.registerCallback, this);
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+});
+
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create(Abstract.EventObserver, {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) var Event = { };
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+  KEY_INSERT:   45,
+
+  cache: { },
+
+  relatedTarget: function(event) {
+    var element;
+    switch(event.type) {
+      case 'mouseover': element = event.fromElement; break;
+      case 'mouseout':  element = event.toElement;   break;
+      default: return null;
+    }
+    return Element.extend(element);
+  }
+});
+
+Event.Methods = (function() {
+  var isButton;
+
+  if (Prototype.Browser.IE) {
+    var buttonMap = { 0: 1, 1: 4, 2: 2 };
+    isButton = function(event, code) {
+      return event.button == buttonMap[code];
+    };
+
+  } else if (Prototype.Browser.WebKit) {
+    isButton = function(event, code) {
+      switch (code) {
+        case 0: return event.which == 1 && !event.metaKey;
+        case 1: return event.which == 1 && event.metaKey;
+        default: return false;
+      }
+    };
+
+  } else {
+    isButton = function(event, code) {
+      return event.which ? (event.which === code + 1) : (event.button === code);
+    };
+  }
+
+  return {
+    isLeftClick:   function(event) { return isButton(event, 0) },
+    isMiddleClick: function(event) { return isButton(event, 1) },
+    isRightClick:  function(event) { return isButton(event, 2) },
+
+    element: function(event) {
+      event = Event.extend(event);
+
+      var node          = event.target,
+          type          = event.type,
+          currentTarget = event.currentTarget;
+
+      if (currentTarget && currentTarget.tagName) {
+        // Firefox screws up the "click" event when moving between radio buttons
+        // via arrow keys. It also screws up the "load" and "error" events on images,
+        // reporting the document as the target instead of the original image.
+        if (type === 'load' || type === 'error' ||
+          (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
+            && currentTarget.type === 'radio'))
+              node = currentTarget;
+      }
+      if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
+      return Element.extend(node);
+    },
+
+    findElement: function(event, expression) {
+      var element = Event.element(event);
+      if (!expression) return element;
+      var elements = [element].concat(element.ancestors());
+      return Selector.findElement(elements, expression, 0);
+    },
+
+    pointer: function(event) {
+      var docElement = document.documentElement,
+      body = document.body || { scrollLeft: 0, scrollTop: 0 };
+      return {
+        x: event.pageX || (event.clientX +
+          (docElement.scrollLeft || body.scrollLeft) -
+          (docElement.clientLeft || 0)),
+        y: event.pageY || (event.clientY +
+          (docElement.scrollTop || body.scrollTop) -
+          (docElement.clientTop || 0))
+      };
+    },
+
+    pointerX: function(event) { return Event.pointer(event).x },
+    pointerY: function(event) { return Event.pointer(event).y },
+
+    stop: function(event) {
+      Event.extend(event);
+      event.preventDefault();
+      event.stopPropagation();
+      event.stopped = true;
+    }
+  };
+})();
+
+Event.extend = (function() {
+  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+    m[name] = Event.Methods[name].methodize();
+    return m;
+  });
+
+  if (Prototype.Browser.IE) {
+    Object.extend(methods, {
+      stopPropagation: function() { this.cancelBubble = true },
+      preventDefault:  function() { this.returnValue = false },
+      inspect: function() { return "[object Event]" }
+    });
+
+    return function(event) {
+      if (!event) return false;
+      if (event._extendedByPrototype) return event;
+
+      event._extendedByPrototype = Prototype.emptyFunction;
+      var pointer = Event.pointer(event);
+      Object.extend(event, {
+        target: event.srcElement,
+        relatedTarget: Event.relatedTarget(event),
+        pageX:  pointer.x,
+        pageY:  pointer.y
+      });
+      return Object.extend(event, methods);
+    };
+
+  } else {
+    Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
+    Object.extend(Event.prototype, methods);
+    return Prototype.K;
+  }
+})();
+
+Object.extend(Event, (function() {
+  var cache = Event.cache;
+
+  function getEventID(element) {
+    if (element._prototypeEventID) return element._prototypeEventID[0];
+    arguments.callee.id = arguments.callee.id || 1;
+    return element._prototypeEventID = [++arguments.callee.id];
+  }
+
+  function getDOMEventName(eventName) {
+    if (eventName && eventName.include(':')) return "dataavailable";
+    return eventName;
+  }
+
+  function getCacheForID(id) {
+    return cache[id] = cache[id] || { };
+  }
+
+  function getWrappersForEventName(id, eventName) {
+    var c = getCacheForID(id);
+    return c[eventName] = c[eventName] || [];
+  }
+
+  function createWrapper(element, eventName, handler) {
+    var id = getEventID(element);
+    var c = getWrappersForEventName(id, eventName);
+    if (c.pluck("handler").include(handler)) return false;
+
+    var wrapper = function(event) {
+      if (!Event || !Event.extend ||
+        (event.eventName && event.eventName != eventName))
+          return false;
+
+      Event.extend(event);
+      handler.call(element, event);
+    };
+
+    wrapper.handler = handler;
+    c.push(wrapper);
+    return wrapper;
+  }
+
+  function findWrapper(id, eventName, handler) {
+    var c = getWrappersForEventName(id, eventName);
+    return c.find(function(wrapper) { return wrapper.handler == handler });
+  }
+
+  function destroyWrapper(id, eventName, handler) {
+    var c = getCacheForID(id);
+    if (!c[eventName]) return false;
+    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+  }
+
+  function destroyCache() {
+    for (var id in cache)
+      for (var eventName in cache[id])
+        cache[id][eventName] = null;
+  }
+
+
+  // Internet Explorer needs to remove event handlers on page unload
+  // in order to avoid memory leaks.
+  if (window.attachEvent) {
+    window.attachEvent("onunload", destroyCache);
+  }
+
+  // Safari has a dummy event handler on page unload so that it won't
+  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
+  // object when page is returned to via the back button using its bfcache.
+  if (Prototype.Browser.WebKit) {
+    window.addEventListener('unload', Prototype.emptyFunction, false);
+  }
+
+  return {
+    observe: function(element, eventName, handler) {
+      element = $(element);
+      var name = getDOMEventName(eventName);
+
+      var wrapper = createWrapper(element, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.addEventListener) {
+        element.addEventListener(name, wrapper, false);
+      } else {
+        element.attachEvent("on" + name, wrapper);
+      }
+
+      return element;
+    },
+
+    stopObserving: function(element, eventName, handler) {
+      element = $(element);
+      var id = getEventID(element), name = getDOMEventName(eventName);
+
+      if (!handler && eventName) {
+        getWrappersForEventName(id, eventName).each(function(wrapper) {
+          element.stopObserving(eventName, wrapper.handler);
+        });
+        return element;
+
+      } else if (!eventName) {
+        Object.keys(getCacheForID(id)).each(function(eventName) {
+          element.stopObserving(eventName);
+        });
+        return element;
+      }
+
+      var wrapper = findWrapper(id, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.removeEventListener) {
+        element.removeEventListener(name, wrapper, false);
+      } else {
+        element.detachEvent("on" + name, wrapper);
+      }
+
+      destroyWrapper(id, eventName, handler);
+
+      return element;
+    },
+
+    fire: function(element, eventName, memo) {
+      element = $(element);
+      if (element == document && document.createEvent && !element.dispatchEvent)
+        element = document.documentElement;
+
+      var event;
+      if (document.createEvent) {
+        event = document.createEvent("HTMLEvents");
+        event.initEvent("dataavailable", true, true);
+      } else {
+        event = document.createEventObject();
+        event.eventType = "ondataavailable";
+      }
+
+      event.eventName = eventName;
+      event.memo = memo || { };
+
+      if (document.createEvent) {
+        element.dispatchEvent(event);
+      } else {
+        element.fireEvent(event.eventType, event);
+      }
+
+      return Event.extend(event);
+    }
+  };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+  fire:          Event.fire,
+  observe:       Event.observe,
+  stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+  fire:          Element.Methods.fire.methodize(),
+  observe:       Element.Methods.observe.methodize(),
+  stopObserving: Element.Methods.stopObserving.methodize(),
+  loaded:        false
+});
+
+(function() {
+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+     Matthias Miller, Dean Edwards and John Resig. */
+
+  var timer;
+
+  function fireContentLoadedEvent() {
+    if (document.loaded) return;
+    if (timer) window.clearInterval(timer);
+    document.fire("dom:loaded");
+    document.loaded = true;
+  }
+
+  if (document.addEventListener) {
+    if (Prototype.Browser.WebKit) {
+      timer = window.setInterval(function() {
+        if (/loaded|complete/.test(document.readyState))
+          fireContentLoadedEvent();
+      }, 0);
+
+      Event.observe(window, "load", fireContentLoadedEvent);
+
+    } else {
+      document.addEventListener("DOMContentLoaded",
+        fireContentLoadedEvent, false);
+    }
+
+  } else {
+    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+    $("__onDOMContentLoaded").onreadystatechange = function() {
+      if (this.readyState == "complete") {
+        this.onreadystatechange = null;
+        fireContentLoadedEvent();
+      }
+    };
+  }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+  Before: function(element, content) {
+    return Element.insert(element, {before:content});
+  },
+
+  Top: function(element, content) {
+    return Element.insert(element, {top:content});
+  },
+
+  Bottom: function(element, content) {
+    return Element.insert(element, {bottom:content});
+  },
+
+  After: function(element, content) {
+    return Element.insert(element, {after:content});
+  }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = Element.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = Element.cumulativeScrollOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = Element.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  // Deprecation layer -- use newer Element methods now (1.5.2).
+
+  cumulativeOffset: Element.Methods.cumulativeOffset,
+
+  positionedOffset: Element.Methods.positionedOffset,
+
+  absolutize: function(element) {
+    Position.prepare();
+    return Element.absolutize(element);
+  },
+
+  relativize: function(element) {
+    Position.prepare();
+    return Element.relativize(element);
+  },
+
+  realOffset: Element.Methods.cumulativeScrollOffset,
+
+  offsetParent: Element.Methods.getOffsetParent,
+
+  page: Element.Methods.viewportOffset,
+
+  clone: function(source, target, options) {
+    options = options || { };
+    return Element.clonePosition(target, source, options);
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+  function iter(name) {
+    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+  }
+
+  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+  function(element, className) {
+    className = className.toString().strip();
+    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+  } : function(element, className) {
+    className = className.toString().strip();
+    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+    if (!classNames && !className) return elements;
+
+    var nodes = $(element).getElementsByTagName('*');
+    className = ' ' + className + ' ';
+
+    for (var i = 0, child, cn; child = nodes[i]; i++) {
+      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+          (classNames && classNames.all(function(name) {
+            return !name.toString().blank() && cn.include(' ' + name + ' ');
+          }))))
+        elements.push(Element.extend(child));
+    }
+    return elements;
+  };
+
+  return function(className, parentElement) {
+    return $(parentElement || document.body).getElementsByClassName(className);
+  };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
+
+Element.addMethods();
\ No newline at end of file
diff --git a/js/src/selectlist.js b/js/src/selectlist.js
new file mode 100644
index 0000000..9e18993
--- /dev/null
+++ b/js/src/selectlist.js
@@ -0,0 +1,24 @@
+/**
+ * Provides the javascript for the selectlist.php script.
+ *
+ * $Horde: gollem/js/src/selectlist.js,v 1.1.2.1 2008/10/09 20:54:41 jan Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+function returnID()
+{
+    var field = parent.opener.document[formid].selectlist_selectid, field2 = parent.opener.document[formid].actionID;
+
+    if (parent.opener.closed || !field || !field2) {
+        alert(GollemText.opener_window);
+        window.close();
+        return;
+    }
+
+    field.value = cacheid;
+    field2.value = 'selectlist_process';
+
+    parent.opener.document[formid].submit();
+    window.close();
+}
diff --git a/js/src/tables.js b/js/src/tables.js
new file mode 100644
index 0000000..e3ffa16
--- /dev/null
+++ b/js/src/tables.js
@@ -0,0 +1,264 @@
+/**
+ * Javascript code for finding all tables with classname "striped" and
+ * dynamically striping their row colors, and finding all tables with
+ * classname "sortable" and making them dynamically sortable.
+ *
+ * TODO: incorporate missing features (if wanted) and improvements from:
+ * http://tetlaw.id.au/view/blog/table-sorting-with-prototype/
+ * http://www.millstream.com.au/view/code/tablekit/
+ * http://tablesorter.com/docs/
+ *
+ * $Horde: gollem/js/src/tables.js,v 1.8.2.1 2008/10/09 20:54:41 jan Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+
+var SORT_COLUMN_INDEX;
+
+function table_stripe(table)
+{
+    var classes = [ 'rowEven', 'rowOdd' ];
+
+    // Tables can have more than one tbody element; get all child
+    // tbody tags and interate through them.
+    table.select('tbody tr').each(function(c) {
+        c.removeClassName(classes[1]);
+        c.addClassName(classes[0]);
+        classes.reverse(true);
+    });
+}
+
+function table_makeSortable(table)
+{
+    var i = 0;
+
+    // We have a first row: assume it's the header, and make its
+    // contents clickable links.
+    table.down('tr').childElements().each(function(cell) {
+        if (cell.hasClassName('nosort')) {
+            ++i;
+            return;
+        }
+
+        cell.setAttribute('columnIndex', i++);
+        cell.setStyle({ cursor: 'pointer' });
+        cell.observe('click', function(e) {
+            var c = e.findElement('th');
+            e.stop();
+            var a = c.down('a');
+            if (a && !a.hasClassName('sortlink')) {
+                return true;
+            }
+            table_resortTable(c);
+            return false;
+        });
+    });
+}
+
+function table_getSortValue(el)
+{
+    var str = '';
+
+    if (Object.isString(el) || Object.isUndefined(el)) {
+        return el;
+    }
+
+    // Use "sortval" if defined.
+    el = $(el);
+    if (el.readAttribute('sortval')) {
+        return el.readAttribute('sortval');
+    }
+
+    if (el.innerText) {
+        // Not needed but it is faster.
+        return el.innerText;
+    }
+
+    $A(el.childNodes).each(function(e) {
+        switch (e.nodeType) {
+        case 1:
+            // ELEMENT_NODE
+            str += table_getSortValue(e);
+            break;
+
+        case 3:
+            // TEXT_NODE
+            str += e.nodeValue;
+            break;
+        }
+    });
+
+    return str;
+}
+
+function table_resortTable(th)
+{
+    var table = th.up('table'),
+        th_siblings = th.up().childElements(),
+        sortfn,
+        sortDown = 0;
+
+    th_siblings.each(function(e) {
+        if (th == e) {
+            if (e.hasClassName('sortup')) {
+                e.removeClassName('sortup');
+                e.addClassName('sortdown');
+            } else if (e.hasClassName('sortdown')) {
+                e.removeClassName('sortdown');
+                e.addClassName('sortup');
+                sortDown = 1;
+            } else {
+                e.addClassName('sortdown');
+            }
+        } else {
+            e.removeClassName('sortup');
+            e.removeClassName('sortdown');
+        }
+    });
+
+    // Work out a type for the column
+    if (th_siblings.size() <= 1) {
+        return;
+    }
+
+    SORT_COLUMN_INDEX = th.readAttribute('columnIndex');
+    var itm = table_getSortValue(table.down('tbody > tr').cells[SORT_COLUMN_INDEX]);
+
+    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/) ||
+        itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) {
+        sortfn = table_sort_date;
+    } else if (itm.match(/^[£$]/)) {
+        sortfn = table_sort_currency;
+    } else if (itm.match(/^[\d\.]+$/)) {
+        sortfn = table_sort_numeric;
+    } else {
+        sortfn = table_sort_caseinsensitive;
+    }
+
+    // Don't mix up seperate tbodies; sort each in turn.
+    $A(table.getElementsByTagName('tbody')).each(function(tbody) {
+        var bottomRows = [ ],
+            newRows = $A(tbody.getElementsByTagName('tr'));
+
+        newRows.sort(sortfn);
+        if (sortDown) {
+            newRows.reverse();
+        }
+
+        // We appendChild rows that already exist to the tbody, so it
+        // moves them rather than creating new ones. Don't do
+        // sortbottom rows.
+        newRows.each(function(r) {
+            if (r.hasClassName('sortbottom')) {
+                bottomRows.push(r);
+            } else {
+                tbody.appendChild(r);
+            }
+        });
+
+        // Do sortbottom rows only.
+        bottomRows.each(function(r) {
+            tbody.appendChild(r);
+        });
+    });
+
+    // If we just resorted a striped table, re-stripe it.
+    if (table.hasClassName('striped')) {
+        table_stripe(table);
+    }
+
+    // Finally, see if we have a callback function to trigger.
+    if (typeof table_sortCallback != 'undefined' && Object.isFunction(table_sortCallback)) {
+        table_sortCallback(table.id, th.id, sortDown);
+    }
+}
+
+function table_sort_date(a, b)
+{
+    // Two digit years less than 50 are treated as 20XX, greater than
+    // 50 are treated as 19XX.
+    var aa = table_getSortValue(a.cells[SORT_COLUMN_INDEX]),
+        bb = table_getSortValue(b.cells[SORT_COLUMN_INDEX]),
+        dt1, dt2, yr;
+
+    if (aa.length == 10) {
+        dt1 = aa.substr(6, 4) + aa.substr(3, 2) + aa.substr(0, 2);
+    } else {
+        yr = aa.substr(6, 2);
+        if (parseInt(yr) < 50) {
+            yr = '20' + yr;
+        } else {
+            yr = '19' + yr;
+        }
+        dt1 = yr + aa.substr(3, 2) + aa.substr(0, 2);
+    }
+    if (bb.length == 10) {
+        dt2 = bb.substr(6, 4) + bb.substr(3, 2) + bb.substr(0, 2);
+    } else {
+        yr = bb.substr(6, 2);
+        if (parseInt(yr) < 50) {
+            yr = '20' + yr;
+        } else {
+            yr = '19' + yr;
+        }
+        dt2 = yr + bb.substr(3, 2) + bb.substr(0, 2);
+    }
+    if (dt1 == dt2) {
+        return 0;
+    } else if (dt1 < dt2) {
+        return -1;
+    }
+    return 1;
+}
+
+function table_sort_currency(a, b)
+{
+    var aa = table_getSortValue(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g, ''),
+        bb = table_getSortValue(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g, '');
+    return parseFloat(aa) - parseFloat(bb);
+}
+
+function table_sort_numeric(a, b)
+{
+    var aa = parseFloat(table_getSortValue(a.cells[SORT_COLUMN_INDEX]));
+    if (isNaN(aa)) {
+        aa = 0;
+    }
+    var bb = parseFloat(table_getSortValue(b.cells[SORT_COLUMN_INDEX]));
+    if (isNaN(bb)) {
+        bb = 0;
+    }
+    return aa - bb;
+}
+
+function table_sort_caseinsensitive(a, b)
+{
+    var aa = table_getSortValue(a.cells[SORT_COLUMN_INDEX]).toLowerCase(),
+        bb = table_getSortValue(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
+    if (aa == bb) {
+        return 0;
+    } else if (aa < bb) {
+        return -1;
+    }
+    return 1;
+}
+
+function table_sort_default(a, b)
+{
+    var aa = table_getSortValue(a.cells[SORT_COLUMN_INDEX]),
+        bb = table_getSortValue(b.cells[SORT_COLUMN_INDEX]);
+    if (aa == bb) {
+        return 0;
+    } else if (aa < bb) {
+        return -1;
+    }
+    return 1;
+}
+
+/* We do everything onload so that the entire document is present
+ * before we start searching it for tables. */
+document.observe('dom:loaded', function() {
+    $$('table.striped').each(table_stripe);
+    $$('table.sortable').each(table_makeSortable);
+});
diff --git a/js/stripe.js b/js/stripe.js
deleted file mode 100644
index 89ebe08..0000000
--- a/js/stripe.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Javascript code for finding all tables with classname "striped" and
- * dynamically striping their row colors.
- *
- * $Horde: gollem/js/stripe.js,v 1.1.2.1 2006/01/17 23:30:32 jan Exp $
- *
- * See the enclosed file COPYING for license information (GPL). If you did not
- * receive this file, see http://www.fsf.org/copyleft/gpl.html.
- */
-
-/* We do everything onload so that the entire document is present
- * before we start searching it for tables. */
-if (window.addEventListener) {
-    window.addEventListener('load', findStripedTables, false);
-} else if (window.attachEvent) {
-    window.attachEvent('onload', findStripedTables);
-} else if (window.onload != null) {
-    var oldOnLoad = window.onload;
-    window.onload = function(e)
-    {
-        oldOnLoad(e);
-        findStripedTables();
-    };
-} else {
-    window.onload = findStripedTables;
-}
-
-function findStripedTables()
-{
-    if (!document.getElementsByTagName) {
-        return;
-    }
-    tables = document.getElementsByTagName('table');
-    for (i = 0; i < tables.length; i++) {
-        if (tables[i].className.indexOf('striped') != -1) {
-            stripe(tables[i]);
-        }
-    }
-}
-
-function stripe(table)
-{
-    // The flag we'll use to keep track of whether the current row is
-    // odd or even.
-    var even = false;
-
-    // Tables can have more than one tbody element; get all child
-    // tbody tags and interate through them.
-    var tbodies = table.childNodes;
-    for (var c = 0; c < tbodies.length; c++) {
-        if (tbodies[c].tagName == 'TBODY') {
-            var trs = tbodies[c].childNodes;
-            for (var i = 0; i < trs.length; i++) {
-                if (trs[i].tagName == 'TR') {
-                    trs[i].className = trs[i].className.replace(/ ?rowEven ?/, '').replace(/ ?rowOdd ?/, '');
-                    if (trs[i].className) {
-                        trs[i].className += ' ';
-                    }
-                    trs[i].className += even ? 'rowEven' : 'rowOdd';
-
-                    // Flip from odd to even, or vice-versa.
-                    even = !even;
-                }
-            }
-        }
-    }
-}
diff --git a/js/tables.js b/js/tables.js
new file mode 100644
index 0000000..41833af
--- /dev/null
+++ b/js/tables.js
@@ -0,0 +1 @@
+var SORT_COLUMN_INDEX;function table_stripe(B){var A=["rowEven","rowOdd"];B.select("tbody tr").each(function(C){C.removeClassName(A[1]);C.addClassName(A[0]);A.reverse(true)})}function table_makeSortable(B){var A=0;B.down("tr").childElements().each(function(C){if(C.hasClassName("nosort")){++A;return}C.setAttribute("columnIndex",A++);C.setStyle({cursor:"pointer"});C.observe("click",function(E){var F=E.findElement("th");E.stop();var D=F.down("a");if(D&&!D.hasClassName("sortlink")){return true}table_resortTable(F);return false})})}function table_getSortValue(A){var B="";if(Object.isString(A)||Object.isUndefined(A)){return A}A=$(A);if(A.readAttribute("sortval")){return A.readAttribute("sortval")}if(A.innerText){return A.innerText}$A(A.childNodes).each(function(C){switch(C.nodeType){case 1:B+=table_getSortValue(C);break;case 3:B+=C.nodeValue;break}});return B}function table_resortTable(D){var C=D.up("table"),E=D.up().childElements(),B,A=0;E.each(function(G){if(D==G){if(G.hasClassName("sortup")){G.removeClassName("sortup");G.addClassName("sortdown")}else{if(G.hasClassName("sortdown")){G.removeClassName("sortdown");G.addClassName("sortup");A=1}else{G.addClassName("sortdown")}}}else{G.removeClassName("sortup");G.removeClassName("sortdown")}});if(E.size()<=1){return}SORT_COLUMN_INDEX=D.readAttribute("columnIndex");var F=table_getSortValue(C.down("tbody > tr").cells[SORT_COLUMN_INDEX]);if(F.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)||F.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)){B=table_sort_date}else{if(F.match(/^[?$]/)){B=table_sort_currency}else{if(F.match(/^[\d\.]+$/)){B=table_sort_numeric}else{B=table_sort_caseinsensitive}}}$A(C.getElementsByTagName("tbody")).each(function(H){var G=[],I=$A(H.getElementsByTagName("tr"));I.sort(B);if(A){I.reverse()}I.each(function(J){if(J.hasClassName("sortbottom")){G.push(J)}else{H.appendChild(J)}});G.each(function(J){H.appendChild(J)})});if(C.hasClassName("striped")){table_stripe(C)}if(typeof table_sortCallback!="undefined"&&Object.isFunction(table_sortCallback)){table_sortCallback(C.id,D.id,A)}}function table_sort_date(C,A){var F=table_getSortValue(C.cells[SORT_COLUMN_INDEX]),G=table_getSortValue(A.cells[SORT_COLUMN_INDEX]),D,B,E;if(F.length==10){D=F.substr(6,4)+F.substr(3,2)+F.substr(0,2)}else{E=F.substr(6,2);if(parseInt(E)<50){E="20"+E}else{E="19"+E}D=E+F.substr(3,2)+F.substr(0,2)}if(G.length==10){B=G.substr(6,4)+G.substr(3,2)+G.substr(0,2)}else{E=G.substr(6,2);if(parseInt(E)<50){E="20"+E}else{E="19"+E}B=E+G.substr(3,2)+G.substr(0,2)}if(D==B){return 0}else{if(D<B){return-1}}return 1}function table_sort_currency(B,A){var C=table_getSortValue(B.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,""),D=table_getSortValue(A.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,"");return parseFloat(C)-parseFloat(D)}function table_sort_numeric(B,A){var C=parseFloat(table_getSortValue(B.cells[SORT_COLUMN_INDEX]));if(isNaN(C)){C=0}var D=parseFloat(table_getSortValue(A.cells[SORT_COLUMN_INDEX]));if(isNaN(D)){D=0}return C-D}function table_sort_caseinsensitive(B,A){var C=table_getSortValue(B.cells[SORT_COLUMN_INDEX]).toLowerCase(),D=table_getSortValue(A.cells[SORT_COLUMN_INDEX]).toLowerCase();if(C==D){return 0}else{if(C<D){return-1}}return 1}function table_sort_default(B,A){var C=table_getSortValue(B.cells[SORT_COLUMN_INDEX]),D=table_getSortValue(A.cells[SORT_COLUMN_INDEX]);if(C==D){return 0}else{if(C<D){return-1}}return 1}document.observe("dom:loaded",function(){$$("table.striped").each(table_stripe);$$("table.sortable").each(table_makeSortable)});
\ No newline at end of file
diff --git a/lib/Auth/gollem.php b/lib/Auth/gollem.php
index 7dc89c1..f13de72 100644
--- a/lib/Auth/gollem.php
+++ b/lib/Auth/gollem.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * The Auth_gollem:: class provides an Gollem implementation of the Horde
+ * The Auth_gollem:: class provides a Gollem implementation of the Horde
  * authentication system.
  *
  * Required parameters:<pre>
@@ -9,9 +9,9 @@
  * Optional parameters:<pre>
  *   None.</pre>
  *
- * $Horde: gollem/lib/Auth/gollem.php,v 1.18.2.5 2007/01/02 13:54:53 jan Exp $
+ * $Horde: gollem/lib/Auth/gollem.php,v 1.18.2.7 2009/01/06 15:23:54 jan Exp $
  *
- * Copyright 2004-2007 Michael Slusarz <slusarz at curecanti.org>
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
  *
  * See the enclosed file COPYING for license information (LGPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
@@ -95,7 +95,12 @@ class Auth_gollem extends Auth {
             return false;
         }
 
+        // Allocate a global VFS object
         $GLOBALS['gollem_vfs'] = &Gollem::getVFSOb($_SESSION['gollem']['backend_key']);
+        if (is_a($GLOBALS['gollem_vfs'], 'PEAR_Error')) {
+            Horde::fatal($GLOBALS['gollem_vfs']);
+        }
+
         $valid = $GLOBALS['gollem_vfs']->checkCredentials();
         if (is_a($valid, 'PEAR_Error')) {
             $msg = $valid->getMessage();
diff --git a/lib/Block/tree_menu.php b/lib/Block/tree_menu.php
index 893888e..c275905 100644
--- a/lib/Block/tree_menu.php
+++ b/lib/Block/tree_menu.php
@@ -6,9 +6,9 @@ $block_type = 'tree';
 /**
  * Gollem tree block.
  *
- * $Horde: gollem/lib/Block/tree_menu.php,v 1.5.2.2 2007/01/02 13:54:53 jan Exp $
+ * $Horde: gollem/lib/Block/tree_menu.php,v 1.5.2.4 2009/01/06 15:23:54 jan Exp $
  *
- * Copyright 2005-2007 Michael Slusarz <slusarz at horde.org>
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
  *
  * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
@@ -26,8 +26,8 @@ class Horde_Block_gollem_tree_menu extends Horde_Block {
             $old_auth = $GLOBALS['authentication'];
         }
         $GLOBALS['authentication'] = 'none';
-        @define('GOLLEM_BASE', dirname(__FILE__) . '/../..');
-        require_once GOLLEM_BASE . '/lib/base.php';
+
+        require_once dirname(__FILE__) . '/../base.php';
         if (isset($old_auth)) {
             $GLOBALS['authentication'] = $old_auth;
         }
@@ -36,14 +36,16 @@ class Horde_Block_gollem_tree_menu extends Horde_Block {
         $login_url = Horde::applicationUrl('login.php');
 
         foreach ($GLOBALS['gollem_backends'] as $key => $val) {
-            $tree->addNode($parent . $key,
-                           $parent,
-                           $val['name'],
-                           $indent + 1,
-                           false,
-                           array('icon' => 'gollem.png',
-                                 'icondir' => $icondir,
-                                 'url' => Util::addParameter($login_url, array('backend_key' => $key, 'change_backend' => 1))));
+            if (Gollem::checkPermissions('backend', PERMS_SHOW, $key)) {
+                $tree->addNode($parent . $key,
+                               $parent,
+                               $val['name'],
+                               $indent + 1,
+                               false,
+                               array('icon' => 'gollem.png',
+                                     'icondir' => $icondir,
+                                     'url' => Util::addParameter($login_url, array('backend_key' => $key, 'change_backend' => 1))));
+            }
         }
     }
 
diff --git a/lib/Gollem.php b/lib/Gollem.php
index f95af39..f16737e 100644
--- a/lib/Gollem.php
+++ b/lib/Gollem.php
@@ -1,43 +1,36 @@
 <?php
-
-// Sort types
-
 /**
- * Sort by file type.
+ * $Horde: gollem/lib/Gollem.php,v 1.172.2.28 2009/03/18 22:18:22 jan Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @package Gollem
  */
+
+/** Sort by file type. */
 define('GOLLEM_SORT_TYPE', 0);
-/**
- * Sort by file name.
- */
+
+/** Sort by file name. */
 define('GOLLEM_SORT_NAME', 1);
-/**
- * Sort by file date.
- */
+
+/** Sort by file date. */
 define('GOLLEM_SORT_DATE', 2);
-/**
- * Sort by file size.
- */
-define('GOLLEM_SORT_SIZE', 3);
 
-// Sort direction
+/** Sort by file size. */
+define('GOLLEM_SORT_SIZE', 3);
 
-/**
- * Sort ascending.
- */
+/** Sort ascending. */
 define('GOLLEM_SORT_ASCEND', 0);
-/**
- * Sort descending.
- */
+
+/** Sort descending. */
 define('GOLLEM_SORT_DESCEND', 1);
 
 /**
  * Gollem Base Class.
  *
- * $Horde: gollem/lib/Gollem.php,v 1.172.2.20 2006/05/19 17:14:06 slusarz Exp $
- *
- * See the enclosed file COPYING for license information (GPL).  If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- *
  * @author  Max Kalika <max at horde.org>
  * @author  Chuck Hagenbuch <chuck at horde.org>
  * @package Gollem
@@ -90,7 +83,7 @@ class Gollem {
     function canAutoLogin($key = null, $force = false)
     {
         $auto_server = Gollem::getPreferredBackend();
-        if (is_null($key)) {
+        if ($key === null) {
             $key = $auto_server;
         }
 
@@ -131,7 +124,7 @@ class Gollem {
     function changeDir()
     {
         $dir = Util::getFormData('dir');
-        if (!is_null($dir)) {
+        if ($dir !== null) {
             if (strpos($dir, '/') !== 0) {
                 $dir = $GLOBALS['gollem_be']['dir'] . '/' . $dir;
             }
@@ -299,31 +292,79 @@ class Gollem {
      */
     function listFolder($dir)
     {
+        global $conf;
+
+        if (!empty($conf['foldercache']['use_cache']) &&
+            !empty($conf['cache']['driver']) &&
+            ($conf['cache']['driver'] != 'none')) {
+            require_once 'Horde/Cache.php';
+            require_once 'Horde/Serialize.php';
+            $key = Gollem::_getCacheID($dir);
+
+            $cache = &Horde_Cache::singleton($conf['cache']['driver'], Horde::getDriverConfig('cache', $conf['cache']['driver']));
+            $res = $cache->get($key, $conf['foldercache']['lifetime']);
+            if ($res !== false) {
+                $res = Horde_Serialize::unserialize($res, SERIALIZE_BASIC);
+                if (is_array($res)) {
+                    return $res;
+                }
+            }
+        }
+
         $files = $GLOBALS['gollem_vfs']->listFolder($dir, isset($GLOBALS['gollem_be']['filter']) ? $GLOBALS['gollem_be']['filter'] : null, $GLOBALS['prefs']->getValue('show_dotfiles'));
         if (!is_a($files, 'PEAR_Error')) {
-            switch ($GLOBALS['prefs']->getValue('sortby')) {
-            case GOLLEM_SORT_TYPE:
-                usort($files, array('Gollem', '_sortType'));
-                break;
-
-            case GOLLEM_SORT_NAME:
-                usort($files, array('Gollem', '_sortName'));
-                break;
-
-            case GOLLEM_SORT_DATE:
-                usort($files, array('Gollem', '_sortDate'));
-                break;
-
-            case GOLLEM_SORT_SIZE:
-                usort($files, array('Gollem', '_sortSize'));
-                break;
-            }
+            $sortcols = array(
+                GOLLEM_SORT_TYPE => '_sortType',
+                GOLLEM_SORT_NAME => '_sortName',
+                GOLLEM_SORT_DATE => '_sortDate',
+                GOLLEM_SORT_SIZE => '_sortSize',
+            );
+            usort($files, array('Gollem', $sortcols[$GLOBALS['prefs']->getValue('sortby')]));
+        }
+
+        if (isset($cache)) {
+            require_once 'Horde/Serialize.php';
+            $cache->set($key, Horde_Serialize::serialize($files, SERIALIZE_BASIC), $conf['foldercache']['lifetime']);
         }
 
         return $files;
     }
 
     /**
+     * Generate the Cache ID for a directory.
+     *
+     * @access private
+     * @since Gollem 1.1
+     *
+     * @param string $dir  The directory name.
+     */
+    function _getCacheID($dir)
+    {
+        global $prefs;
+        return implode('|', array(Auth::getAuth(), $_SESSION['gollem']['backend_key'], $prefs->getValue('show_dotfiles'), $prefs->getValue('sortdirsfirst'), $prefs->getValue('sortby'), $prefs->getValue('sortdir'), $dir));
+    }
+
+    /**
+     * Expire a folder cache entry.
+     *
+     * @since Gollem 1.1
+     *
+     * @param string $dir  The directory name.
+     */
+    function expireCache($dir)
+    {
+        global $conf;
+
+        if (!empty($conf['foldercache']['use_cache']) &&
+            !empty($conf['cache']['driver']) &&
+            ($conf['cache']['driver'] != 'none')) {
+            require_once 'Horde/Cache.php';
+            $cache = &Horde_Cache::singleton($conf['cache']['driver'], Horde::getDriverConfig('cache', $conf['cache']['driver']));
+            $cache->expire(Gollem::_getCacheID($dir));
+        }
+    }
+
+    /**
      * Generate correct subdirectory links.
      *
      * @param string $base  The base directory.
@@ -592,6 +633,9 @@ class Gollem {
 
         /* Read the source data. */
         $data = $from_be->read($dir, $name);
+        if (is_a($data, 'PEAR_Error')) {
+            return $data;
+        }
 
         /* Write the target data. */
         $res = $to_be->writeData($newdir, $name, $data);
@@ -700,18 +744,15 @@ class Gollem {
                               $backend = null)
     {
         $userID = Auth::getAuth();
-        if (is_null($backend)) {
+        if ($backend === null) {
             $backend = $_SESSION['gollem']['backend_key'];
         }
 
         switch ($filter) {
         case 'backend':
             $backendTag = 'gollem:backends:' . $backend;
-            if (!$GLOBALS['perms']->exists($backendTag) ||
-                $GLOBALS['perms']->hasPermission($backendTag, $userID, $permission)) {
-                return true;
-            }
-            break;
+            return (!$GLOBALS['perms']->exists($backendTag) ||
+                    $GLOBALS['perms']->hasPermission($backendTag, $userID, $permission));
         }
 
         return false;
@@ -729,14 +770,15 @@ class Gollem {
     {
         $label = array();
         $root_dir = Gollem::getRoot();
+        $root_dir_name = $_SESSION['gollem']['backends'][$_SESSION['gollem']['backend_key']]['name'];
 
         if ($currdir == $root_dir) {
-            $label[] = '[' . _("Root") . ']';
+            $label[] = '[' . $root_dir_name . ']';
         } else {
             $parts = explode('/', $currdir);
             $parts_count = count($parts);
 
-            $label[] = Horde::link(Util::addParameter($url, 'dir', $root_dir), sprintf(_("Up to %s"), _("Root"))) . '[' . _("Root") . ']</a>';
+            $label[] = Horde::link(Util::addParameter($url, 'dir', $root_dir), sprintf(_("Up to %s"), $root_dir_name)) . '[' . $root_dir_name . ']</a>';
 
             for ($i = 1; $i <= $parts_count; $i++) {
                 $part = array_slice($parts, 0, $i);
@@ -766,29 +808,20 @@ class Gollem {
     {
         require_once 'Horde/Menu.php';
 
-        $menu = &new Menu();
-        $menu->add(Util::addParameter(Horde::applicationUrl('manager.php'), 'dir', Gollem::getHome()), _("Home"), 'home.png');
-
-        if (strpos($_SERVER['PHP_SELF'], 'manager.php') !== false) {
-            if (Gollem::checkPermissions('backend', PERMS_EDIT)) {
-                $menu->add('#', _("Create Folder"), 'mkdir.png', null, '', 'createFolder(); return false;');
-            }
-            if (Gollem::checkPermissions('backend', PERMS_READ)) {
-                $menu->add('#', _("Change Folder"), 'cd.png', null, '', 'changeDirectory(); return false;');
-            }
-        }
+        $menu = new Menu();
+        $menu->add(Util::addParameter(Horde::applicationUrl('manager.php'), 'dir', Gollem::getHome()), _("_My Home"), 'folder_home.png');
 
         if (!empty($_SESSION['gollem'])) {
             $backend_key = $_SESSION['gollem']['backend_key'];
             if (Auth::isAdmin()) {
-                $menu->add(Util::addParameter(Horde::applicationUrl('permissions.php'), 'backend', $backend_key), _("Permissions"), 'administration.png', $GLOBALS['registry']->getImageDir('horde'));
+                $menu->add(Util::addParameter(Horde::applicationUrl('permissions.php'), 'backend', $backend_key), _("_Permissions"), 'perms.png', $GLOBALS['registry']->getImageDir('horde'));
             }
 
             if ($_SESSION['gollem']['hasquota'] &&
                 ($_SESSION['gollem']['backends'][$backend_key]['quota_val'] != -1)) {
                 if ($GLOBALS['browser']->hasFeature('javascript')) {
-                    Horde::addScriptFile('open_quota_win.js', 'gollem');
-                    $quota_url = 'javascript:open_quota_win(\'' . addslashes('backend=' . $backend_key) . '\');';
+                    Horde::addScriptFile('popup.js');
+                    $quota_url = "javascript:popup_gollem('" . Horde::applicationUrl('quota.php') . "',300,300,'" . addslashes('backend=' . $backend_key) . "');";
                 } else {
                     $quota_url = Util::addParameter(Horde::applicationUrl('quota.php'), 'backend', $backend_key);
                 }
@@ -810,8 +843,7 @@ class Gollem {
      */
     function menu()
     {
-        require_once 'Horde/Template.php';
-        $t = &new Horde_Template();
+        $t = new Gollem_Template();
 
         $t->set('forminput', Util::formInput());
         $t->set('be_select', Gollem::backendSelect(), true);
@@ -907,7 +939,7 @@ class Gollem {
     function realPath($path)
     {
         /* Standardize on UNIX directory separators. */
-        if (substr(PHP_OS, 0, 3) == 'WIN') {
+        if (!strncasecmp(PHP_OS, 'WIN', 3)) {
             $path = str_replace('\\', '/', $path);
         }
 
@@ -943,14 +975,35 @@ class Gollem {
      *
      * @return object  The Horde_VFS object requested.
      */
-    function &getVFSOb($backend_key)
+    function &getVFSOb($backend_key, $params = array())
     {
-        $be_config = &$_SESSION['gollem']['backends'][$backend_key];
-        $params = $be_config['params'];
-        if (!empty($params['password'])) {
-            $params['password'] = Secret::read(Secret::getKey('gollem'), $params['password']);
+        if (isset($_SESSION['gollem']['backends'][$backend_key])) {
+            $be_config = &$_SESSION['gollem']['backends'][$backend_key];
+        } else {
+            $be_config = $GLOBALS['gollem_backends'][$backend_key];
         }
+        if (!count($params)) {
+            $params = $be_config['params'];
+            if (!empty($params['password'])) {
+                $params['password'] = Secret::read(Secret::getKey('gollem'), $params['password']);
+            }
+        }
+
+        // Create VFS object
         $ob = &VFS::singleton($be_config['driver'], $params);
+        if (is_a($ob, 'PEAR_Error')) {
+            return $ob;
+        }
+
+        // Enable logging within VFS
+        $logger = &Horde::getLogger();
+        if ($logger !== false) {
+            // TODO: No need to do this check as of Horde 3.2
+            if (!is_a($logger, 'Log')) {
+                Horde::fatal(PEAR::raiseError('An error has occurred. Furthermore, Horde encountered an error attempting to log this error. Please check your Horde logging configuration in horde/config/conf.php.'), __FILE__, __LINE__, false);
+            }
+            $ob->setLogger($logger, $GLOBALS['conf']['log']['priority']);
+        }
 
         if (!isset($be_config['quota_val'])) {
             /* Only set quota information when we are finished with session
@@ -985,7 +1038,7 @@ class Gollem {
             }
         } elseif ($be_config['quota_val'] > -1) {
             $ob->setQuota($be_config['quota_val'], $be_config['quota_metric']);
-            $ob->setQuotaRoot(Gollem::getRoot());
+            $ob->setQuotaRoot($be_config['root']);
         }
 
         return $ob;
@@ -1041,4 +1094,126 @@ class Gollem {
         return $backends;
     }
 
+    /**
+     * Add inline javascript to the output buffer.
+     *
+     * @since Gollem 1.1
+     *
+     * @param string $script  The script text to add.
+     *
+     * @return string  The javascript text to output, or empty if the page
+     *                 headers have not yet been sent.
+     */
+    function addInlineScript($script)
+    {
+        $script = trim($script);
+        if (empty($script)) {
+            return;
+        }
+
+        if (!isset($GLOBALS['__gollem_inline_script'])) {
+            $GLOBALS['__gollem_inline_script'] = array();
+        }
+        $GLOBALS['__gollem_inline_script'][] = $script;
+
+        // If headers have already been sent, we need to output a
+        // <script> tag directly.
+        if (ob_get_length() || headers_sent()) {
+            Gollem::outputInlineScript();
+        }
+    }
+
+    /**
+     * Print inline javascript to the output buffer.
+     *
+     * @since Gollem 1.1
+     *
+     * @return string  The javascript text to output.
+     */
+    function outputInlineScript()
+    {
+        if (!empty($GLOBALS['__gollem_inline_script'])) {
+            echo '<script type="text/javascript">//<![CDATA[' . "\n";
+            foreach ($GLOBALS['__gollem_inline_script'] as $val) {
+                echo $val . "\n";
+            }
+            echo "//]]></script>\n";
+        }
+
+        $GLOBALS['__gollem_inline_script'] = array();
+    }
+
+    /**
+     * Cleans a path presented to Gollem's browse API call.
+     *
+     * This will remove:
+     * - leading '/'
+     * - leading 'gollem'
+     * - trailing '/'
+     * The desired end result is the path including VFS backend.
+     *
+     * @param string $path  Path as presented to Gollem API.
+     *
+     * @return string  Cleaned path as described above.
+     */
+    function stripAPIPath($path)
+    {
+        // Strip leading '/'
+        if (substr($path, 0, 1) == '/') {
+            $path = substr($path, 1);
+        }
+        // Remove 'gollem' from path
+        if (substr($path, 0, 6) == 'gollem') {
+            $path = substr($path, 6);
+        }
+        // Remove leading '/'
+        if (substr($path, 0, 1) == '/') {
+            $path = substr($path, 1);
+        }
+        // Remove trailing '/'
+        if (substr($path, -1) == '/') {
+            $path = substr($path, 0, -1);
+        }
+        return $path;
+    }
+
+    /**
+     * Convert a Gollem path into a URL encoded string, but keep '/'.
+     * This allows for proper PATH_INFO path parsing.
+     * Special care is taken to handle "+" and " ".
+     *
+     * @since Gollem 1.1
+     *
+     * @param string $path  Path to be urlencode()d.
+     *
+     * @return string  URL-encoded string with '/' preserved.
+     */
+    function pathEncode($path)
+    {
+         // TODO: Must use str_replace here instead of str_ireplace which is
+         // PHP5-only.
+         return str_replace(array('%2F', '%2f'), '/', rawurlencode($path));
+     }
+
+     /**
+      * Take a fully qualified and break off the file or directory name.
+      * This pair is used for the input to many VFS library functions.
+      *
+      * @param string $fullpath   Path to be split.
+      *
+      * @return array  Array of ($path, $name)
+      */
+     function getVFSPath($fullpath)
+     {
+        // Convert the path into VFS's ($path, $name) convention
+        $i = strrpos($fullpath, '/');
+        if ($i !== false) {
+            $path = substr($fullpath, 0, $i);
+            $name = substr($fullpath, $i + 1);
+        } else {
+            $name = $fullpath;
+            $path = '';
+        }
+        return array($name, $path);
+     }
 }
diff --git a/lib/JSON.php b/lib/JSON.php
new file mode 100644
index 0000000..36c8e8d
--- /dev/null
+++ b/lib/JSON.php
@@ -0,0 +1,481 @@
+<?php
+/**
+ * Converts to and from JSON format.
+ *
+ * JSON (JavaScript Object Notation) is a lightweight data-interchange
+ * format. It is easy for humans to read and write. It is easy for
+ * machines to parse and generate. It is based on a subset of the
+ * JavaScript Programming Language, Standard ECMA-262 3rd Edition -
+ * December 1999.  This feature can also be found in Python. JSON is a
+ * text format that is completely language independent but uses
+ * conventions that are familiar to programmers of the C-family of
+ * languages. These properties make JSON an ideal data-interchange
+ * language.
+ *
+ * This package provides a simple encoder and decoder for JSON notation. It
+ * is intended for use with client-side Javascript applications that make
+ * use of HTTPRequest to perform server communication functions - data can
+ * be encoded into JSON notation for use in a client-side javascript, or
+ * decoded from incoming Javascript requests. JSON format is native to
+ * Javascript, and can be directly eval()'ed with no further parsing
+ * overhead
+ *
+ * All strings should be in ASCII or UTF-8 format.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met: Redistributions of source code must retain the
+ * above copyright notice, this list of conditions and the following
+ * disclaimer. Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * For information the UTF-8 encoding operations please see
+ * http://www.cl.cam.ac.uk/~mgk25/unicode.html
+ *
+ * @package Horde_Serialize
+ * @author Michal Migurski <mike-json at teczno.com>
+ * @author Matt Knapp <mdknapp[at]gmail[dot]com>
+ * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
+ * @copyright 2005 Michal Migurski
+ * @license http://www.opensource.org/licenses/bsd-license.php
+ * @todo Remove for Horde 4.0
+ */
+
+/**
+ * Marker constant for JSON::decode(), used to flag stack state.
+ */
+define('GOLLEM_SERIALIZE_JSON_SLICE', 1);
+define('GOLLEM_SERIALIZE_JSON_IN_STR', 2);
+define('GOLLEM_SERIALIZE_JSON_IN_ARR', 4);
+define('GOLLEM_SERIALIZE_JSON_IN_OBJ', 8);
+define('GOLLEM_SERIALIZE_JSON_IN_CMT', 16);
+
+/**
+ * Converts to and from JSON format.
+ *
+ * @package Horde_Serialize
+ * @author Michal Migurski <mike-json at teczno.com>
+ * @author Matt Knapp <mdknapp[at]gmail[dot]com>
+ * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
+ * @copyright 2005 Michal Migurski
+ */
+class Gollem_Serialize_JSON {
+
+    /**
+     * Added to Gollem's json.php to use json_encode() if available.
+     */
+    function encode($var)
+    {
+        if (Util::extensionExists('json')) {
+            return json_encode($var);
+        } else {
+            require_once 'Horde/String.php';
+            return Gollem_Serialize_JSON::_encode($var);
+        }
+    }
+
+    /**
+     * Encodes an arbitrary variable into JSON format.
+     *
+     * @param mixed $var  Any number, boolean, string, array, or object to be
+     *                    encoded.
+     *
+     * @return string  JSON string representation of input var.
+     */
+    function _encode($var)
+    {
+        switch (gettype($var)) {
+        case 'boolean':
+            return $var ? 'true' : 'false';
+
+        case 'NULL':
+        case 'null':
+            return 'null';
+
+        case 'integer':
+            return (int) $var;
+
+        case 'double':
+        case 'float':
+            return (float) $var;
+
+        case 'string':
+            // Expected to be in ASCII or UTF-8.
+            $strlen = String::length($var, 'utf-8');
+            $needles = array('\\', "\x08", "\t", "\n", "\x0c", "\r", '"', '/');
+            $replacements = array('\\\\', '\b', '\t', '\n', '\f', '\r', '\"', '\/');
+
+            // Short circuit for ASCII strings.
+            if ($strlen == strlen($var)) {
+                $var = str_replace($needles, $replacements, $var);
+                return '"' . $var . '"';
+            }
+
+            // Iterate over every character in the string, escaping with a
+            // slash or encoding to UTF-8 code points where necessary.
+            $ascii = '';
+            preg_match_all("/(.{1})/su", $var, $m);
+            foreach ($m[1] as $char) {
+                if (strlen($char) == 1) {
+                    $ascii .= str_replace($needles, $replacements, $char);
+                    continue;
+                }
+                $ascii .= '\u' . bin2hex(String::convertCharset($char, 'utf-8', 'utf-16be'));
+            }
+
+            return '"' . $ascii . '"';
+
+        case 'array':
+            /* As per JSON spec if any array key is not an integer we
+             * must treat the the whole array as an object. We also
+             * try to catch a sparsely populated associative array
+             * with numeric keys here because some JS engines will
+             * create an array with empty indexes up to max_index
+             * which can cause memory issues and because the keys,
+             * which may be relevant, will be remapped otherwise.
+             *
+             * As per the ECMA and JSON specification an object may
+             * have any string as a property. Unfortunately due to a
+             * hole in the ECMA specification if the key is a ECMA
+             * reserved word or starts with a digit the parameter is
+             * only accessible using ECMAScript's bracket notation. */
+
+            // Treat as a JSON object.
+            if (is_array($var) &&
+                count($var) &&
+                (array_keys($var) !== range(0, sizeof($var) - 1))) {
+                return '{' .
+                    implode(',', array_map(array('Gollem_Serialize_JSON', '_nameValue'),
+                                           array_keys($var),
+                                           array_values($var))) . '}';
+            }
+
+            // Treat it like a regular array.
+            return '[' . implode(',', array_map(array('Gollem_Serialize_JSON', '_encode'), $var)) . ']';
+
+        case 'object':
+            $vars = get_object_vars($var);
+            return '{' .
+                implode(',', array_map(array('gollem_Serialize_JSON', '_nameValue'),
+                                       array_keys($vars),
+                                       array_values($vars))) . '}';
+
+        default:
+            return '';
+        }
+    }
+
+    /**
+     * Added to Gollem's json.php to use json_decode() if available.
+     */
+    function decode($var)
+    {
+        if (Util::extensionExists('json')) {
+            return json_decode($var);
+        } else {
+            require_once 'Horde/String.php';
+            return Gollem_Serialize_JSON::_decode($var);
+        }
+    }
+
+    /**
+     * Decodes a JSON string into appropriate variable.
+     *
+     * @param string $str  JSON-formatted string.
+     *
+     * @return mixed  Number, boolean, string, array, or object corresponding
+     *                to given JSON input string. Note that decode() always
+     *                returns strings in ASCII or UTF-8 format.
+     */
+    function _decode($str)
+    {
+        $str = Gollem_Serialize_JSON::_reduce($str);
+
+        switch (strtolower($str)) {
+        case 'true':
+            return true;
+
+        case 'false':
+            return false;
+
+        case 'null':
+            return null;
+
+        default:
+            if (is_numeric($str)) {
+                // Return float or int, as appropriate.
+                return ((float)$str == (integer)$str)
+                    ? (integer)$str
+                    : (float)$str;
+
+            } elseif (preg_match('/^("|\').+(\1)$/s', $str, $m) && $m[1] == $m[2]) {
+                // Strings are returned in UTF-8.
+                $delim = substr($str, 0, 1);
+                $chrs = substr($str, 1, -1);
+                $utf8 = '';
+                $strlen_chrs = strlen($chrs);
+
+                for ($c = 0; $c < $strlen_chrs; ++$c) {
+                    $substr_chrs_c_2 = substr($chrs, $c, 2);
+                    $ord_chrs_c = ord($chrs{$c});
+
+                    switch (true) {
+                    case $substr_chrs_c_2 == '\b':
+                        $utf8 .= chr(0x08);
+                        ++$c;
+                        break;
+
+                    case $substr_chrs_c_2 == '\t':
+                        $utf8 .= chr(0x09);
+                        ++$c;
+                        break;
+
+                    case $substr_chrs_c_2 == '\n':
+                        $utf8 .= chr(0x0A);
+                        ++$c;
+                        break;
+
+                    case $substr_chrs_c_2 == '\f':
+                        $utf8 .= chr(0x0C);
+                        ++$c;
+                        break;
+
+                    case $substr_chrs_c_2 == '\r':
+                        $utf8 .= chr(0x0D);
+                        ++$c;
+                        break;
+
+                    case $substr_chrs_c_2 == '\\"':
+                    case $substr_chrs_c_2 == '\\\'':
+                    case $substr_chrs_c_2 == '\\\\':
+                    case $substr_chrs_c_2 == '\\/':
+                        if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
+                            ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
+                            $utf8 .= $chrs{++$c};
+                        }
+                        break;
+
+                    case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
+                        // Single, escaped unicode character.
+                        $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) . chr(hexdec(substr($chrs, ($c + 4), 2)));
+                        $utf8 .= String::convertCharset($utf16, 'utf-16', 'utf-8');
+                        $c += 5;
+                        break;
+
+                    case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
+                        $utf8 .= $chrs{$c};
+                        break;
+
+                    case ($ord_chrs_c & 0xE0) == 0xC0:
+                        // Characters U-00000080 - U-000007FF, mask 110XXXXX
+                        $utf8 .= substr($chrs, $c, 2);
+                        ++$c;
+                        break;
+
+                    case ($ord_chrs_c & 0xF0) == 0xE0:
+                        // Characters U-00000800 - U-0000FFFF, mask 1110XXXX
+                        $utf8 .= substr($chrs, $c, 3);
+                        $c += 2;
+                        break;
+
+                    case ($ord_chrs_c & 0xF8) == 0xF0:
+                        // Characters U-00010000 - U-001FFFFF, mask 11110XXX
+                        $utf8 .= substr($chrs, $c, 4);
+                        $c += 3;
+                        break;
+
+                    case ($ord_chrs_c & 0xFC) == 0xF8:
+                        // Characters U-00200000 - U-03FFFFFF, mask 111110XX
+                        $utf8 .= substr($chrs, $c, 5);
+                        $c += 4;
+                        break;
+
+                    case ($ord_chrs_c & 0xFE) == 0xFC:
+                        // Characters U-04000000 - U-7FFFFFFF, mask 1111110X
+                        $utf8 .= substr($chrs, $c, 6);
+                        $c += 5;
+                        break;
+                    }
+                }
+
+                return $utf8;
+            } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
+                // Array or object notation.
+                if ($str[0] == '[') {
+                    $stk = array(GOLLEM_SERIALIZE_JSON_IN_ARR);
+                    $arr = array();
+                } else {
+                    $stk = array(GOLLEM_SERIALIZE_JSON_IN_OBJ);
+                    $obj = new stdClass();
+                }
+
+                array_push($stk, array('what'  => GOLLEM_SERIALIZE_JSON_SLICE,
+                                       'where' => 0,
+                                       'delim' => false));
+
+                $chrs = substr($str, 1, -1);
+                $chrs = Gollem_Serialize_JSON::_reduce($chrs);
+
+                if ($chrs == '') {
+                    if (reset($stk) == GOLLEM_SERIALIZE_JSON_IN_ARR) {
+                        return $arr;
+                    } else {
+                        return $obj;
+                    }
+                }
+
+                $strlen_chrs = strlen($chrs);
+
+                for ($c = 0; $c <= $strlen_chrs; ++$c) {
+                    $top = end($stk);
+                    $substr_chrs_c_2 = substr($chrs, $c, 2);
+
+                    if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == GOLLEM_SERIALIZE_JSON_SLICE))) {
+                        // Found a comma that is not inside a string,
+                        // array, etc., OR we've reached the end of
+                        // the character list.
+                        $slice = substr($chrs, $top['where'], ($c - $top['where']));
+                        array_push($stk, array('what' => GOLLEM_SERIALIZE_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
+
+                        if (reset($stk) == GOLLEM_SERIALIZE_JSON_IN_ARR) {
+                            // We are in an array, so just push an
+                            // element onto the stack.
+                            array_push($arr, Gollem_Serialize_JSON::_decode($slice));
+
+                        } elseif (reset($stk) == GOLLEM_SERIALIZE_JSON_IN_OBJ) {
+                            // We are in an object, so figure out the
+                            // property name and set an element in an
+                            // associative array for now.
+                            if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+                                // "name":value pair
+                                $key = Gollem_Serialize_JSON::_decode($parts[1]);
+                                $val = Gollem_Serialize_JSON::_decode($parts[2]);
+
+                                $obj->$key = $val;
+                            } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+                                // name:value pair, where name is unquoted
+                                $key = $parts[1];
+                                $obj->$key = Gollem_Serialize_JSON::_decode($parts[2]);
+                            }
+                        }
+                    } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != GOLLEM_SERIALIZE_JSON_IN_STR)) {
+                        // Found a quote, and we are not inside a string.
+                        array_push($stk, array('what' => GOLLEM_SERIALIZE_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
+
+                    } elseif (($chrs{$c} == $top['delim']) &&
+                              ($top['what'] == GOLLEM_SERIALIZE_JSON_IN_STR) &&
+                              (($chrs{$c - 1} != "\\") ||
+                               ($chrs{$c - 1} == "\\" && $chrs{$c - 2} == "\\"))) {
+                        // Found a quote, we're in a string, and it's
+                        // not escaped.
+                        array_pop($stk);
+
+                    } elseif (($chrs{$c} == '[') &&
+                              in_array($top['what'], array(GOLLEM_SERIALIZE_JSON_SLICE, GOLLEM_SERIALIZE_JSON_IN_ARR, GOLLEM_SERIALIZE_JSON_IN_OBJ))) {
+                        // Found a left-bracket, and we are in an
+                        // array, object, or slice.
+                        array_push($stk, array('what' => GOLLEM_SERIALIZE_JSON_IN_ARR, 'where' => $c, 'delim' => false));
+
+                    } elseif (($chrs{$c} == ']') && ($top['what'] == GOLLEM_SERIALIZE_JSON_IN_ARR)) {
+                        // found a right-bracket, and we're in an array
+                        array_pop($stk);
+
+                    } elseif (($chrs{$c} == '{') &&
+                              in_array($top['what'], array(GOLLEM_SERIALIZE_JSON_SLICE, GOLLEM_SERIALIZE_JSON_IN_ARR, GOLLEM_SERIALIZE_JSON_IN_OBJ))) {
+                        // Found a left-brace, and we are in an array,
+                        // object, or slice.
+                        array_push($stk, array('what' => GOLLEM_SERIALIZE_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
+
+                    } elseif (($chrs{$c} == '}') && ($top['what'] == GOLLEM_SERIALIZE_JSON_IN_OBJ)) {
+                        // Found a right-brace, and we're in an object.
+                        array_pop($stk);
+
+                    } elseif (($substr_chrs_c_2 == '/*') &&
+                              in_array($top['what'], array(GOLLEM_SERIALIZE_JSON_SLICE, GOLLEM_SERIALIZE_JSON_IN_ARR, GOLLEM_SERIALIZE_JSON_IN_OBJ))) {
+                        // Found a comment start, and we are in an
+                        // array, object, or slice.
+                        array_push($stk, array('what' => GOLLEM_SERIALIZE_JSON_IN_CMT, 'where' => $c, 'delim' => false));
+                        $c++;
+
+                    } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == GOLLEM_SERIALIZE_JSON_IN_CMT)) {
+                        // Found a comment end, and we're in one now.
+                        array_pop($stk);
+                        $c++;
+
+                        for ($i = $top['where']; $i <= $c; ++$i) {
+                            $chrs = substr_replace($chrs, ' ', $i, 1);
+                        }
+                    }
+                }
+
+                if (reset($stk) == GOLLEM_SERIALIZE_JSON_IN_ARR) {
+                    return $arr;
+                } elseif (reset($stk) == GOLLEM_SERIALIZE_JSON_IN_OBJ) {
+                    return $obj;
+                }
+            }
+        }
+    }
+
+    /**
+     * Array-walking function for use in generating JSON-formatted
+     * name-value pairs.
+     *
+     * @access private
+     *
+     * @param string $name  Name of key to use.
+     * @param mixed $value  Reference to an array element to be encoded.
+     *
+     * @return string  JSON-formatted name-value pair, like '"name":value'.
+     */
+    function _nameValue($name, $value)
+    {
+        return Gollem_Serialize_JSON::_encode(strval($name)) . ':' . Gollem_Serialize_JSON::_encode($value);
+    }
+
+    /**
+     * Reduce a string by removing leading and trailing comments and
+     * whitespace.
+     *
+     * @access private
+     *
+     * @param $str string  String value to strip of comments and whitespace.
+     *
+     * @return string  String value stripped of comments and whitespace.
+     */
+    function _reduce($str)
+    {
+        $str = preg_replace(array(
+            // Eliminate single line comments in '// ...' form.
+            '#^\s*//(.+)$#m',
+
+            // Eliminate multi-line comments in '/* ... */' form, at
+            // start of string.
+            '#^\s*/\*(.+)\*/#Us',
+
+            // Eliminate multi-line comments in '/* ... */' form, at
+            // end of string.
+            '#/\*(.+)\*/\s*$#Us'
+            ), '', $str);
+
+        // Eliminate extraneous space.
+        return trim($str);
+    }
+
+}
diff --git a/lib/MIME/Viewer/images.php b/lib/MIME/Viewer/images.php
index e400fbf..7546c65 100644
--- a/lib/MIME/Viewer/images.php
+++ b/lib/MIME/Viewer/images.php
@@ -6,9 +6,9 @@ require_once 'Horde/MIME/Viewer/images.php';
  * The Gollem_MIME_Viewer_images class allows images to be displayed
  * inline in a message.
  *
- * $Horde: gollem/lib/MIME/Viewer/images.php,v 1.22.2.2 2007/01/02 13:54:53 jan Exp $
+ * $Horde: gollem/lib/MIME/Viewer/images.php,v 1.22.2.4 2009/01/06 15:23:55 jan Exp $
  *
- * Copyright 2002-2007 Mike Cochrane <mike at graftonhall.co.nz>
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
  *
  * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
diff --git a/lib/Session.php b/lib/Session.php
index e31ac4f..1e04248 100644
--- a/lib/Session.php
+++ b/lib/Session.php
@@ -2,11 +2,9 @@
 /**
  * Functions required to start a Gollem session.
  *
- * $Horde: gollem/lib/Session.php,v 1.34.2.5 2007/01/02 13:54:52 jan Exp $
+ * $Horde: gollem/lib/Session.php,v 1.34.2.8 2009/01/06 15:23:54 jan Exp $
  *
- * Copyright 1999-2007 Chuck Hagenbuch <chuck at horde.org>
- * Copyright 2000-2007 Max Kalika <max at horde.org>
- * Copyright 2004-2007 Michael Slusarz <slusarz at horde.org>
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
  *
  * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
@@ -92,7 +90,7 @@ class Gollem_Session {
         /* Set username now. Don't set the current username if the backend
          * already has a username defined. */
         if (empty($ptr['params']['username'])) {
-            $ptr['params']['username'] = (is_null($user)) ? Auth::getBareAuth() : $user;
+            $ptr['params']['username'] = ($user === null) ? Auth::getBareAuth() : $user;
         }
 
         /* Set password now. The password should always be encrypted within
@@ -100,7 +98,9 @@ class Gollem_Session {
         if (!empty($ptr['params']['password'])) {
             $pass = $ptr['params']['password'];
         }
-        if (!is_null($pass)) {
+        if ($pass === null) {
+            $ptr['params']['password'] = null;
+        } else {
             $ptr['params']['password'] = Secret::write(Secret::getKey('gollem'), $pass);
         }
 
diff --git a/lib/Template.php b/lib/Template.php
new file mode 100644
index 0000000..a652760
--- /dev/null
+++ b/lib/Template.php
@@ -0,0 +1,513 @@
+<?php
+/**
+ * Horde Template system. Adapted from bTemplate by Brian Lozier
+ * <brian at massassi.net>.
+ *
+ * Horde_Template provides a basic template engine with tags, loops,
+ * and if conditions. However, it is also a simple interface with
+ * several essential functions: set(), fetch(), and
+ * parse(). Subclasses or decorators can implement (or delegate) these
+ * three methods, plus the options api, and easily implement other
+ * template engines (PHP code, XSLT, etc.) without requiring usage
+ * changes.
+ *
+ * Compilation code adapted from code written by Bruno Pedro <bpedro at ptm.pt>.
+ *
+ * $Horde: gollem/lib/Template.php,v 1.8.2.2 2009/01/06 15:23:54 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck at horde.org>
+ * @author  Michael Slusarz <slusarz at horde.org>
+ * @package Horde_Template
+ */
+
+/** The identifier to use for memory-only templates. */
+define('GOLLEM_TEMPLATE_STRING', '**string');
+
+class Gollem_Template {
+
+    /**
+     * The Horde_Cache object to use.
+     *
+     * @var Horde_Cache
+     */
+    var $_cache;
+
+    /**
+     * Option values.
+     *
+     * @var array
+     */
+    var $_options = array();
+
+    /**
+     * Directory that templates should be read from.
+     *
+     * @var string
+     */
+    var $_basepath = '';
+
+    /**
+     * Tag (scalar) values.
+     *
+     * @var array
+     */
+    var $_scalars = array();
+
+    /**
+     * Loop tag values.
+     *
+     * @var array
+     */
+    var $_arrays = array();
+
+    /**
+     * Path to template source.
+     *
+     * @var string
+     */
+    var $_templateFile = null;
+
+    /**
+     * Template source.
+     *
+     * @var string
+     */
+    var $_template = null;
+
+    /**
+     * Foreach variable mappings.
+     *
+     * @var array
+     */
+    var $_foreachMap = array();
+
+    /**
+     * Foreach variable incrementor.
+     *
+     * @var integer
+     */
+    var $_foreachVar = 0;
+
+    /**
+     * preg_match() cache.
+     *
+     * @var array
+     */
+    var $_pregcache = array();
+
+    /**
+     * Constructor.
+     *
+     * @param string $basepath  The directory where templates are read from.
+     */
+    function Gollem_Template($basepath = null)
+    {
+        if (!is_null($basepath)) {
+            $this->_basepath = $basepath;
+        }
+
+        if (!empty($GLOBALS['conf']['cache']['driver'])) {
+            require_once 'Horde/Cache.php';
+            $this->_cache = &Horde_Cache::singleton($GLOBALS['conf']['cache']['driver'], Horde::getDriverConfig('cache', $GLOBALS['conf']['cache']['driver']));
+        }
+
+        // DEBUGGING
+        $this->setOption('forcecompile', true);
+    }
+
+    /**
+     * Sets an option.
+     * Currently available options are:
+     * <pre>
+     * 'debug' - Output debugging information to screen
+     * 'forcecompile' - Force a compilation on every page load
+     * 'gettext' - Activate gettext detection
+     * <pre>
+     *
+     * @param string $option  The option name.
+     * @param mixed $val      The option's value.
+     */
+    function setOption($option, $val)
+    {
+        $this->_options[$option] = $val;
+    }
+
+    /**
+     * Set the template contents to a string.
+     *
+     * @param string $template  The template text.
+     */
+    function setTemplate($template)
+    {
+        $this->_template = $template;
+        $this->_parse();
+        $this->_templateFile = GOLLEM_TEMPLATE_STRING;
+    }
+
+    /**
+     * Returns an option's value.
+     *
+     * @param string $option  The option name.
+     *
+     * @return mixed  The option's value.
+     */
+    function getOption($option)
+    {
+        return isset($this->_options[$option]) ? $this->_options[$option] : null;
+    }
+
+    /**
+     * Sets a tag, loop, or if variable.
+     *
+     * @param string|array $tag   Either the tag name or a hash with tag names
+     *                            as keys and tag values as values.
+     * @param mixed        $var   The value to replace the tag with.
+     */
+    function set($tag, $var)
+    {
+        if (is_array($tag)) {
+            foreach ($tag as $tTag => $tVar) {
+                $this->set($tTag, $tVar);
+            }
+        } elseif (is_array($var) || is_object($var)) {
+            $this->_arrays[$tag] = $var;
+        } else {
+            $this->_scalars[$tag] = $var;
+        }
+    }
+
+    /**
+     * Returns the value of a tag or loop.
+     *
+     * @param string $tag  The tag name.
+     *
+     * @return mixed  The tag value or null if the tag hasn't been set yet.
+     */
+    function get($tag)
+    {
+        if (isset($this->_arrays[$tag])) {
+            return $this->_arrays[$tag];
+        }
+        if (isset($this->_scalars[$tag])) {
+            return $this->_scalars[$tag];
+        }
+        return null;
+    }
+
+    /**
+     * Fetches a template from the specified file and return the parsed
+     * contents.
+     *
+     * @param string $filename  The file to fetch the template from.
+     *
+     * @return string  The parsed template.
+     */
+    function fetch($filename = null)
+    {
+        $file = $this->_basepath . $filename;
+        $force = $this->getOption('forcecompile');
+
+        if (!is_null($filename) && ($file != $this->_templateFile)) {
+            $this->_template = $this->_templateFile = null;
+        }
+
+        /* First, check for a cached compiled version. */
+        if (!$force && is_null($this->_template) && isset($this->_cache)) {
+            $cacheid = 'horde_template|' . filemtime($file) . '|' . $file;
+            $this->_template = $this->_cache->get($cacheid, 0);
+            if ($this->_template === false) {
+                $this->_template = null;
+            }
+        }
+
+        /* Parse and compile the template. */
+        if ($force || is_null($this->_template)) {
+            $this->_template = str_replace("\n", " \n", file_get_contents($file));
+            $this->_parse();
+            if (isset($cacheid) &&
+                !$this->_cache->set($cacheid, $this->_template)) {
+                Horde::logMessage(sprintf(_("Could not save the compiled template file '%s'."), $file), __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        }
+
+        $this->_templateFile = $file;
+
+        /* Template debugging. */
+        if ($this->getOption('debug')) {
+            echo '<pre>' . htmlspecialchars($this->_template) . '</pre>';
+        }
+
+        return $this->parse();
+    }
+
+    /**
+     * Parses all variables/tags in the template.
+     *
+     * @param string $contents  The unparsed template.
+     *
+     * @return string  The parsed template.
+     */
+    function parse($contents = null)
+    {
+        if (!is_null($contents)) {
+            $this->setTemplate(str_replace("\n", " \n", $contents));
+        }
+
+        /* Evaluate the compiled template and return the output. */
+        ob_start();
+        eval('?>' . $this->_template);
+        return str_replace(" \n", "\n", ob_get_clean());
+    }
+
+    /**
+     * Parses all variables/tags in the template.
+     */
+    function _parse()
+    {
+        // Escape XML instructions.
+        $this->_template = preg_replace('/\?>|<\?/',
+                                        '<?php echo \'$0\' ?>',
+                                        $this->_template);
+
+        // Parse gettext tags, if the option is enabled.
+        if ($this->getOption('gettext')) {
+            $this->_parseGettext();
+        }
+
+        // Process ifs.
+        $this->_parseIf();
+
+        // Process loops and arrays.
+        $this->_parseLoop();
+
+        // Process base scalar tags.  Needs to be after _parseLoop() as we
+        // rely on _foreachMap().
+        $this->_parseTags();
+
+        // Finally, process any associative array scalar tags.
+        $this->_parseAssociativeTags();
+    }
+
+    /**
+     * Parses gettext tags.
+     *
+     * @access private
+     */
+    function _parseGettext()
+    {
+        if (preg_match_all("/<gettext>(.+?)<\/gettext>/s", $this->_template, $matches, PREG_SET_ORDER)) {
+            $replace = array();
+            foreach ($matches as $val) {
+                $replace[$val[0]] = '<?php echo _(\'' . str_replace("'", "\\'", $val[1]) . '\'); ?>';
+            }
+            $this->_doReplace($replace);
+        }
+    }
+
+    /**
+     * Parses 'if' statements.
+     *
+     * @access private
+     *
+     * @param string $key  The key prefix to parse.
+     */
+    function _parseIf($key = null)
+    {
+        $replace = array();
+
+        foreach ($this->_doSearch('if', $key) as $val) {
+            $replace[$val[0]] = '<?php if (!empty(' . $this->_generatePHPVar('scalars', $val[1]) . ') || !empty(' . $this->_generatePHPVar('arrays', $val[1]) . ')): ?>';
+            $replace[$val[2]] = '<?php endif; ?>';
+
+            // Check for else statement.
+            foreach ($this->_doSearch('else', $key) as $val2) {
+                $replace[$val2[0]] = '<?php else: ?>';
+                $replace[$val2[2]] = '';
+            }
+        }
+
+        $this->_doReplace($replace);
+    }
+
+    /**
+     * Parses the given array for any loops or other uses of the array.
+     *
+     * @access private
+     *
+     * @param string $key  The key prefix to parse.
+     */
+    function _parseLoop($key = null)
+    {
+        $replace = array();
+
+        foreach ($this->_doSearch('loop', $key) as $val) {
+            $divider = null;
+
+            // See if we have a divider.
+            if (preg_match("/<divider:" . $val[1] . ">(.*)<\/divider:" . $val[1] . ">/sU", $this->_template, $m)) {
+                $divider = $m[1];
+                $replace[$m[0]] = '';
+            }
+
+            if (!isset($this->_foreachMap[$val[1]])) {
+                $this->_foreachMap[$val[1]] = ++$this->_foreachVar;
+            }
+            $varId = $this->_foreachMap[$val[1]];
+            $var = $this->_generatePHPVar('arrays', $val[1]);
+
+            $replace[$val[0]] = '<?php ' .
+                (($divider) ? '$i' . $varId . ' = count(' . $var . '); ' : '') .
+                'foreach (' . $this->_generatePHPVar('arrays', $val[1]) . ' as $k' . $varId . ' => $v' . $varId . '): ?>';
+            $replace[$val[2]] = '<?php ' .
+                (($divider) ? 'if (--$i' . $varId . ' != 0) { echo \'' . $divider . '\'; }; ' : '') .
+                'endforeach; ?>';
+
+            // Parse ifs.
+            $this->_parseIf($val[1]);
+
+            // Parse interior loops.
+            $this->_parseLoop($val[1]);
+
+            // Replace scalars.
+            $this->_parseTags($val[1]);
+        }
+
+        $this->_doReplace($replace);
+    }
+
+    /**
+     * Replaces 'tag' tags with their PHP equivalents.
+     *
+     * @access private
+     *
+     * @param string $key  The key prefix to parse.
+     */
+    function _parseTags($key = null)
+    {
+        $replace = array();
+
+        foreach ($this->_doSearch('tag', $key, true) as $val) {
+            $replace_text = '<?php ';
+            if (isset($this->_foreachMap[$val[1]])) {
+                $var = $this->_foreachMap[$val[1]];
+                $replace_text .= 'if (isset($v' . $var . ')) { echo is_array($v' . $var . ') ? $k' . $var . ' : $v' . $var . '; } else';
+            }
+            $var = $this->_generatePHPVar('scalars', $val[1]);
+            $replace[$val[0]] = $replace_text . 'if (isset(' . $var . ')) { echo ' . $var . '; } ?>';
+        }
+
+        $this->_doReplace($replace);
+    }
+
+    /**
+     * Parse associative tags (i.e. <tag:foo.bar />).
+     *
+     * @access private
+     */
+    function _parseAssociativeTags()
+    {
+        $replace = array();
+
+        foreach ($this->_pregcache['tag'] as $key => $val) {
+            $parts = explode('.', $val[1]);
+            $var = '$this->_arrays[\'' . $parts[0] . '\'][\'' . $parts[1] . '\']';
+            $replace[$val[0]] = '<?php if (isset(' . $var . ')) { echo ' . $var . '; } ?>';
+            unset($this->_pregcache['tag'][$key]);
+        }
+
+        $this->_doReplace($replace);
+    }
+
+    /**
+     * Output the correct PHP variable string for use in template space.
+     *
+     * @access private
+     */
+    function _generatePHPVar($tag, $key)
+    {
+        $out = '';
+
+        $a = explode('.', $key);
+        $a_count = count($a);
+
+        if ($a_count == 1) {
+            switch ($tag) {
+            case 'arrays':
+                $out = '$this->_arrays';
+                break;
+
+            case 'scalars':
+                $out = '$this->_scalars';
+                break;
+            }
+        } else {
+            $out = '$v' . $this->_foreachMap[implode('.', array_slice($a, 0, -1))];
+        }
+
+        return $out . '[\'' . end($a) . '\']';
+    }
+
+    /**
+     * TODO
+     *
+     * @access private
+     */
+    function _doSearch($tag, $key, $noclose = false)
+    {
+        $out = array();
+        $level = (is_null($key)) ? 0 : substr_count($key, '.') + 1;
+
+        if (!isset($this->_pregcache[$key])) {
+            $regex = ($noclose) ?
+                "/<" . $tag . ":(.+?)\s\/>/" :
+                "/<" . $tag . ":([^>]+)>/";
+            preg_match_all($regex, $this->_template, $this->_pregcache[$tag], PREG_SET_ORDER);
+        }
+
+        foreach ($this->_pregcache[$tag] as $pkey => $val) {
+            $val_level = substr_count($val[1], '.');
+            $add = false;
+            if (is_null($key)) {
+                $add = !$val_level;
+            } else {
+                $add = (($val_level == $level) &&
+                        (strpos($val[1], $key . '.') === 0));
+            }
+            if ($add) {
+                if (!$noclose) {
+                    $val[2] = '</' . $tag . ':' . $val[1] . '>';
+                }
+                $out[] = $val;
+                unset($this->_pregcache[$tag][$pkey]);
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * TODO
+     *
+     * @access private
+     */
+    function _doReplace($replace)
+    {
+        if (empty($replace)) {
+            return;
+        }
+
+        $search = array();
+
+        foreach (array_keys($replace) as $val) {
+            $search[] = '/' . preg_quote($val, '/') . '/';
+        }
+
+        $this->_template = preg_replace($search, array_values($replace), $this->_template);
+    }
+
+}
diff --git a/lib/api.php b/lib/api.php
index d09ab5b..11bbde3 100644
--- a/lib/api.php
+++ b/lib/api.php
@@ -5,13 +5,39 @@
  * This file defines Gollem's external API interface. Other
  * applications can interact with Gollem through this API.
  *
- * $Horde: gollem/lib/api.php,v 1.14.2.2 2005/12/19 07:51:15 slusarz Exp $
+ * $Horde: gollem/lib/api.php,v 1.14.2.5 2008/10/09 20:54:42 jan Exp $
  *
  * @author  Amith Varghese (amith at xalan.com)
  * @author  Michael Slusarz (slusarz at curecanti.org)
+ * @author  Ben Klang (bklang at alkaloid.net)
  * @package Gollem
  */
 
+$_services['browse'] = array(
+    'args' => array('path' => 'string'),
+    'type' => '{urn:horde}hashHash',
+);
+
+$_services['put'] = array(
+    'args' => array('path' => 'string', 'content' => 'string', 'content_type' => 'string'),
+    'type' => 'int',
+);
+
+$_services['mkcol'] = array(
+    'args' => array('path' => 'string'),
+    'type' => 'int',
+);
+
+$_services['move'] = array(
+    'args' => array('path' => 'string', 'dest' => 'string'),
+    'type' => 'int',
+);
+
+$_services['path_delete'] = array(
+    'args' => array('path' => 'string'),
+    'type' => 'int',
+);
+
 $_services['perms'] = array(
     'args' => array(),
     'type' => '{urn:horde}stringArray');
@@ -32,6 +58,417 @@ $_services['setSelectList'] = array(
     'args' => array('selectid' => 'string', 'files' => 'array'),
     'type' => 'string');
 
+$_services['getViewLink'] = array(
+    'args' => array('dir' => 'string', 'file' => 'string', 'backend' => 'string'),
+    'type' => 'string');
+
+/**
+ * Browses through the VFS tree.
+ *
+ * Each VFS backend is listed as a directory at the top level.  No modify
+ * operations are allowed outside any VFS area.
+ *
+ * @since Gollem 1.1
+ *
+ * @param string $path       The level of the tree to browse.
+ * @param array $properties  The item properties to return. Defaults to 'name',
+ *                           'icon', and 'browseable'.
+ *
+ * @return array  The contents of $path.
+ */
+function _gollem_browse($path = '', $properties = array())
+{
+    @define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+    $GLOBALS['authentication'] = 'none';
+    require_once GOLLEM_BASE . '/lib/base.php';
+    require_once GOLLEM_BASE . '/lib/Session.php';
+    require GOLLEM_BASE . '/config/backends.php';
+    require GOLLEM_BASE . '/config/credentials.php';
+
+    $path = Gollem::stripAPIPath($path);
+
+    // Default properties.
+    if (!$properties) {
+        $properties = array('name', 'icon', 'browseable');
+    }
+
+    $results = array();
+    if ($path == '') {
+        // We are at the root of gollem.  Return a set of folders, one for
+        // each backend available.
+        foreach ($backends as $backend => $curBackend) {
+            if (Gollem::checkPermissions('backend', PERMS_SHOW, $backend)) {
+                $results['gollem/' . $backend]['name'] = $curBackend['name'];
+                $results['gollem/' . $backend]['browseable'] = true;
+            }
+        }
+    } else {
+        // A file or directory has been requested.
+
+        // Locate the backend_key in the path.
+        if (strchr($path, '/')) {
+            $backend_key = substr($path, 0, strpos($path, '/'));
+        } else {
+            $backend_key = $path;
+        }
+
+        // Validate and perform permissions checks on the requested backend
+        if (!isset($backends[$backend_key])) {
+            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+        }
+        //if (!Gollem::canAutoLogin($backend_key)) {
+        //    // FIXME: Is it possible to request secondary authentication
+        //    // credentials here for backends that require it?
+        //    return PEAR::raiseError(_("Additional authentication required."));
+        //}
+        if (!Gollem_Session::createSession($backend_key)) {
+            return PEAR::raiseError(_("Unable to create Gollem session"));
+        }
+        if (!Gollem::checkPermissions('backend', PERMS_READ)) {
+            return PEAR::raiseError(_("Permission denied to this backend."));
+        }
+
+        // Trim off the backend_key (and '/') to get the VFS relative path
+        $fullpath = substr($path, strlen($backend_key) + 1);
+
+        // Get the VFS-standard $name,$path pair
+        list($name, $path) = Gollem::getVFSPath($fullpath);
+
+        // Check to see if the request is a file or folder
+        if ($GLOBALS['gollem_vfs']->isFolder($path, $name)) {
+            // This is a folder request.  Return a directory listing.
+            $list = Gollem::listFolder($path . '/' . $name);
+            if (is_a($list, 'PEAR_Error')) {
+                return $list;
+            }
+
+            // Iterate over the directory contents
+            if (is_array($list) && count($list)) {
+                $index = 'gollem/' . $backend_key . '/' . $fullpath;
+                foreach ($list as $key => $val) {
+                    $entry = Gollem::pathEncode($index . '/' . $val['name']);
+                    $results[$entry]['name'] = $val['name'];
+                    $results[$entry]['modified'] = $val['date'];
+                    if ($val['type'] == '**dir') {
+                        $results[$entry]['browseable'] = true;
+                    } else {
+                        $results[$entry]['browseable'] = false;
+                        $results[$entry]['contentlength'] = $val['size'];
+                    }
+                }
+            }
+        } else {
+            // A file has been requested.  Return the contents of the file.
+
+            // Get the file meta-data
+            $list = Gollem::listFolder($path);
+            $i = false;
+            foreach ($list as $key => $file) {
+                if ($file['name'] == $name) {
+                    $i = $key;
+                    break;
+                }
+            }
+            if ($i === false) {
+                // File not found
+                return $i;
+            }
+
+            // Read the file contents
+            $data = $GLOBALS['gollem_vfs']->read($path, $name);
+            if (is_a($data, 'PEAR_Error')) {
+                return false;
+            }
+
+            // Send the file
+            $results['name'] = $name;
+            $results['data'] = $data;
+            $results['contentlength'] = $list[$i]['size'];
+            $results['mtime'] = $list[$i]['date'];
+        }
+    }
+
+    return $results;
+}
+
+/**
+ * Accepts a file for storage into the VFS
+ *
+ * @since Gollem 1.1
+ *
+ * @param string $path           Path to store file
+ * @param string $content        Contents of file
+ * @param string $content_type   MIME type of file
+ *
+ * @return mixed                 True on success; PEAR_Error on failure
+ */
+function _gollem_put($path, $content, $content_type)
+{
+    @define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+    // Gollem does not handle authentication
+    $GLOBALS['authentication'] = 'none';
+
+    // Include Gollem base libraries
+    require_once GOLLEM_BASE . '/lib/base.php';
+    require_once GOLLEM_BASE . '/lib/Session.php';
+    require GOLLEM_BASE . '/config/backends.php';
+    require GOLLEM_BASE . '/config/credentials.php';
+
+    // Clean off the irrelevant portions of the path
+    $path = Gollem::stripAPIPath($path);
+
+    if ($path == '') {
+        // We are at the root of gollem.  Any writes at this level are
+        // disallowed.
+        return PEAR::raiseError(_("Files must be written inside a VFS backend."));
+    } else {
+        // We must be inside one of the VFS areas.  Determine which one.
+         // Locate the backend_key in the path
+        if (strchr($path, '/')) {
+            $backend_key = substr($path, 0, strpos($path, '/'));
+        } else {
+            $backend_key = $path;
+        }
+
+        // Validate and perform permissions checks on the requested backend
+        if (!isset($backends[$backend_key])) {
+            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+        }
+        //if (!Gollem::canAutoLogin($backend_key)) {
+        //    // FIXME: Is it possible to request secondary authentication
+        //    // credentials here for backends that require it?
+        //    return PEAR::raiseError(_("Additional authentication required."));
+        //}
+        if (!Gollem_Session::createSession($backend_key)) {
+            return PEAR::raiseError(_("Unable to create Gollem session"));
+        }
+        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+            return PEAR::raiseError(_("Permission denied to this backend."));
+        }
+
+        // Trim off the backend_key (and '/') to get the VFS relative path
+        $fullpath = substr($path, strlen($backend_key) + 1);
+
+        // Get the VFS-standard $name,$path pair
+        list($name, $path) = Gollem::getVFSPath($fullpath);
+
+        return $GLOBALS['gollem_vfs']->writeData($path, $name, $content);
+    }
+}
+
+/**
+ * Creates a directory ("collection" in WebDAV-speak) within the VFS
+ *
+ * @since Gollem 1.1
+ *
+ * @param string $path           Path of directory to create
+ *
+ * @return mixed                 True on success; PEAR_Error on failure
+ */
+function _gollem_mkcol($path)
+{
+    @define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+    // Gollem does not handle authentication
+    $GLOBALS['authentication'] = 'none';
+
+    // Include Gollem base libraries
+    require_once GOLLEM_BASE . '/lib/base.php';
+    require_once GOLLEM_BASE . '/lib/Session.php';
+    require GOLLEM_BASE . '/config/backends.php';
+    require GOLLEM_BASE . '/config/credentials.php';
+
+    // Clean off the irrelevant portions of the path
+    $path = Gollem::stripAPIPath($path);
+
+    if ($path == '') {
+        // We are at the root of gollem.  Any writes at this level are
+        // disallowed.
+        return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
+    } else {
+        // We must be inside one of the VFS areas.  Determine which one.
+        // Locate the backend_key in the path
+        if (!strchr($path, '/')) {
+            // Disallow attempts to create a share-level directory.  
+            return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
+        } else {
+            $backend_key = substr($path, 0, strpos($path, '/'));
+        }
+
+        // Validate and perform permissions checks on the requested backend
+        if (!isset($backends[$backend_key])) {
+            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+        }
+        //if (!Gollem::canAutoLogin($backend_key)) {
+        //    // FIXME: Is it possible to request secondary authentication
+        //    // credentials here for backends that require it?
+        //    return PEAR::raiseError(_("Additional authentication required."));
+        //}
+        if (!Gollem_Session::createSession($backend_key)) {
+            return PEAR::raiseError(_("Unable to create Gollem session"));
+        }
+        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+            return PEAR::raiseError(_("Permission denied to this backend."));
+        }
+
+        // Trim off the backend_key (and '/') to get the VFS relative path
+        $fullpath = substr($path, strlen($backend_key) + 1);
+
+        // Get the VFS-standard $name,$path pair
+        list($name, $path) = Gollem::getVFSPath($fullpath);
+
+        return $GLOBALS['gollem_vfs']->createFolder($path, $name);
+    }
+}
+
+/**
+ * Renames a file or directory
+ *
+ * @since Gollem 1.1
+ *
+ * @param string $path           Path to source object to be renamed
+ * @param string $dest           Path to new name
+ *
+ * @return mixed                 True on success; PEAR_Error on failure
+ */
+function _gollem_move($path, $dest)
+{
+    @define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+    // Gollem does not handle authentication
+    $GLOBALS['authentication'] = 'none';
+
+    // Include Gollem base libraries
+    require_once GOLLEM_BASE . '/lib/base.php';
+    require_once GOLLEM_BASE . '/lib/Session.php';
+    require GOLLEM_BASE . '/config/backends.php';
+    require GOLLEM_BASE . '/config/credentials.php';
+
+    // Clean off the irrelevant portions of the path
+    $path = Gollem::stripAPIPath($path);
+    $dest = Gollem::stripAPIPath($dest);
+
+    if ($path == '') {
+        // We are at the root of gollem.  Any writes at this level are
+        // disallowed.
+        return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
+    } else {
+        // We must be inside one of the VFS areas.  Determine which one.
+        // Locate the backend_key in the path
+        if (!strchr($path, '/')) {
+            // Disallow attempts to rename a share-level directory.  
+            return PEAR::raiseError(_('Renaming of backends is not allowed.'));
+        } else {
+            $backend_key = substr($path, 0, strpos($path, '/'));
+        }
+
+        // Ensure that the destination is within the same backend
+        if (!strchr($dest, '/')) {
+            // Disallow attempts to rename a share-level directory.  
+            return PEAR::raiseError(_('Renaming of backends is not allowed.'));
+        } else {
+            $dest_backend_key = substr($path, 0, strpos($path, '/'));
+            if ($dest_backend_key != $backend_key) {
+                return PEAR::raiseError(_('Renaming across backends is not supported.'));
+            }
+        }
+
+        // Validate and perform permissions checks on the requested backend
+        if (!isset($backends[$backend_key])) {
+            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+        }
+        //if (!Gollem::canAutoLogin($backend_key)) {
+        //    // FIXME: Is it possible to request secondary authentication
+        //    // credentials here for backends that require it?
+        //    return PEAR::raiseError(_("Additional authentication required."));
+        //}
+        if (!Gollem_Session::createSession($backend_key)) {
+            return PEAR::raiseError(_("Unable to create Gollem session"));
+        }
+        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+            return PEAR::raiseError(_("Permission denied to this backend."));
+        }
+
+        // Trim off the backend_key (and '/') to get the VFS relative path
+        $srcfullpath = substr($path, strlen($backend_key) + 1);
+        $dstfullpath = substr($dest, strlen($backend_key) + 1);
+
+        // Get the VFS-standard $name,$path pair
+        list($srcname, $srcpath) = Gollem::getVFSPath($srcfullpath);
+        list($dstname, $dstpath) = Gollem::getVFSPath($dstfullpath);
+
+        return $GLOBALS['gollem_vfs']->rename($srcpath, $srcname, $dstpath, $dstname);
+    }
+}
+
+/**
+ * Removes a file or folder from the VFS
+ *
+ * @since Gollem 1.1
+ *
+ * @param string $path           Path of file or folder to delete
+ *
+ * @return mixed                 True on success; PEAR_Error on failure
+ */
+function _gollem_path_delete($path)
+{
+    @define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+    // Gollem does not handle authentication
+    $GLOBALS['authentication'] = 'none';
+
+    // Include Gollem base libraries
+    require_once GOLLEM_BASE . '/lib/base.php';
+    require_once GOLLEM_BASE . '/lib/Session.php';
+    require GOLLEM_BASE . '/config/backends.php';
+    require GOLLEM_BASE . '/config/credentials.php';
+
+    // Clean off the irrelevant portions of the path
+    $path = Gollem::stripAPIPath($path);
+
+    if ($path == '') {
+        // We are at the root of gollem.  Any writes at this level are
+        // disallowed.
+        return PEAR::raiseError(_("The application folder can not be deleted."));
+    } else {
+        // We must be inside one of the VFS areas.  Determine which one.
+        // Locate the backend_key in the path
+        if (strchr($path, '/')) {
+            $backend_key = substr($path, 0, strpos($path, '/'));
+        } else {
+            $backend_key = $path;
+        }
+
+        // Validate and perform permissions checks on the requested backend
+        if (!isset($backends[$backend_key])) {
+            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+        }
+        //if (!Gollem::canAutoLogin($backend_key)) {
+        //    // FIXME: Is it possible to request secondary authentication
+        //    // credentials here for backends that require it?
+        //    return PEAR::raiseError(_("Additional authentication required."));
+        //}
+        if (!Gollem_Session::createSession($backend_key)) {
+            return PEAR::raiseError(_("Unable to create Gollem session"));
+        }
+        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+            return PEAR::raiseError(_("Permission denied to this backend."));
+        }
+
+        // Trim off the backend_key (and '/') to get the VFS relative path
+        $fullpath = substr($path, strlen($backend_key) + 1);
+
+        // Get the VFS-standard $name,$path pair
+        list($name, $path) = Gollem::getVFSPath($fullpath);
+
+        // Apparently Gollem::verifyDir() (called by deleteF* next) needs to
+        // see a path with a leading '/' 
+        $path = $backends[$backend_key]['root'] . $path;
+        if ($GLOBALS['gollem_vfs']->isFolder($path, $name)) {
+            return Gollem::deleteFolder($path, $name);
+        } else {
+            return Gollem::deleteFile($path, $name);
+        }
+    }
+}
+
 function _gollem_perms()
 {
     static $perms = array();
@@ -55,16 +492,48 @@ function _gollem_perms()
 }
 
 /**
+ * Returns a link to the gollem file preview interface
+ *
+ * @since Gollem 1.1
+ *
+ * @param string $dir       File absolute path
+ * @param string $file      File basename
+ * @param string $backend   Backend key. Defaults to Gollem::getPreferredBackend()
+ *
+ * @return string  The URL string.
+ */
+function _gollem_getViewLink($dir, $file, $backend = '')
+{
+    @define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+    require_once GOLLEM_BASE . '/lib/base.php';
+
+    if (empty($backend)) {
+        $backend = Gollem::getPreferredBackend();
+    }
+
+    $url = Util::addParameter(
+        Horde::applicationUrl('view.php'),
+        array('actionID' => 'view_file',
+              'type' => substr($file, strrpos($file, '.') + 1),
+              'file' => $file,
+              'dir' => $dir,
+              'driver' => $_SESSION['gollem']['backends'][$backend]['driver']));
+
+    return $url;
+}
+
+/**
  * Creates a link to the gollem file selection window.
+ *
  * The file section window will return a cache ID value which should be used
- * (along with the selectListResults and returnFromSelectList functions
- * belong) to obtain the data from a list of selected files.
+ * (along with the selectListResults and returnFromSelectList functions below)
+ * to obtain the data from a list of selected files.
  *
- * THERE MUST be a form field named 'selectlist_selectid' in the calling
+ * There MUST be a form field named 'selectlist_selectid' in the calling
  * form. This field will be populated with the selection ID when the user
  * completes file selection.
  *
- * THERE MUST be a form parameter named 'actionID' in the calling form.
+ * There MUST be a form parameter named 'actionID' in the calling form.
  * This form will be populated with the value 'selectlist_process' when
  * the user completes file selection.  The calling form will be submitted
  * after the window closes (i.e. the calling form must process the
@@ -81,16 +550,17 @@ function _gollem_perms()
 function _gollem_selectlistLink($link_text, $link_style, $formid,
                                 $icon = false, $selectid = '')
 {
-    Horde::addScriptFile('open_selectlist_win.js', 'gollem');
-    $link = Horde::link('#', $link_text, $link_style, '', 'open_selectlist_win(\'' . $formid . '\', \'' . $selectid . '\'); return false;');
+    Horde::addScriptFile('popup.js', 'gollem');
+    $link = Horde::link('#', $link_text, $link_style, '_blank', "popup_gollem('" . Horde::applicationUrl(Util::addParameter('selectlist.php', array('formid' => $formid, 'cacheid' => $selectid))) . "', 300, 500); return false;");
     if ($icon) {
-        $link_text = Horde::img('gollem.png', $link_text, 'style="vertical-align:middle"');
+        $link_text = Horde::img('gollem.png', $link_text);
     }
-    return $link . $link_text . '</a>';
+    return '<script type="text/javascript">document.write(\''
+        . addslashes($link . $link_text) . '<\' + \'/a>\');</script>';
 }
 
 /**
- * Return the list of files selected by the user for a given selection ID.
+ * Returns the list of files selected by the user for a given selection ID.
  *
  * @param string $selectid  The selection ID.
  *
@@ -133,7 +603,7 @@ function _gollem_returnFromSelectlist($selectid, $index)
 }
 
 /**
- * Set the files selected for a given selection ID
+ * Sets the files selected for a given selection ID.
  *
  * @param string $selectid  The selection ID to use.
  * @param array $files      An array with each file entry stored in its own
@@ -148,7 +618,7 @@ function _gollem_setSelectlist($selectid = '', $files = array())
     require_once GOLLEM_BASE . '/lib/base.php';
 
     if (empty($selectid)) {
-        $selectid = substr(base_convert(microtime() . mt_rand(), 10, 36), -16);
+        $selectid = uniqid(mt_rand(), true);
     }
 
     if (count($files) > 0) {
diff --git a/lib/base.php b/lib/base.php
index b6541eb..a980f07 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -15,16 +15,16 @@
  *   $gollem_be - A link to the current backend parameters in the session
  *   $gollem_vfs - A link to the current VFS object for the active backend
  *
- * $Horde: gollem/lib/base.php,v 1.60.2.3 2007/02/16 09:12:28 slusarz Exp $
+ * $Horde: gollem/lib/base.php,v 1.60.2.4 2008/10/09 20:54:42 jan Exp $
  *
- * See the enclosed file COPYING for license information (GPL).  If you
+ * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
  */
 
 // Check for a prior definition of HORDE_BASE (perhaps by an
 // auto_prepend_file definition for site customization).
 if (!defined('HORDE_BASE')) {
-    @define('HORDE_BASE', dirname(__FILE__) . '/../..');
+    define('HORDE_BASE', dirname(__FILE__) . '/../..');
 }
 
 // Load the Horde Framework core, and set up inclusion paths.
@@ -44,22 +44,24 @@ if (is_a(($pushed = $registry->pushApp('gollem', !defined('AUTH_HANDLER'))), 'PE
     Horde::fatal($pushed, __FILE__, __LINE__, false);
 }
 $conf = &$GLOBALS['conf'];
- at define('GOLLEM_TEMPLATES', $registry->get('templates'));
+define('GOLLEM_TEMPLATES', $registry->get('templates'));
 
 // Notification system.
 $notification = &Notification::singleton();
 $notification->attach('status');
 
 // Find the base file path of Gollem.
- at define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+if (!defined('GOLLEM_BASE')) {
+    define('GOLLEM_BASE', dirname(__FILE__) . '/..');
+}
 
-// Horde base libraries.
-require_once 'Horde/Help.php';
+// Horde libraries.
 require_once 'Horde/Secret.php';
 require_once 'VFS.php';
 
-// Gollem base library.
+// Gollem libraries.
 require_once GOLLEM_BASE . '/lib/Gollem.php';
+require_once GOLLEM_BASE . '/lib/Template.php';
 
 // If Gollem isn't responsible for Horde auth, and no one is logged into
 // Horde, redirect to the login screen.
@@ -72,6 +74,13 @@ if (!Util::nonInputVar('no_compress')) {
     Horde::compressOutput();
 }
 
+// Set the global $gollem_be variable to the current backend's parameters.
+if (empty($_SESSION['gollem']['backend_key'])) {
+    $GLOBALS['gollem_be'] = null;
+} else {
+    $GLOBALS['gollem_be'] = &$_SESSION['gollem']['backends'][$_SESSION['gollem']['backend_key']];
+}
+
 $authentication = Util::nonInputVar('authentication');
 if ($authentication !== 'none') {
     // If we've gotten to this point and have valid login credentials
@@ -86,15 +95,9 @@ if ($authentication !== 'none') {
         exit;
     }
 
+    // Check authentication and create $GLOBALS['gollem_vfs'] object
     Gollem::checkAuthentication($authentication);
 }
 
-// Set the global $gollem_be variable to the current backend's parameters.
-if (empty($_SESSION['gollem']['backend_key'])) {
-    $GLOBALS['gollem_be'] = null;
-} else {
-    $GLOBALS['gollem_be'] = &$_SESSION['gollem']['backends'][$_SESSION['gollem']['backend_key']];
-}
-
 // Load the backend list.
 Gollem::loadBackendList();
diff --git a/lib/prefs.php b/lib/prefs.php
index 5645ad3..4f6b03e 100644
--- a/lib/prefs.php
+++ b/lib/prefs.php
@@ -1,13 +1,13 @@
 <?php
 /**
- * $Horde: gollem/lib/prefs.php,v 1.2.2.2 2007/01/02 13:54:52 jan Exp $
+ * $Horde: gollem/lib/prefs.php,v 1.2.2.4 2009/01/06 15:23:54 jan Exp $
  *
- * Copyright 1999-2007 Charles J. Hagenbuch <chuck at horde.org>
- * Copyright 1999-2007 Jon Parise <jon at horde.org>
- * Copyright 1999-2007 Max Kalika <max at horde.org>
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
  *
- * See the enclosed file COPYING for license information (GPL).  If you
+ * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Max Kalika <max at horde.org>
  */
 
 function handle_columnselect($updated)
@@ -16,7 +16,6 @@ function handle_columnselect($updated)
     if (!empty($columns)) {
         $GLOBALS['prefs']->setValue('columns', $columns);
         return true;
-    } else {
-        return false;
     }
+    return false;
 }
diff --git a/lib/version.php b/lib/version.php
index b5c55de..f741795 100644
--- a/lib/version.php
+++ b/lib/version.php
@@ -1 +1 @@
-<?php define('GOLLEM_VERSION', 'H3 (1.0.3)') ?>
+<?php define('GOLLEM_VERSION', 'H3 (1.1)') ?>
diff --git a/locale/bg_BG/LC_MESSAGES/gollem.mo b/locale/bg_BG/LC_MESSAGES/gollem.mo
index c321080..ee7d17c 100644
Binary files a/locale/bg_BG/LC_MESSAGES/gollem.mo and b/locale/bg_BG/LC_MESSAGES/gollem.mo differ
diff --git a/locale/ca_ES/help.xml b/locale/ca_ES/help.xml
index 7747681..722adc4 100755
--- a/locale/ca_ES/help.xml
+++ b/locale/ca_ES/help.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="iso-8859-1"?>
-<!-- $Horde: gollem/locale/ca_ES/help.xml,v 1.1.2.1 2007/02/07 16:23:16 jan Exp $ -->
+<!-- $Horde: gollem/locale/ca_ES/help.xml,v 1.1.2.2 2008/10/09 20:54:43 jan Exp $ -->
 <help>
 
-<entry id="file-actions" md5="4496a389972e236d397a3661b6767c9e" state="uptodate">
+<entry id="file-actions" md5="b0de5fc9b7cc30fea350fc3900737896" state="uptodate">
     <title>Administrador d'arxius: Accions</title>
     <heading>Creació de carpetes</heading>
     <para>
diff --git a/locale/cs_CZ/LC_MESSAGES/gollem.mo b/locale/cs_CZ/LC_MESSAGES/gollem.mo
index 40c3ee9..3d79773 100644
Binary files a/locale/cs_CZ/LC_MESSAGES/gollem.mo and b/locale/cs_CZ/LC_MESSAGES/gollem.mo differ
diff --git a/locale/da_DK/LC_MESSAGES/gollem.mo b/locale/da_DK/LC_MESSAGES/gollem.mo
index 0f8aa73..1af7524 100644
Binary files a/locale/da_DK/LC_MESSAGES/gollem.mo and b/locale/da_DK/LC_MESSAGES/gollem.mo differ
diff --git a/locale/de_DE/LC_MESSAGES/gollem.mo b/locale/de_DE/LC_MESSAGES/gollem.mo
index a9ff96f..8eb6674 100644
Binary files a/locale/de_DE/LC_MESSAGES/gollem.mo and b/locale/de_DE/LC_MESSAGES/gollem.mo differ
diff --git a/locale/en_US/help.xml b/locale/en_US/help.xml
index f9e6871..c6e4c06 100644
--- a/locale/en_US/help.xml
+++ b/locale/en_US/help.xml
@@ -1,20 +1,20 @@
 <?xml version='1.0'?>
-<!-- $Horde: gollem/locale/en_US/help.xml,v 1.7 2005/08/03 06:43:46 slusarz Exp $ -->
+<!-- $Horde: gollem/locale/en_US/help.xml,v 1.7.2.1 2008/10/09 20:54:45 jan Exp $ -->
 <help>
 
 <entry id="file-actions">
     <title>File Manager: Actions</title>
     <heading>Creating Folders</heading>
     <para>
-    To create a folder in the current directory, simply choose Create Folder from the More Actions drop down box.  
+    To create a folder in the current directory, simply choose Create Folder from the Actions drop down box.  
     </para>
     <heading>Delete, cut, copy and paste files</heading>
     <para>
     In order to delete, cut or copy a file or files, you need to select at least one file.
     After you select one or more files using the checkboxes to the left of the item, select either Cut Items, Copy Items or 
-    Delete Items from the More Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
+    Delete Items from the Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
     paste those files elsewhere in the File Manager.  Navigate to the location that you wish to paste the items, then select 
-    Paste Items from the More Actions drop down box.  The items will now be either moved (Cut Items) or copied (Copy Items) to 
+    Paste Items from the Actions drop down box.  The items will now be either moved (Cut Items) or copied (Copy Items) to 
     the location that you chose.
     </para>
     <heading>Delete, cut, copy and paste directories</heading>
@@ -26,9 +26,9 @@
     
     In order to delete, cut or copy a directory or directories, you need to select at least one directory.
     After you select one or more directories using the checkboxes to the left of the item, select either Cut Items, Copy Items 
-    or Delete Items from the More Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
+    or Delete Items from the Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
     paste those directories elsewhere in the File Manager.  Navigate to the location that you wish to paste the items, then 
-    select Paste Items from the More Actions drop down box.  The items will now either be moved (Cut Items) or copied (Copy Items)
+    select Paste Items from the Actions drop down box.  The items will now either be moved (Cut Items) or copied (Copy Items)
     to the location that you chose.
     </para>
     <heading>Working with the Clipboard</heading>
diff --git a/locale/es_ES/LC_MESSAGES/gollem.mo b/locale/es_ES/LC_MESSAGES/gollem.mo
index ebde13a..8f1ae6a 100644
Binary files a/locale/es_ES/LC_MESSAGES/gollem.mo and b/locale/es_ES/LC_MESSAGES/gollem.mo differ
diff --git a/locale/es_ES/help.xml b/locale/es_ES/help.xml
index 574e689..99350bb 100644
--- a/locale/es_ES/help.xml
+++ b/locale/es_ES/help.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0"?>
-<!-- $Horde: gollem/locale/es_ES/help.xml,v 1.6.2.2 2006/02/16 17:21:11 jan Exp $ -->
+<!-- $Horde: gollem/locale/es_ES/help.xml,v 1.6.2.4 2009/02/21 11:16:28 jan Exp $ -->
 <help>
-
-<entry id="file-actions" md5="4496a389972e236d397a3661b6767c9e" state="uptodate">
+  <entry id="file-actions" md5="4496a389972e236d397a3661b6767c9e" state="changed">
     <title>Administrador de archivos: Acciones</title>
     <heading>Creaci&#xF3;n de carpetas</heading>
     <para>
-    Para crear una carpeta en el directorio actual, elija Crear carpeta en la barra de men&#xFA;.
+    Para crear una carpeta en el directorio actual, elija Crear carpeta en el men&#xFA; desplegable Acciones.
     </para>
     <heading>Eliminar, cortar, copiar y pegar archivos</heading>
     <para>
@@ -40,15 +39,14 @@
     del portapapeles pulsando su icono, que est&#xE1; sobre el listado de carpetas a la derecha del icono Actualizar
     El icono no se mostrar&#xE1; a menos que exista alg&#xFA;n elemento en el portapapeles. Los elementos cortados o copiados
     no dispondr&#xE1;n de una casilla a su izquierda, lo que significa que est&#xE1;n en el portapapeles. Cuando pulse en el 
-    icono del portapapeles, ver&#xE1; un listado de los elementos que haya copiado o cortado. Seleccione los elementos que 
+    icono del portapapeles, ver&#xE1; un listado de los elementos que haya copiado o cortado. Puede pegar esos elementos en la carpeta actual, mostrada sobre la lista de elementos, desde el visor del Portapapeles. Seleccione los elementos que 
     desee pegar y pulse pegar. Esos elementos se pegar&#xE1;n en la carpeta actual y se eliminar&#xE1;n del portapapeles. 
     Tras completar la operaci&#xF3;n de pegado, regresar&#xE1; a la carpeta actual. Si deseara eliminar un elemento del portapapeles, 
-    s&#xF3;lo tiene que seleccionarlo y pulsar Limpiar. Esos elementos se eliminar&#xE1;n del portapapeles se mostrar&#xE1; de
-    nuevo la carpeta actual.  
+    s&#xF3;lo tiene que seleccionarlo y pulsar Limpiar. Esos elementos se eliminar&#xE1;n del portapapeles y se mostrar&#xE1; de
+    nuevo la carpeta actual.
     </para>
 </entry>
-
-<entry id="file-upload" md5="f07834ebb15641cdc4e349ed8d304d75" state="uptodate">
+  <entry id="file-upload" md5="f07834ebb15641cdc4e349ed8d304d75" state="uptodate">
     <title>Administrador de archivos: Carga</title>
     <heading>Carga de archivos</heading>
     <para>
@@ -61,8 +59,7 @@
     actual del Administrador de archivos.
 </para>
 </entry>
-
-<entry id="sorting" md5="0ce2080b01acce2e9affc2191fc0386c" state="uptodate">
+  <entry id="sorting" md5="0ce2080b01acce2e9affc2191fc0386c" state="uptodate">
     <title>Ordenaci&#xF3;n de entradas</title>
     <heading>Ordenaci&#xF3;n de entradas</heading>
     <para>
diff --git a/locale/et_EE/LC_MESSAGES/gollem.mo b/locale/et_EE/LC_MESSAGES/gollem.mo
new file mode 100644
index 0000000..4d7cb7b
Binary files /dev/null and b/locale/et_EE/LC_MESSAGES/gollem.mo differ
diff --git a/locale/eu_ES/LC_MESSAGES/gollem.mo b/locale/eu_ES/LC_MESSAGES/gollem.mo
new file mode 100644
index 0000000..ed47e44
Binary files /dev/null and b/locale/eu_ES/LC_MESSAGES/gollem.mo differ
diff --git a/locale/eu_ES/help.xml b/locale/eu_ES/help.xml
new file mode 100644
index 0000000..fd33b68
--- /dev/null
+++ b/locale/eu_ES/help.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<!-- $Horde: gollem/locale/eu_ES/help.xml,v 1.1.2.2 2008/10/09 20:54:48 jan Exp $ -->
+<help>
+
+<entry id="file-actions" md5="b0de5fc9b7cc30fea350fc3900737896" state="uptodate">
+    <title>Fitxategi-kudeatzailea: Ekintzak</title>
+    <heading>Karpetak sortzea</heading> <para>Uneko direktorioan karpeta bat sortzeko, hautatu 'Ekintza gehiago' goitibehera koadroan 'Sortu karpeta' aukera.</para> <heading>Fitxategiak ezabatu, ebaki, kopiatu eta itsatsi</heading> <para>Fitxategi bat edo fitxategi batzuk ezabatu, ebaki edo kopiatzeko, fitxategi bat hautatu behar duzu gutxienez. Fitxategi bat edo gehiago hautatu badituzu elementuaren ezkerrean dauden kontrol-laukiak erabiliz, hautatu Ebaki elementuak, Kopiatu elementuak edo Ezabatu elementuak 'Ekintza gehiago' izeneko goitibeherako koadroan.  Ebaki elementuak edo Kopiatu elementuak hautatu baduzu, fitxategi horiek Fitxategi-kudeatzaileko beste leku batean itsats ditzakezu orain.  Nabigatu elementuak itsatsi nahi dituzun lekura, eta ondoren hautatu Itsatsi elementuak 'Ekintza gehiago' izeneko goitibeherako koadroan.  Orduan elementuak lekuz aldatu (Ebaki elementuak) edo kopiatu (Kopiatu elementuak) egingo dira zuk aukeratutako lekura.</para> <heading>Ezabatu, ebaki, kopiatu eta itsatsi direktorioak</heading> <para>Lehenespenez, hutsik dauden direktorioak bakarrik ezaba ditzakezu, baina hutsik ez dauden direktorioak ezabatzeko aukera ere gaitu dezakezu (ezabatze errekurtsiboa).  Hutsik ez dagoen direktorio bat ezabatzen saiatzen bazara eginbide hau gaitu gabe, ezabatzeak huts egingo du eta errorea emango dizu.  Hutsik ez dauden direktorioak ezabatzeko aukera gaitzeko, hautatu Aukerak, Ezarpenak eta aukeratu Bai, 'Ezabatu karpetak errekurtsiboki' aukeraren goitibeherako koadroan.  Orduan gaituta geratuko da ezabatze errekurtsiboaren aukera.
+    
+    Direktorio bat edo gehiago ezabatzeko, ebakitzeko edo kopiatzeko, direktorio bat hautatu behar duzu gutxienez. Direktorio bat edo gehiago hautatu badituzu elementuaren ezkerrean dauden kontrol-laukiak erabiliz, hautatu Ebaki elementuak, Kopiatu elementuak edo Ezabatu elementuak 'Ekintza gehiago' izeneko goitibeherako koadroan.  Ebaki elementuak edo Kopiatu elementuak hautatu baduzu, direktorio horiek Fitxategi-kudeatzaileko beste leku batean itsats ditzakezu orain.  Nabigatu elementuak itsatsi nahi dituzun lekura, eta ondoren hautatu Itsatsi elementuak 'Ekintza gehiago' izeneko goitibeherako koadroan.  Orduan elementuak lekuz aldatu (Ebaki elementuak) edo kopiatu (Kopiatu elementuak) egingo dira zuk aukeratutako lekura.</para> <heading>Arbelaren erabilera</heading> <para>Fitxategi edo direktorio bat kopiatzean edo ebakitzean, arbelean kokatzen da.  Arbelaren edukia ikusteko, egin klik arbelaren ikonoan. Direktorio/Karpeten zerrendaren goialdean eta 'Freskatu' ikonoaren eskuinean aurkituko duzu.  Ikonoa ez da agertuko arbelean elementurik ez badago.  Ebakitzen edo kopiatzen diren elementuek ez dute kontrol-laukirik edukiko ezkerraldean, arbelean daudela esan nahi baitu horrek.  Arbel-ikustailean klik egindakoan, kopiatu edo ebakitako elementuen zerrenda ikusiko duzu. Arbel-ikustailetik, elementu horiek uneko direktorioan itsats ditzakezu. Elementuen zerrendaren goialdean bistaratzen da, hain zuzen ere, uneko direktorioa. Besterik gabe hautatu itsatsi nahi dituzun elementuak, eta egin klik Itsatsi botoian.  Elementu horiek uneko direktorioan itsatsiko dira, arbeletik kenduz aldi berean.  Itsasten amaitutakoan, uneko direktoriora itzuliko zara.  Elementu bat kendu nahi baduzu arbeletik, egin klik elementuetan eta sakatu Garbitu botoia.  Elementuak kendu egingo dira arbeletik, eta uneko direktoriora itzuliko zara.</para></entry>
+
+<entry id="file-upload" md5="f07834ebb15641cdc4e349ed8d304d75" state="uptodate">
+    <title>Fitxategi-kudeatzailea: Kargatu</title>
+    <heading>Fitxategiak kargatu</heading> <para>Fitxategi bat edo gehiago kargatzeko, nabigatu fitxategiak kargatu nahi dituzun direktoriora Fitxategi-kudeatzailean.  Direktorio horri Uneko direktorioa esaten zaio.  Direktorio egokian zaudenean Fitxategi-kudeatzailean, egin klik Arakatu botoian. Fitxategiak kargatzeko leihoa bistaratuko da.  Leihoa irekitzen denean, nabigatu kargatu nahi duzun fitxategira, hautatu eta egin klik Ados.  Hainbat fitxategi karga ditzakezu batera.  Fitxategi-kudeatzaileak automatikoki gehituko du beste Arakatu botoi bat fitxategi bat hautatzen duzun bakoitzean.  Kargatu nahi dituzun fitxategi guztiak hautatutakoan, egin klik 'Kargatu fitxategia(k)' botoian eta hautatutako fitxategiak kargatu egingo dira Fitxategi-kudeatzaileko uneko direktoriora.</para></entry>
+
+<entry id="sorting" md5="0ce2080b01acce2e9affc2191fc0386c" state="uptodate">
+    <title>Ordenatu sarrerak</title>
+    <heading>Ordenatu sarrerak</heading> <para>Fitxategi edo direktorioen zerrenda bat ikustean, edozein zutaberen arabera ordena ditzakezu.  Hori egiteko, egin klik ordenatzeko erabili nahi duzun zutabearen goiburuan, eta zerrenda freskatu egingo da, hautatutako zutabearen arabera ordenatuta.  Goranzko edo beheranzko ordenara aldatzeko, klik egin zutabearen izenburuko geziaren ikonoan.</para></entry></help>
diff --git a/locale/fi_FI/LC_MESSAGES/gollem.mo b/locale/fi_FI/LC_MESSAGES/gollem.mo
index 1eefd65..7c1221f 100644
Binary files a/locale/fi_FI/LC_MESSAGES/gollem.mo and b/locale/fi_FI/LC_MESSAGES/gollem.mo differ
diff --git a/locale/fi_FI/help.xml b/locale/fi_FI/help.xml
index 5a56cfa..0c24a5a 100644
--- a/locale/fi_FI/help.xml
+++ b/locale/fi_FI/help.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0"?>
-<!-- $Horde: gollem/locale/fi_FI/help.xml,v 1.2.2.1 2006/02/20 11:20:49 jan Exp $ -->
+<!-- $Horde: gollem/locale/fi_FI/help.xml,v 1.2.2.4 2009/02/03 00:16:18 jan Exp $ -->
 <help>
-  <entry id="file-actions" md5="4496a389972e236d397a3661b6767c9e" state="uptodate">
+  <entry id="file-actions" md5="b0de5fc9b7cc30fea350fc3900737896" state="uptodate">
     <title>Tiedostohallinta: Toiminnot</title>
     <heading>Hakemistojen luonti</heading>
     <para>
@@ -13,37 +13,22 @@
     Kun olet valinnut yhden tai useamman tiedoston rastittamalla valintalaatikot objektin vasemmalla puolella, voit alasvetovalikosta valita yhden seuraavista  toiminnoista "Leikkaa objekteja", "Kopioi objekteja" tai "Poista objektit". Jos valitset "Leikkaa objekti" tai "Kopioi objekti", niin t&#xE4;m&#xE4;n j&#xE4;lkeen voit liitt&#xE4;&#xE4; objektin muuhun paikkaan tiedostonhallinnassa. Kun haluat liitt&#xE4;&#xE4; objektin, niin siirry valitsemaasi kohteeseen ja valitse t&#xE4;m&#xE4;n j&#xE4;lkeen toiminnot alasvetovalikosta "Liit&#xE4; objekti". T&#xE4;m&#xE4;n j&#xE4;lkeen objektit ovat joko siirtyneet (Leikkaa objekti) tai kopioituneet (Kopioi objekti) valitsemaasi kohteeseen.
     </para>
     <heading>Hakemistojen poisto, leikkaus, kopiointi ja liit&#xE4;minen</heading>
-    <heading>Delete, cut, copy and paste directories</heading>
     <para>	
     Oletuksena voit poistaa hakemiston vain jos se on tyhj&#xE4;. On kuitenkin mahdollista poistaa ei-tyhj&#xE4; hakemisto jos valitset asetuksen "Poista hakemistot rekursiivisesti". Jos yrit&#xE4;t poistaa ei-tyhj&#xE4;&#xE4; hakemistoa ilman ett&#xE4; sinulla on p&#xE4;&#xE4;ll&#xE4; em. asetus, niin poistaminen ep&#xE4;onnistuu virheilmoituksen kera. Jos haluat poistaa ei-tyhji&#xE4; hakemistoja, niin valitse laita asetuksista p&#xE4;&#xE4;lle asetus "Poista hakemistot rekursiivisesti.".
     Poistaaksesi, leikataksesi tai kopioidaksesi hakemistoja, sinun pit&#xE4;&#xE4; valita ainakin yksi hakemisto.
     Kun olet valinnut yhden tai useamman hakemiston rastittamalla valintalaatikon hakemiston vasemmalla puolella, voit alasvetovalikosta valita yhden seuraavista  toiminnoista "Leikkaa objekteja", "Kopioi objekteja" tai "Poista objektit". Jos valitset "Leikkaa objekti" tai "Kopioi objekti", niin t&#xE4;m&#xE4;n j&#xE4;lkeen voit liitt&#xE4;&#xE4; objektin muuhun paikkaan tiedostonhallinnassa. Kun haluat liitt&#xE4;&#xE4; objektin, niin siirry valitsemaasi kohteeseen ja valitse t&#xE4;m&#xE4;n j&#xE4;lkeen toiminnot alasvetovalikosta "Liit&#xE4; objekti". T&#xE4;m&#xE4;n j&#xE4;lkeen objektit ovat joko siirtyneet (Leikkaa objekti) tai kopioituneet (Kopioi objekti) valitsemaasi kohteeseen.
     </para>
-    <heading>Ty&#xF6;skentely leikep&#xF6;yd&#xE4;n kanssa</heading>
+
+    <heading>Ty&#xF6;skentely leikep&#xF6;yd&#xE4;ll&#xE4;</heading>
     <para>
-    Kun kopioit tai siirr&#xE4;t tiedoston tai hakemiston, niin se
-    laitetaan leikep&#xF6;yd&#xE4;lle. Voit tarkistaa leikep&#xF6;yd&#xE4;n sis&#xE4;ll&#xF6;n
-    napsauttamalla leikep&#xF6;yt&#xE4; -ikonia, joka l&#xF6;ytyy
-    hakemisto/kansiolistauksen yl&#xE4;puolelta p&#xE4;ivit&#xE4; -ikonin oikealta
-    puolelta. Leikep&#xF6;yt&#xE4; -ikoni ei ole n&#xE4;kyvill&#xE4;, jos leikep&#xF6;yt&#xE4; on
-    tyhj&#xE4;. Leikep&#xF6;yd&#xE4;lle kopioitujen tai siirrettyjen objektien vieress&#xE4;
-    vasemmalla puolella ei ole valintaruutua. Kun napsautat leikep&#xF6;yt&#xE4;
-    -ikonia, niin sinulle tulee n&#xE4;kyviin sinne kopioidut tai siirretyt
-    objektit. Leikep&#xF6;yt&#xE4; n&#xE4;kym&#xE4;st&#xE4; voit liitt&#xE4;&#xE4; tietoja sen hetkiseen
-    hakemistoon, joka n&#xE4;kyy objektien yl&#xE4;puolella. Liitt&#xE4;&#xE4;ksesi sen
-    hetkiseen hakemistoon ohjektit valitse vain haluamasi ja valitse
-    liit&#xE4;. Liitetyt objektit poistuvat liit&#xE4; operaation j&#xE4;lkeen
-    automaattisesti leikep&#xF6;yd&#xE4;lt&#xE4; ja palaat takaisin sen hetkiseen
-    hakemistoon. Jos haluat poistaa objektin leikep&#xF6;yd&#xE4;lt&#xE4;, niin valitse
-    se ja napsauta tyhjenn&#xE4; painiketta. Toiminnon j&#xE4;lkeen palaat takaisin
-    sen hetkiseen hakemistoon.
+    Kun olet kopioinut tai leikannut tiedoston tai hakemiston, niin se laitetaan leikep&#xF6;yd&#xE4;lle. Voit tarkastella leikep&#xF6;yd&#xE4;n sis&#xE4;lt&#xF6;&#xE4; napsauttamalla leikep&#xF6;yd&#xE4;n kuvaketta, joka sijaitsee hakemistolistauksen yl&#xE4;puolella oikealla p&#xE4;ivit&#xE4; ikonista. T&#xE4;m&#xE4; ikoni on n&#xE4;kyviss&#xE4; vain jos leikep&#xF6;yd&#xE4;ll&#xE4; on objekteja. Jos objektin vieress&#xE4; ei ole valintalaatikkoa, niin se tarkoittaa, ett&#xE4; n&#xE4;m&#xE4; objektit on jo leikep&#xF6;yd&#xE4;ll&#xE4;. Kun napsautat auki leikep&#xF6;yd&#xE4;n, niin n&#xE4;et listan objekteista, jotka on sinne kopioitu. Voit t&#xE4;st&#xE4; listasta valita haluamasi objektit ja liitt&#xE4;&#xE4; ne nykyiseen hakemistoon. Palaat automaattisesti takaisin nykyiseen hakistoon kun liitt&#xE4;minen on onnistunut. Jos haluat poistaa jonkun objektin leikep&#xF6;yd&#xE4;lt&#xE4;, niin valitse objekti ja napsauta poista. Kun poistaminen on onnistunut, niin palaat takaisin nykyiseen hakemistoon.
     </para>
 </entry>
   <entry id="file-upload" md5="f07834ebb15641cdc4e349ed8d304d75" state="uptodate">
-    <title>Tiedostohallinta: Tuonti</title>
-    <heading>Tiedostojen tuonti</heading>
+    <title>Tiedostohallinta: Tuonti palvelimelle</title>
+    <heading>Tiedostojen tuonti palvelimelle</heading>
     <para>
-    Tuodaksesi tiedoston tai tiedostoja, niin mene ensin siihen hakemistoon johon haluat tuoda tiedostot. T&#xE4;m&#xE4; valitsemasi hakemisto on nykyinen hakemistosi. Kun tiedostonhallinnassa oikeassa hakemistossa, niin napsauta "Browse" -painiketta ja tiedoston tuonti -ikkunan pit&#xE4;isi aueta. Kun ikkuna on auki ja n&#xE4;kyviss&#xE4; valitse tuotava tiedosto ja napsauta OK-painiketta. Voit my&#xF6;s tuoda useampia tiedostoja kerrallaan, joka kerta kun olet valinnut yhden tiedoston, niin ilmestyy uusi "Browse" -painike. Kun olet valinnut kaikki tuotavat tiedostot, niin napsauta "Tuo tiedostot" -painiketta ja valitsemasi tiedostot tuodaan tiedostohallinnan nykyiseen hakemistoon.
+    Tuodaksesi tiedoston tai tiedostoja palvelimelle, niin mene ensin siihen hakemistoon johon haluat tuoda tiedostot. T&#xE4;m&#xE4; valitsemasi hakemisto on nykyinen hakemistosi. Kun olet tiedostonhallinnassa oikeassa hakemistossa, niin napsauta "Browse" -painiketta ja tiedoston tuonti palvelimelle -ikkunan pit&#xE4;isi aueta. Kun ikkuna on auennut ja olet valinnut palvelimelle tuotavan tiedosto, niin napsauta OK-painiketta. Voit my&#xF6;s tuoda palvelimelle useampia tiedostoja kerrallaan. Kun olet valinnut yhden tiedoston, niin tämän jälkeen ilmestyy näkyviin uusi "Browse" -painike. Kun olet valinnut kaikki palvelimelle tuotavat tiedostot, niin napsauta "Tuo tiedostot palvelimelle" -painiketta ja valitsemasi tiedostot tuodaan palvelimelle tiedostohallinnan nykyiseen hakemistoon.
 </para>
 </entry>
   <entry id="sorting" md5="0ce2080b01acce2e9affc2191fc0386c" state="uptodate">
diff --git a/locale/fr_FR/LC_MESSAGES/gollem.mo b/locale/fr_FR/LC_MESSAGES/gollem.mo
index b97c15f..a524702 100644
Binary files a/locale/fr_FR/LC_MESSAGES/gollem.mo and b/locale/fr_FR/LC_MESSAGES/gollem.mo differ
diff --git a/locale/hu_HU/LC_MESSAGES/gollem.mo b/locale/hu_HU/LC_MESSAGES/gollem.mo
index ee144be..e1d4a10 100644
Binary files a/locale/hu_HU/LC_MESSAGES/gollem.mo and b/locale/hu_HU/LC_MESSAGES/gollem.mo differ
diff --git a/locale/hu_HU/help.xml b/locale/hu_HU/help.xml
index b8e89bc..fe29645 100644
--- a/locale/hu_HU/help.xml
+++ b/locale/hu_HU/help.xml
@@ -1,104 +1,131 @@
 <?xml version="1.0" encoding="iso-8859-2"?>
-<!-- Gollem 1.0.2 help: hu_HU $Horde: gollem/locale/hu_HU/help.xml,v 1.2.2.1 2006/03/03 13:44:11 jan Exp $ -->
+<!-- $Horde: gollem/locale/hu_HU/help.xml,v 1.2.2.2 2008/10/09 20:54:50 jan Exp $ -->
 <help>
-  <entry id="file-actions" state="uptodate" md5="4496a389972e236d397a3661b6767c9e">
-    <title>Fájlkezelõ: Mûveletek</title>
+  <entry id="file-actions" state="unknown">
+    <title>Fájl menedzser: Mûveletek</title>
     <heading>Mappák létrehozása</heading>
     <para>
-    Ha új almappát kívánunk létrehozni a jelenlegi mappán belül, válasszuk az 
-    "Új mappa létrehozása" funkciót a "Mûveletek" 
-    legördülõ menübõl.
+    Ha új almappát kívánunk létrehozni a jelenlegi mappán belül, válasszuk az 'Új mappa
+    létrehozása' funkciót a 'Mûveletek' legördülõ menübõl.
     </para>
-
-    <heading>Fájlok törlése, vágóasztalra való áthelyezése, másolása ill. onnan 
-             történõ beillesztése</heading>
+    <heading>Fájlok törlése, vágóasztalra való áthelyezése, másolása ill. onnan történõ
+    beillesztése</heading>
     <para>
-    Ezekhez a mûveletekhez elõször ki kell jelölnie legalább egy fájlt. Miután a 
-    fájlok bal oldalán levõ kis négyzetekben megjelölte a megfelelõket, válassza 
-    ki a megfelelõ funkciót a "Mûveletek" legördülõ menübõl. Ha az 
-    "Áthelyezés a vágóasztalra" vagy a 
-    "Másolás a vágóasztalra" mûveletet választotta, akkor megjelenik a
-    "Beillesztés a vágóasztalról" funkció. Keresse meg azt a mappát 
-    ahova be kívánja illeszteni a fájlokat, majd válassza ki a 
-    "Beillesztés a vágóasztalról" funkciót
-    a "Mûveletek" legördülõ menübõl. A korábban kiválasztott fájlok 
-    vagy átkerülnek (áthelyzezés esetén) vagy átmásolódnak (másolás esetén) 
-    az új mappába.
+    Ezekhez a mûveletekhez elõször ki kell jelölnie legalább egy fájlt. Miután a fájlok
+    bal oldalán levõ kis négyzetekben megjelölte a megfelelõket, válassza ki a megfelelõ
+    funkciót a 'Mûveletek' legördülõ menübõl. Ha az 'Áthelyezés a vágóasztalra' vagy a
+    'Másolás a vágóasztalra' mûveletet választotta, akkor megjelenik a
+    'Beillesztés a vágóasztalról' funkció. Keresse meg azt a mappát ahova be kívánja
+    illeszteni a fájlokat, majd válassza ki a 'Beillesztés a vágóasztalról' funkciót
+    a 'Mûveletek' legördülõ menübõl. A korábban kiválasztott fájlok vagy átkerülnek
+    (áthelyzezés esetén) vagy átmásolódnak (másolás esetén) az új mappába.
     </para>
-
-    <heading>Mappák törlése, vágóasztalra való áthelyezése, másolása ill. onnan 
-             történõ beillesztése</heading>
+    <heading>Mappák törlése, vágóasztalra való áthelyezése, másolása ill. onnan történõ
+    beillesztése</heading>
     <para>
-    Alapértelmezett beállítás esetén csak olyan mappákat törölhet, melyek üresek,
-    bár az opciókban beállíthatja a nemüres mappák (rekurzív) törölhetõségét. 
-    Enélkül viszont, ha megkísérel kitörölni egy nemüres mappát, akkor ez nem 
-    fog sikerülni, hibajelzést kap. A nemüres mappák rekurzív törléséhez 
-    kattintson az Opciókra, majd itt a Beállítások pontra, ahol már beállítható 
-    a kívánt lehetõség.
-    Ha mappákat kíván törölni, áthelyezni, vagy másolni, akkor elõször válasszon 
-    ki legalább egy mappát. Miután a mappák bal oldalán levõ kis négyzetekben 
-    megjelölte a megfelelõket, válassza ki a megfelelõ funkciót a 
-    "Mûveletek" legördülõ menübõl. 
-    Ha az "Áthelyezés a vágóasztalra" vagy a 
-    "Másolás a vágóasztalra" mûveletet választotta, akkor megjelenik 
-    a "Beillesztés a vágóasztalról" funkció. Keresse meg 
+    Alapértelmezett beállítás esetén csak olyan mappákat törölhet, melyek üresek, bár
+    az opciókban beállíthatja a nemüres mappák (rekurzív) törölhetõségét. Enélkül viszont
+    ha megkísérel kitörölni egy nemüres mappát, akkor ez nem fog sikerülni, hibajelzést 
+    kap. A nemüres mappák rekurzív törléséhez kattintson az Opciókra, majd itt a 
+    Beállítások pontra, ahol már beállítható a kívánt lehetõség.
+    Ha mappákat kíván törölni, áthelyezni, vagy másolni, akkor elõször válasszon ki 
+    legalább egy mappát. Miután a mappák bal oldalán levõ kis négyzetekben megjelölte 
+    a megfelelõket, válassza ki a megfelelõ funkciót a 'Mûveletek' legördülõ menübõl. 
+    Ha az 'Áthelyezés a vágóasztalra' vagy a 'Másolás a vágóasztalra' mûveletet 
+    választotta, akkor megjelenik a 'Beillesztés a vágóasztalról' funkció. Keresse meg 
     azt a mappát ahova be kívánja illeszteni a mappákat, majd válassza ki a 
-    "Beillesztés a vágóasztalról" funkciót a "Mûveletek" 
-    legördülõ menübõl. A korábban kiválasztott mappák vagy átkerülnek 
-    (áthelyzezés esetén) vagy átmásolódnak (másolás esetén) az új mappába.
+    'Beillesztés a vágóasztalról' funkciót a 'Mûveletek' legördülõ menübõl. 
+    A korábban kiválasztott mappák vagy átkerülnek (áthelyzezés esetén) vagy 
+    átmásolódnak (másolás esetén) az új mappába.
     </para>
-
-    <heading>A vágóasztal használata</heading>
+<!-- English entry:
+<entry id="file-actions">
+    <title>File Manager: Actions</title>
+    <heading>Creating Folders</heading>
     <para>
-    Az "Áthelyezés a vágóasztalra" ill. a 
-    "Másolás a vágóasztalra" mûveletek elvégzése után a kiválasztott
-    állományok ill. mappák a vágóasztalra kerülnek. Ennek tartalma megjeleníthetõ.
-    Ehhez a vágóasztal ikonjára kell kattintani, ami az állomány- ill. mappalista
-    tetején, a frissítés gombtól jobbra van. Ez az ikon csak akkor jelenik meg,
-    ha a vágóasztal ténylegesen tartalmaz valamit. A szokásos állománylistában 
-    hiányozni fog a kis kijelölõ négyzet azon állományok sorában, melyeket
-    áthelyeztünk vagy átmásoltunk a vágóasztalra. Ha rákattintunk a vágóasztal
-    ikonjára, akkor megjelenik azoknak az állományoknak a listája, melyeket a
-    vágóasztal éppen tartalmaz. Ezek az állományok beilleszthetõk innen az 
-    aktuálisan érvényes alkönyvtárba mely a lista fölött látható. Ehhez 
-    egyszerûen ki kell jelölni a kívánt állományokat és a 
-    "Beillesztés"-re kell kattintani. Erre a kijelölt állományok 
-    bekerülnek az aktuálisan érvényes alkönyvtárba, és törlõdnek a vágóasztalról.
-    A beillesztést követõen visszakerül az aktuálisan érvényes alkönyvtárba.
-    Ha törölni óhajt egy állományt a vágóasztalról, akkor jelölje ki és 
-    kattintson a "Törlés"-re. Erre törlõdnek a kijelölt állományok
-    és visszakerül az aktuálisan érvényes alkönyvtárba.
+    To create a folder in the current directory, simply choose Create Folder from the More Actions drop down box.  
     </para>
-  </entry>
-
-  <entry id="file-upload" state="uptodate" md5="f07834ebb15641cdc4e349ed8d304d75">
-    <title>Fájlkezelõ: Feltöltés</title>
-    <heading>Fájlok feltöltése</heading>
+    <heading>Delete, cut, copy and paste files</heading>
+    <para>
+    In order to delete, cut or copy a file or files, you need to select at least one file.
+    After you select one or more files using the checkboxes to the left of the item, select either Cut Items, Copy Items or 
+    Delete Items from the More Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
+    paste those files elsewhere in the File Manager.  Navigate to the location that you wish to paste the items, then select 
+    Paste Items from the More Actions drop down box.  The items will now be either moved (Cut Items) or copied (Copy Items) to 
+    the location that you chose.
+    </para>
+    <heading>Delete, cut, copy and paste directories</heading>
+    <para>	
+    By default, you can only delete directories that are empty, however you can enable the deletion of non-empty directories 
+    (recursive delete).  If you try to delete a non-empty directory without enabling this feature, deletion will fail with 
+    an error.  To enable the deleting non-empty directories, click on Options, Settings and choose Yes from the drop down box 
+    for the Delete Folders Recursively option.  This will enable the recursive delete option.
+    
+    In order to delete, cut or copy a directory or directories, you need to select at least one directory.
+    After you select one or more directories using the checkboxes to the left of the item, select either Cut Items, Copy Items 
+    or Delete Items from the More Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
+    paste those directories elsewhere in the File Manager.  Navigate to the location that you wish to paste the items, then 
+    select Paste Items from the More Actions drop down box.  The items will now either be moved (Cut Items) or copied (Copy Items)
+    to the location that you chose.
+    </para>
+    <heading>Working with the Clipboard</heading>
     <para>
-    Ha egy vagy több fájlt kíván a szerverre feltölteni, elõször keresse meg a 
-    Fájlkezelõben azt a mappát, ahova az állományokat el kívánja helyezni. Ez 
-    lesz az aktuálisan érvényes alkönyvtár. Ezután kattintson a 
-    "Böngészés" (Browse) gombra, majd a felbukkanó Fájlfeltöltés 
-    (File upload) ablakban keresse meg és jelölje ki (a saját gépén) a 
-    feltöltendõ fájlt és kattintson az OK (esetleg Megnyitás/Open)
-    gombra. Egyszerre több fájlt is feltölthet. Minden fájlkiválasztás után a 
-    Fájlkezelõ egy újabb sorban új "Böngészés" (Browse) gombot 
-    jelenít meg. Ezt használva több fájl is kijelölhetõ feltöltésre. Miután 
-    kijelölte a feltöltendõ fájl(oka)t, kattintson a Fájlfeltöltés gombra. 
-    Erre a kijelölt állományok a szerverre fognak kerülni, a Fájlkezelõben 
-    korábban kiválasztott mappába.
+    Once you copy or cut a file or directory, it is placed in the clipboard.  You can view the contents of the clipboard by clicking
+    on the clipboard icon, which is located above the directory/folder listing and to the right of the refresh icon.  This icon is not 
+    present unless there are items in the clipboard.  Items that are cut or copied will not have a checkbox to the left of them, which 
+    means they are in the clipboard.  Once you click on the clipboard viewer, you will see a list of items that you have copied or cut.
+    From the clipboard viewer, you can paste these items to the current directory, which is displayed above the list of items.
+    Simply check the items that you want to paste, then click paste.  These items will be pasted into the current directory and be removed
+    from the clipboard.  You will be returned to the current directory after the paste is complete.  If you would like to remove an item 
+    from the clipboard, simply check the items and click clear.  These items will then be removed from the clipboard and you will be returned 
+    to the current directory.  
     </para>
-  </entry>
-
-  <entry id="sorting" state="uptodate" md5="0ce2080b01acce2e9affc2191fc0386c">
+</entry>--></entry>
+  <entry id="file-upload" state="unknown">
+    <title>Fájl menedzser: Feltöltés</title>
+    <heading>Fájlok feltöltése</heading>
+    <para>
+    Ha egy vagy több fájlt kíván a szerverre feltölteni, elõször keresse meg a Fájl 
+    menedzserben azt a mappát, ahova az állományokat el kívánja helyezni. Ez lesz az 
+    éppen érvényes alkönyvtár. Ezután kattintson a 'Böngészés' (Browse) gombra, majd
+    a felbukkanó Fájlfeltöltés (File upload) ablakban keresse meg és jelölje ki 
+    (a saját gépén) a feltöltendõ fájlt és kattintson az OK (esetleg Megnyitás/Open)
+    gombra. Egyszerre több fájlt is feltölthet. Minden fájlkiválasztás után a Fájl
+    menedzser egy újabb sorban új 'Böngészés' (Browse) gombot jelenít meg. Ezt használva
+    több fájl is kijelölhetõ feltöltésre. Miután kijelölte a feltöltendõ fájl(oka)t,
+    kattintson a Fájlfeltöltés gombra. Erre a kijelölt állományok a szerverre fognak
+    kerülni, a Fájl menedzserben korábban kiválasztott mappába.
+</para>
+<!-- English entry:
+<entry id="file-upload">
+    <title>File Manager: Uploading</title>
+    <heading>Uploading Files</heading>
+    <para>
+    To upload a file or files, navigate to the directory on the File Manager that you wish to upload the files to.  This is 
+    known as your current directory.  Once you are in the proper directory on the File Manager, click on the Browse button.
+    This will display a File Upload window.  Once this window is open, navigate to the file which you wish to upload, select 
+    it and click OK.  You can upload multiple files at once.  The File Manager will automatically add another 
+    Browse button after each file you select.  Once you have selected all of the files that you wish to upload, click the 
+    Upload File(s) button and the file or files that you selected will be uploaded to your current directory on the File Manager.
+</para>
+</entry>--></entry>
+  <entry id="sorting" state="unknown">
     <title>Sorbarendezés</title>
     <heading>Sorbarendezés</heading>
     <para>
-    Az állományok listázáskor bármely oszlop szerint sorbarendezhetõk. Ehhez 
-    mindössze a megfelelõ oszlop fejlécére kell kattintania. Erre a lista 
-    újra megjelenik, mégpedig a kívánt oszlop szerinti sorrendben. Ha a 
-    sorbarendezés irányát kívánja megváltoztatni, akkor kattintson a fejlécben 
-    levõ kis háromszög alakú nyílra.
+    Az állományok listázáskor bármely oszlop szerint sorbarendezhetõk. Ehhez mindössze
+    a megfelelõ oszlop fejlécére kell kattintania. Erre a lista újra megjelenik, mégpedig
+    a kívánt oszlop szerinti sorrendben. Ha a sorbarendezés irányát kívánja megváltoztatni,
+    akkor kattintson a fejlécben levõ kis háromszög alakú nyílra.
+    </para>
+<!-- English entry:
+<entry id="sorting">
+    <title>Sorting Entries</title>
+    <heading>Sorting Entries</heading>
+    <para>
+    When viewing a list of files or directories, you can sort them by any column.  To do this, click on the appropriate 
+    column heading that you wish to sort by, and the list will refreshed, sorted by the column you selected.  To switch 
+    columns between ascending and descending order, click on the arrow icon in the column heading.
     </para>
-  </entry>
+</entry>--></entry>
 </help>
diff --git a/locale/it_IT/LC_MESSAGES/gollem.mo b/locale/it_IT/LC_MESSAGES/gollem.mo
index 22adef8..352203a 100644
Binary files a/locale/it_IT/LC_MESSAGES/gollem.mo and b/locale/it_IT/LC_MESSAGES/gollem.mo differ
diff --git a/locale/ko_KR/LC_MESSAGES/gollem.mo b/locale/ko_KR/LC_MESSAGES/gollem.mo
index 31ae2ee..00ca17c 100644
Binary files a/locale/ko_KR/LC_MESSAGES/gollem.mo and b/locale/ko_KR/LC_MESSAGES/gollem.mo differ
diff --git a/locale/lv_LV/LC_MESSAGES/gollem.mo b/locale/lv_LV/LC_MESSAGES/gollem.mo
index 497d812..0b7cd5f 100644
Binary files a/locale/lv_LV/LC_MESSAGES/gollem.mo and b/locale/lv_LV/LC_MESSAGES/gollem.mo differ
diff --git a/locale/lv_LV/help.xml b/locale/lv_LV/help.xml
new file mode 100644
index 0000000..3ae37e5
--- /dev/null
+++ b/locale/lv_LV/help.xml
@@ -0,0 +1,57 @@
+<?xml version='1.0' encoding='cp1257'?>
+<!-- $Horde: gollem/locale/lv_LV/help.xml,v 1.1.2.2 2008/10/09 20:54:51 jan Exp $ -->
+<help>
+
+<entry id="file-actions">
+    <title>Failu pârvaldnieks: Darbîbas</title>
+    <heading>Mapju izveidoðana</heading>
+    <para>
+    Lai izveidotu mapi aktuâlajâ katalogâ, uzklikðíiniet ikonai <b>Izveidot mapi</b> Failu pârvaldnieka rîkjoslâ.  
+    </para>
+    <heading>Failu dzçðana, izgrieðana, kopçðana, ielîmçðana</heading>
+    <para>
+    Lai dzçstu, izgrieztu vai nokopçtu failu(s), ir jâizvçlas vismaz viens fails, atzîmçjot to(s) izvçles rûtiòâ(s) pa kreisi no faila nosaukuma. Pçc tam, kad ir atzîmçti nepiecieðami faili, no izkrîtoðâs komandkartes <b>Darbîbas</b> izvçlieties nepiecieðamo darbîbu. Ja izvçlçjâties <b>Kopçt</b> vai <b>Izgriezt</b>, atzîmçtie faili tiks nokopçti vai pârvietoti uz Starpliktuvi un lietotâja saskarnç parâdîsies jauna ikona <b>Skatît starpliktuvi</b>. No Starpliktuves Jums ir iespçja ielîmçt atbilstoðos failus jebkurâ Failu pârvaldnieka vietâ. Pârejiet uz vietu direktoriju kokâ, kurâ nepiecieðams novietot failu(s), atveriet Starpliktuvi, atzîmçjiet nepiecieðamo(s) failu(s) rûtiòâ(s) pa kreisi no faila nosaukuma un uzklikðíiniet uz <b>Ielîmçt</b>. Pçc ðîs darbîbas faili tiks ielîmçti aktuâlajâ katalogâ un Starpliktuve – attîrîta.   
+    </para>
+    <heading>Katalogu dzçðana, izgrieðana, kopçðana, ielîmçðana</heading>
+    <para>	
+Pçc noklusçðanas, izdzçst katalogu iespçjams tikai gadîjumâ, ja tajâ nav failu. Ja vçlaties dzçst katalogus ar visu to saturu, varat ieslçgt rekursîvu katalogu dzçðanu. Lai to ieslçgtu, uzklikðíiniet ikonai <b>Opcijas</b>, izvçlieties <b>Iestatîjumi</b> un  no izkrîtoðajâ komandkartç piedâvâtajâm iespçjâm - Jums vçlamo variantu. 
+Ja mçìinâsiet dzçst katalogu ar failiem, neieslçdzot minçto opciju, dzçðanas mçìinâjums beigsies ar kïûdu.
+
+Lai dzçstu, izgrieztu vai nokopçtu katalogu(s), ir jâizvçlas vismaz viens katalogs, atzîmçjot to(s) izvçles rûtiòâ(s) pa kreisi no kataloga nosaukuma. Pçc tam, kad ir atzîmçti nepiecieðami katalogi, no izkrîtoðâs komandkartes <b>Darbîbas</b> izvçlieties nepiecieðamo darbîbu. Ja izvçlçjâties <b>Kopçt</b> vai <b>Izgriezt</b>, atzîmçtie katalogi tiks nokopçti vai pârvietoti uz Starpliktuvi un lietotâja saskarnç parâdîsies jauna ikona <b>Skatît starpliktuvi</b>. No Starpliktuves Jums ir iespçja ielîmçt atbilstoðos katalogus jebkurâ katalogu pârvaldnieka vietâ. Pârejiet uz vietu direktoriju kokâ, kurâ nepiecieðams novietot katalogu(s), atveriet Starpliktuvi, atzîmçjiet nepiecieðamo(s) katalogu(s) rûtiòâ(s) pa kreisi no kataloga nosaukuma un uzklikðíiniet uz <b>Ielîmçt</b>. Pçc ðîs darbîbas katalogi tiks ielîmçti aktuâlajâ katalogâ un Starpliktuve – attîrîta.   
+    
+    </para>
+    <heading>Darbs ar starpliktuvi</heading>
+    <para>
+Ja Jûs kopçjat vai izgreiþat failu vai katalogu, tas tiek novietots starpliktuvç, kuras saturu varat apskatît, uzklikðíinot ikonai <b>Starpliktuve</b>, kas atrodas virs failu un katalogu saraksta pa labio no <b>Atsvaidzinât skatu</b> ikonas.
+Ðî ikona parâdâs lietotâja saskarnç tikai tad, kad starpliktuvç atrodas kâds objekts.Pçc tam, kad kâds fails vai katalogs ir nokopçts vai izgriezts, blakus tâ tâ nosaukumam Failu pârvaldnieka galvenajâ logâ vairs nebûs izvçles rûtiòa.
+</para>
+<para>
+Kad atvçrsiet Starpliktuves pârlûku, redzçsiet uz starpliktuvi kopçto vai izgriezto objektu sarakstu, kur katram objektam blakus ir attçlota iepriekð veikto darbîbu atainojoða piktogramma. No ðejienes Jûs varat ielîmçt objektus aktuâlajâ katalogâ, kas parâdîts virs objektu saraksta – atzîmçjiet ielîmçjamos failus un nospiediet pogu <b>Ielîmçt</b>, objekti tiks ievietoti aktuâlajâ katalogâ un notîrîti no starpliktuves. Pçc objektu ielîmçðanas Failu pârvaldnieks atgriezîsies aktuâlajâ katalogâ. Ja vçlaties vienkârði notîrît kâdu objektu no starpliktuves, atzîmçjiet to un nospiediet pogu <b>Notîrît</b>. Objekts(-i) tiks notîrîti no starpliktuves un Failu  pârvaldnieks atgriezîsies aktuâlajâ katalogâ.
+</para>
+<para>
+Piezîme: ja objekts tika izgriezts uz starpliktuvi, pçc notîrîðanas no starpliktuves objekts paliek savâ iepriekðçjâ vietâ.
+    </para>
+</entry>
+
+<entry id="file-upload">
+    <title>Failu pârvaldnieks: Augðupielâdçðana</title>
+    <heading>Failu augðupielâdçðana</heading>
+    <para>
+Piezîme: failu augðupielâdçðana darbojas tikai gadîjumâ, ja ir pareizi konfigurçtas VFS palîgmoduïa pieejas tiesîbas. To var izdarît tikai sistçmas administrators.
+    </para>
+    <para>
+     Lai augðupielâdçtu failus, pârejiet uz vietu katalogu kokâ, kurâ vçlaties tos novietot. Tas bûs aktuâlais katalogs. Kad esat atvçruði vajadzîgo katalogu, Failu pârvaldnieka loga apakðâ nospiediet pogu <b>Browse...</b> (pârlûkot). Tas atvçrs <b>File Upload</b> logu, kurâ Jums jâsameklç augðupielâdçjamais fails, jâuzklikðíina tâ nosaukumam un <b>OK</b> pogai.
+</para>
+<para>
+Vienlaikus ir iespçjams augðupielâdçt vairâkus failus – Failu pârvaldnieks automâtiski pievienos jaunu <b>Browse...</b> pogu pçc katra faila izvçles. Kad ir sameklçti visi faili, nospiediet pogu <b>Augðupielâdçt failu(s)</b> un tie tiks augðupielâdçti aktuâlajâ Failu pârvaldnieka katalogâ.
+</para>
+</entry>
+
+<entry id="sorting">
+    <title>Ðíiroðana</title>
+    <heading>Ðíiroðana</heading>
+    <para>
+    Apskatot failu vai katalogu sarakstus, Jûs varat to sakârtot pçc jebkuras slejas. Lai to izdarîtu, uzklikðíiniet atbilstoðâs slejas virsrakstam un saraksts tiks atsvaidzinâts, sakârtojot objektus atbilstoði Jûsu izvçlçtajai slejai. Lai mainîtu kârtoðanas secîbu no augoðas uz dilstoðu un otrâdi, uzklikðíiniet slejas virsrakstam blakus esoðajai bultiòai.
+    </para>
+</entry>
+</help>
\ No newline at end of file
diff --git a/locale/nl_NL/LC_MESSAGES/gollem.mo b/locale/nl_NL/LC_MESSAGES/gollem.mo
index 3f67a62..be7d385 100644
Binary files a/locale/nl_NL/LC_MESSAGES/gollem.mo and b/locale/nl_NL/LC_MESSAGES/gollem.mo differ
diff --git a/locale/nn_NO/LC_MESSAGES/gollem.mo b/locale/nn_NO/LC_MESSAGES/gollem.mo
index 813103e..83ae8e4 100644
Binary files a/locale/nn_NO/LC_MESSAGES/gollem.mo and b/locale/nn_NO/LC_MESSAGES/gollem.mo differ
diff --git a/locale/pl_PL/LC_MESSAGES/gollem.mo b/locale/pl_PL/LC_MESSAGES/gollem.mo
index 0c8547d..4380349 100644
Binary files a/locale/pl_PL/LC_MESSAGES/gollem.mo and b/locale/pl_PL/LC_MESSAGES/gollem.mo differ
diff --git a/locale/pt_BR/LC_MESSAGES/gollem.mo b/locale/pt_BR/LC_MESSAGES/gollem.mo
index 7dad167..5b20c7a 100644
Binary files a/locale/pt_BR/LC_MESSAGES/gollem.mo and b/locale/pt_BR/LC_MESSAGES/gollem.mo differ
diff --git a/locale/ro_RO/LC_MESSAGES/gollem.mo b/locale/ro_RO/LC_MESSAGES/gollem.mo
index 958c45e..c502b82 100644
Binary files a/locale/ro_RO/LC_MESSAGES/gollem.mo and b/locale/ro_RO/LC_MESSAGES/gollem.mo differ
diff --git a/locale/ru_RU/LC_MESSAGES/gollem.mo b/locale/ru_RU/LC_MESSAGES/gollem.mo
index 68388d3..59c42ff 100644
Binary files a/locale/ru_RU/LC_MESSAGES/gollem.mo and b/locale/ru_RU/LC_MESSAGES/gollem.mo differ
diff --git a/locale/sk_SK/LC_MESSAGES/gollem.mo b/locale/sk_SK/LC_MESSAGES/gollem.mo
new file mode 100644
index 0000000..c8e2fa8
Binary files /dev/null and b/locale/sk_SK/LC_MESSAGES/gollem.mo differ
diff --git a/locale/sl_SI/LC_MESSAGES/gollem.mo b/locale/sl_SI/LC_MESSAGES/gollem.mo
index d59bfec..cc087da 100644
Binary files a/locale/sl_SI/LC_MESSAGES/gollem.mo and b/locale/sl_SI/LC_MESSAGES/gollem.mo differ
diff --git a/locale/sv_SE/LC_MESSAGES/gollem.mo b/locale/sv_SE/LC_MESSAGES/gollem.mo
index 630deb9..8bd07ef 100644
Binary files a/locale/sv_SE/LC_MESSAGES/gollem.mo and b/locale/sv_SE/LC_MESSAGES/gollem.mo differ
diff --git a/locale/tr_TR/LC_MESSAGES/gollem.mo b/locale/tr_TR/LC_MESSAGES/gollem.mo
new file mode 100644
index 0000000..e803a50
Binary files /dev/null and b/locale/tr_TR/LC_MESSAGES/gollem.mo differ
diff --git a/locale/uk_UA/LC_MESSAGES/gollem.mo b/locale/uk_UA/LC_MESSAGES/gollem.mo
new file mode 100644
index 0000000..eb11a14
Binary files /dev/null and b/locale/uk_UA/LC_MESSAGES/gollem.mo differ
diff --git a/locale/zh_TW/LC_MESSAGES/gollem.mo b/locale/zh_TW/LC_MESSAGES/gollem.mo
index 94b64e7..efd7700 100644
Binary files a/locale/zh_TW/LC_MESSAGES/gollem.mo and b/locale/zh_TW/LC_MESSAGES/gollem.mo differ
diff --git a/locale/zh_TW/help.xml b/locale/zh_TW/help.xml
index b5063ec..4dbcda7 100644
--- a/locale/zh_TW/help.xml
+++ b/locale/zh_TW/help.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="Big5"?>
-<!-- $Horde: gollem/locale/zh_TW/help.xml,v 1.3 2005/08/03 11:58:56 jan Exp $ -->
+<!-- $Horde: gollem/locale/zh_TW/help.xml,v 1.3.2.1 2008/10/09 20:54:55 jan Exp $ -->
 <help>
   <entry id="file-actions" md5="a422465ce502865cd4625fbbb8d9a4dc" state="changed">
     <title>ÀÉ®×Á`ºÞ: §@¥Î¤è¦¡</title>
@@ -81,6 +81,48 @@
     from the clipboard, simply check the items and click clear.  These items will then be removed from the clipboard and you will be returned 
     to the current directory.  
     </para>
+</entry>--><!-- English entry:
+<entry id="file-actions">
+    <title>File Manager: Actions</title>
+    <heading>Creating Folders</heading>
+    <para>
+    To create a folder in the current directory, simply choose Create Folder from the Actions drop down box.  
+    </para>
+    <heading>Delete, cut, copy and paste files</heading>
+    <para>
+    In order to delete, cut or copy a file or files, you need to select at least one file.
+    After you select one or more files using the checkboxes to the left of the item, select either Cut Items, Copy Items or 
+    Delete Items from the Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
+    paste those files elsewhere in the File Manager.  Navigate to the location that you wish to paste the items, then select 
+    Paste Items from the Actions drop down box.  The items will now be either moved (Cut Items) or copied (Copy Items) to 
+    the location that you chose.
+    </para>
+    <heading>Delete, cut, copy and paste directories</heading>
+    <para>	
+    By default, you can only delete directories that are empty, however you can enable the deletion of non-empty directories 
+    (recursive delete).  If you try to delete a non-empty directory without enabling this feature, deletion will fail with 
+    an error.  To enable the deleting non-empty directories, click on Options, Settings and choose Yes from the drop down box 
+    for the Delete Folders Recursively option.  This will enable the recursive delete option.
+    
+    In order to delete, cut or copy a directory or directories, you need to select at least one directory.
+    After you select one or more directories using the checkboxes to the left of the item, select either Cut Items, Copy Items 
+    or Delete Items from the Actions drop down box.  If you selected Cut Items or Copy Items, you now have the option to 
+    paste those directories elsewhere in the File Manager.  Navigate to the location that you wish to paste the items, then 
+    select Paste Items from the Actions drop down box.  The items will now either be moved (Cut Items) or copied (Copy Items)
+    to the location that you chose.
+    </para>
+    <heading>Working with the Clipboard</heading>
+    <para>
+    Once you copy or cut a file or directory, it is placed in the clipboard.  You can view the contents of the clipboard by clicking
+    on the clipboard icon, which is located above the directory/folder listing and to the right of the refresh icon.  This icon is not 
+    present unless there are items in the clipboard.  Items that are cut or copied will not have a checkbox to the left of them, which 
+    means they are in the clipboard.  Once you click on the clipboard viewer, you will see a list of items that you have copied or cut.
+    From the clipboard viewer, you can paste these items to the current directory, which is displayed above the list of items.
+    Simply check the items that you want to paste, then click paste.  These items will be pasted into the current directory and be removed
+    from the clipboard.  You will be returned to the current directory after the paste is complete.  If you would like to remove an item 
+    from the clipboard, simply check the items and click clear.  These items will then be removed from the clipboard and you will be returned 
+    to the current directory.  
+    </para>
 </entry>--></entry>
   <entry id="file-upload" md5="90de3164e835816348ab7659f7bf28a7" state="changed">
     <title>ÀÉ®×Á`ºÞ: ¤W¶ÇÀÉ®×</title>
@@ -113,6 +155,18 @@
     Browse button after each file you select.  Once you have selected all of the files that you wish to upload, click the 
     Upload File(s) button and the file or files that you selected will be uploaded to your current directory on the File Manager.
 </para>
+</entry>--><!-- English entry:
+<entry id="file-upload">
+    <title>File Manager: Uploading</title>
+    <heading>Uploading Files</heading>
+    <para>
+    To upload a file or files, navigate to the directory on the File Manager that you wish to upload the files to.  This is 
+    known as your current directory.  Once you are in the proper directory on the File Manager, click on the Browse button.
+    This will display a File Upload window.  Once this window is open, navigate to the file which you wish to upload, select 
+    it and click OK.  You can upload multiple files at once.  The File Manager will automatically add another 
+    Browse button after each file you select.  Once you have selected all of the files that you wish to upload, click the 
+    Upload File(s) button and the file or files that you selected will be uploaded to your current directory on the File Manager.
+</para>
 </entry>--></entry>
   <entry id="sorting" md5="7bbea5e84fddaffa02edae9e705f7fb1" state="changed">
     <title>±Æ¦C¶µ¥Ø</title>
@@ -138,5 +192,14 @@
     column heading that you wish to sort by, and the list will refreshed, sorted by the column you selected.  To switch 
     columns between ascending and descending order, click on the arrow icon in the column heading.
     </para>
+</entry>--><!-- English entry:
+<entry id="sorting">
+    <title>Sorting Entries</title>
+    <heading>Sorting Entries</heading>
+    <para>
+    When viewing a list of files or directories, you can sort them by any column.  To do this, click on the appropriate 
+    column heading that you wish to sort by, and the list will refreshed, sorted by the column you selected.  To switch 
+    columns between ascending and descending order, click on the arrow icon in the column heading.
+    </para>
 </entry>--></entry>
 </help>
diff --git a/login.php b/login.php
index 2b8027c..253689c 100644
--- a/login.php
+++ b/login.php
@@ -2,20 +2,22 @@
 /**
  * Login screen for Gollem.
  *
- * $Horde: gollem/login.php,v 1.94.2.13 2007/01/02 13:54:52 jan Exp $
+ * $Horde: gollem/login.php,v 1.94.2.17 2009/03/19 14:43:18 jan Exp $
  *
- * Copyright 1999-2007 Charles J. Hagenbuch <chuck at horde.org>
- * Copyright 1999-2007 Max Kalika <max at horde.org>
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
  *
- * See the enclosed file COPYING for license information (GPL).  If you
+ * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Max Kalika <max at horde.org>
+ * @author Chuck Hagenbuch <chuck at horde.org>
  */
 
-function _notificationOutput(&$template)
+function _notificationOutput(&$t)
 {
     ob_start();
     $GLOBALS['notification']->notify(array('listeners' => 'status'));
-    $template->set('notification_output', ob_get_contents());
+    $t->set('notification_output', ob_get_contents());
     ob_end_clean();
 }
 
@@ -133,27 +135,30 @@ if ($gollem_auth) {
     Secret::setKey('auth');
 }
 /* Prepare the login template. */
-require_once 'Horde/Template.php';
-$template = &new Horde_Template();
-$template->setOption('gettext', true);
+$t = new Gollem_Template();
+$t->setOption('gettext', true);
 
 require_once 'Horde/Menu.php';
-$menu = &new Menu(HORDE_MENU_MASK_NONE);
-$template->set('menu', $menu->render(), true);
-$template->set('title', sprintf(_("%s Login"), $registry->get('name')));
+$menu = new Menu(HORDE_MENU_MASK_NONE);
+$t->set('menu', $menu->render(), true);
+$t->set('title', sprintf(_("%s Login"), $registry->get('name')));
 
 if (!$backend_key) {
     $backend_key = Gollem::getPreferredBackend();
     /* If there is no backend key defined, there is no available backends for
      * the current user. */
-    if (is_null($backend_key)) {
-        $reason = _("There are no backends available for the current user.");
+    if ($backend_key === null) {
+        $notification->push(_("There are no backends available for the current user."), 'horde.error');
         require GOLLEM_TEMPLATES . '/common-header.inc';
-        _notificationOutput($template);
-        $template->set('allowlogin', false, true);
-        echo $template->fetch(GOLLEM_TEMPLATES . '/login/login.html');
-        if (@is_readable(GOLLEM_BASE . '/config/motd.php')) {
-            require GOLLEM_BASE . '/config/motd.php';
+        _notificationOutput($t);
+        $t->set('allowlogin', false, true);
+        echo $t->fetch(GOLLEM_TEMPLATES . '/login/login.html');
+        if (is_callable(array('Horde', 'loadConfiguration'))) {
+            Horde::loadConfiguration('motd.php', null, null, true);
+        } else {
+            if (is_readable(GOLLEM_BASE . '/config/motd.php')) {
+                require GOLLEM_BASE . '/config/motd.php';
+            }
         }
         require $registry->get('templates', 'horde') . '/common-footer.inc';
         exit;
@@ -162,11 +167,12 @@ if (!$backend_key) {
 
 $login_vfs = array();
 foreach ($GLOBALS['gollem_backends'] as $key => $curBackend) {
-    $login_vfs[$key] = &VFS::singleton($curBackend['driver'], array_merge($curBackend['params'], array('user' => Auth::getAuth())));
+    $login_vfs[$key] = &Gollem::getVFSOb($key, array_merge($curBackend['params'], array('user' => Auth::getAuth())));
     if (is_a($login_vfs[$key], 'PEAR_Error')) {
         Horde::fatal($login_vfs[$key]);
     }
 }
+// Make the VFS global
 $GLOBALS['gollem_vfs'] = $login_vfs[$backend_key];
 
 /* If we only have one backend, and it doesn't require authentication, skip
@@ -180,13 +186,16 @@ if (!$logout_reason && !$autologin_fail) {
             $redirect_params['autologin'] = 1;
         }
     } elseif (Util::getFormData('change_backend') &&
-              empty($GLOBALS['gollem_backends'][$backend_key]['loginparams'])) {
+              Gollem::canAutoLogin($backend_key, true)) {
         $redirect_params['autologin'] = 1;
     }
 }
 
 if (!empty($redirect_params)) {
     $redirect_params += array('actionID' => 'login', 'backend_key' => $backend_key);
+    if ($url_param) {
+        $redirect_params['url'] = $url_param;
+    }
     $url = Util::addParameter(Horde::applicationUrl('redirect.php', true), $redirect_params, null, false);
     header('Location: ' . $url);
     exit;
@@ -200,10 +209,9 @@ if ($logout_reason && $gollem_auth && $conf['menu']['always']) {
     $notification->push('setFocus()', 'javascript');
 }
 
-$reason = $auth->getLogoutReasonString();
-
-/* Add some javascript. */
-Horde::addScriptFile('enter_key_trap.js', 'horde', true);
+if ($reason = $auth->getLogoutReasonString()) {
+    $notification->push(str_replace('<br />', ' ', $reason), 'horde.message');
+}
 
 /* Do we need to do IE version detection? */
 if (!Auth::getAuth() &&
@@ -213,31 +221,30 @@ if (!Auth::getAuth() &&
 }
 
 $choose_language = ($gollem_auth && !$prefs->isLocked('language'));
+$lang_url = null;
 if ($choose_language) {
     $lang_url = (!empty($url_param)) ? urlencode($url_param) : null;
 }
 
-require GOLLEM_TEMPLATES . '/common-header.inc';
-require_once GOLLEM_TEMPLATES . '/login/javascript.inc';
-
 $tabindex = 0;
-$template->set('allowlogin', true, true);
-
-$template->set('action', Horde::url('redirect.php', false, -1, true));
-$template->set('gollem_auth', $gollem_auth, true);
-$template->set('form_input', Util::formInput());
-$template->set('actionid', $actionID);
-$template->set('load_frameset', ($gollem_auth) ? 1 : 0);
-$template->set('url', htmlspecialchars($url_param));
-$template->set('autologin', ($autologin || Gollem::canAutoLogin($backend_key, true)) ? 1 : 0);
-$template->set('clientcaps', !empty($ie_clientcaps), true);
-$template->set('backend_key', ($conf['backend']['backend_list'] != 'shown') ? $backend_key : null, true);
-
-_notificationOutput($template);
-
-$template->set('backend_shown', ($conf['backend']['backend_list'] == 'shown'), true);
-if ($template->get('backend_shown')) {
-    $template->set('shown_tabindex', ++$tabindex);
+$t->set('allowlogin', true, true);
+
+$t->set('action', Horde::url('redirect.php', false, -1, true));
+$t->set('gollem_auth', $gollem_auth, true);
+$t->set('form_input', Util::formInput());
+$t->set('actionid', $actionID);
+$t->set('load_frameset', intval($gollem_auth));
+$t->set('url', htmlspecialchars($url_param));
+$t->set('autologin', ($autologin || Gollem::canAutoLogin($backend_key, true)) ? 1 : 0);
+$t->set('clientcaps', !empty($ie_clientcaps), true);
+$t->set('anchor_string', htmlspecialchars(Util::getFormData('anchor_string')));
+$t->set('backend_key', ($conf['backend']['backend_list'] != 'shown') ? $backend_key : null, true);
+
+_notificationOutput($t);
+
+$t->set('backend_shown', ($conf['backend']['backend_list'] == 'shown'), true);
+if ($t->get('backend_shown')) {
+    $t->set('shown_tabindex', ++$tabindex);
     $shown_array = array();
     foreach ($GLOBALS['gollem_backends'] as $key => $curBackend) {
         $shown_array[] = array(
@@ -246,12 +253,12 @@ if ($template->get('backend_shown')) {
             'val' => $key
         );
     }
-    $template->set('shown_array', $shown_array, true);
+    $t->set('shown_array', $shown_array, true);
 }
 
-$template->set('backend_hidden', ($conf['backend']['backend_list'] == 'hidden'), true);
-if ($template->get('backend_hidden')) {
-    $template->set('hidden_text', sprintf(_("Connect to: %s"), $GLOBALS['gollem_backends'][$backend_key]['name']));
+$t->set('backend_hidden', ($conf['backend']['backend_list'] == 'hidden'), true);
+if ($t->get('backend_hidden')) {
+    $t->set('hidden_text', sprintf(_("Connect to: %s"), $GLOBALS['gollem_backends'][$backend_key]['name']));
 }
 
 $loginparams = array();
@@ -265,7 +272,7 @@ if (!empty($GLOBALS['gollem_backends'][$backend_key]['loginparams'])) {
         );
     }
 }
-$template->set('loginparams', $loginparams);
+$t->set('loginparams', $loginparams);
 
 $log_creds = array();
 require GOLLEM_BASE . '/config/credentials.php';
@@ -282,11 +289,11 @@ foreach ($login_vfs[$backend_key]->getRequiredCredentials() as $credential) {
         );
     }
 }
-$template->set('log_creds', $log_creds);
+$t->set('log_creds', $log_creds);
 
-$template->set('choose_language', $choose_language, true);
+$t->set('choose_language', $choose_language, true);
 if ($choose_language) {
-    $template->set('langs_tabindex', ++$tabindex);
+    $t->set('langs_tabindex', ++$tabindex);
     $_SESSION['horde_language'] = NLS::select();
     $langs = array();
     foreach ($nls['languages'] as $key => $val) {
@@ -296,15 +303,31 @@ if ($choose_language) {
             'name' => $val
         );
     }
-    $template->set('langs', $langs, true);
+    $t->set('langs', $langs, true);
 }
 
-$template->set('login_tabindex', ++$tabindex);
-$template->set('login', _("Login"));
+$t->set('login_tabindex', ++$tabindex);
+$t->set('login', _("Login"));
+
+$js_code = array(
+    'var ie_clientcaps = ' . intval($t->get('clientcaps')),
+    'var gollem_auth = ' . intval($gollem_auth),
+    'var lang_url = ' . (is_null($lang_url) ? 'null' : '\'' . $lang_url . '\''),
+    'var nomenu = ' . intval(empty($conf['menu']['always'])),
+    'var reload_url = \'' . Util::addParameter(Horde::selfUrl(false, true, true), array('backend_key' => null), null, false) . '\'',
+);
 
-echo $template->fetch(GOLLEM_TEMPLATES . '/login/login.html');
+Horde::addScriptFile('prototype.js', 'gollem', true);
+Horde::addScriptFile('login.js', 'gollem', true);
+require GOLLEM_TEMPLATES . '/common-header.inc';
+Gollem::addInlineScript(implode(';', $js_code));
+echo $t->fetch(GOLLEM_TEMPLATES . '/login/login.html');
 
-if (@is_readable(GOLLEM_BASE . '/config/motd.php')) {
-    require GOLLEM_BASE . '/config/motd.php';
+if (is_callable(array('Horde', 'loadConfiguration'))) {
+    Horde::loadConfiguration('motd.php', null, null, true);
+} else {
+    if (is_readable(GOLLEM_BASE . '/config/motd.php')) {
+        require GOLLEM_BASE . '/config/motd.php';
+    }
 }
 require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/manager.php b/manager.php
index 5bf4b45..b93674f 100644
--- a/manager.php
+++ b/manager.php
@@ -1,23 +1,29 @@
 <?php
 /**
- * $Horde: gollem/manager.php,v 1.146.2.19 2007/01/02 13:54:52 jan Exp $
+ * $Horde: gollem/manager.php,v 1.146.2.23 2009/03/19 14:18:21 jan Exp $
  *
- * Copyright 1999-2007 Charles J. Hagenbuch <chuck at horde.org>
- * Copyright 1999-2007 Max Kalika <max at horde.org>
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
  *
- * See the enclosed file COPYING for license information (GPL).  If you
+ * See the enclosed file COPYING for license information (GPL). If you
  * did notcan receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Max Kalika <max at horde.org>
+ * @author Chuck Hagenbuch <chuck at horde.org>
  */
 
 @define('GOLLEM_BASE', dirname(__FILE__));
 require_once GOLLEM_BASE . '/lib/base.php';
-require_once 'Horde/MIME/Part.php';
-require_once 'Horde/Template.php';
+require_once 'Horde/Help.php';
 
 $actionID = Util::getFormData('actionID');
 $backkey = $_SESSION['gollem']['backend_key'];
 $old_dir = Gollem::getDir();
 
+/* Get permissions. */
+$delete_perms = Gollem::checkPermissions('backend', PERMS_DELETE);
+$edit_perms = Gollem::checkPermissions('backend', PERMS_EDIT);
+$read_perms = Gollem::checkPermissions('backend', PERMS_READ);
+
 /* Set directory. */
 if (is_a($result = Gollem::changeDir(), 'PEAR_Error')) {
     $notification->push($result);
@@ -26,7 +32,7 @@ if (is_a($result = Gollem::changeDir(), 'PEAR_Error')) {
 /* Run through the action handlers. */
 switch ($actionID) {
 case 'create_folder':
-    if (Gollem::checkPermissions('backend', PERMS_EDIT)) {
+    if ($edit_perms) {
         if ($new_folder = Util::getPost('new_folder')) {
             $result = Gollem::createFolder($old_dir, $new_folder);
             if (is_a($result, 'PEAR_Error')) {
@@ -39,12 +45,11 @@ case 'create_folder':
     break;
 
 case 'rename_items':
-    if (Gollem::checkPermissions('backend', PERMS_EDIT)) {
+    if ($edit_perms) {
         $new = explode('|', Util::getPost('new_names'));
         $old = explode('|', Util::getPost('old_names'));
         if (!empty($new) && !empty($old) && (count($new) == count($old))) {
-            $iMax = count($new);
-            for ($i = 0; $i < $iMax; $i++) {
+            for ($i = 0, $iMax = count($new); $i < $iMax; ++$i) {
                 $result = Gollem::renameItem($old_dir, $old[$i], $old_dir, $new[$i]);
                 if (is_a($result, 'PEAR_Error')) {
                     $notification->push($result->getMessage(), 'horde.error');
@@ -52,6 +57,7 @@ case 'rename_items':
                     $notification->push(sprintf(_("\"%s\" renamed to \"%s\""), $old[$i], $new[$i]), 'horde.success');
                 }
             }
+            Gollem::expireCache($old_dir);
         } else {
             $notification->push(_("Incorrect number of items."), 'horde.error');
         }
@@ -60,24 +66,35 @@ case 'rename_items':
 
 case 'chmod_modify':
 case 'delete_items':
-    if (Gollem::checkPermissions('backend', PERMS_DELETE)) {
+    if ($delete_perms) {
         $items = Util::getPost('items');
         if (is_array($items) && count($items)) {
             $chmod = Util::getPost('chmod');
             foreach ($items as $item) {
                 if (($actionID == 'chmod_modify') && $chmod) {
                     if (!is_a(Gollem::changePermissions(Gollem::getDir(), $item, $chmod), 'PEAR_Error')) {
+                        Gollem::expireCache($old_dir);
                         $notification->push(_("Chmod done: ") . $item, 'horde.success');
                     } else {
-                        $notification->push(sprintf(_("Cannot chmod %s"), $item), 'horde.error');
+                        $notification->push(sprintf(_("Cannot chmod %s: %s"), $item, $result->getMessage()), 'horde.error');
                     }
                 } elseif ($actionID == 'delete_items') {
-                    if (!is_a($result = Gollem::deleteFile($old_dir, $item), 'PEAR_Error')) {
-                        $notification->push(_("File deleted: ") . $item, 'horde.success');
-                    } elseif (!is_a($result = Gollem::deleteFolder($old_dir, $item), 'PEAR_Error')) {
-                        $notification->push(_("Folder removed: ") . $item, 'horde.success');
+                    if ($GLOBALS['gollem_vfs']->isFolder($old_dir, $item)) {
+                        $result = Gollem::deleteFolder($old_dir, $item);
+                        if (is_a($result, 'PEAR_Error')) {
+                            $notification->push(sprintf(_("Unable to delete folder %s: %s"), $item, $result->getMessage()), 'horde.error');
+                        } else {
+                            Gollem::expireCache($old_dir);
+                            $notification->push(_("Folder removed: ") . $item, 'horde.success');
+                        }
                     } else {
-                        $notification->push(sprintf(_("Cannot delete \"%s\": %s"), $item, $result->getMessage()), 'horde.error');
+                        $result = Gollem::deleteFile($old_dir, $item);
+                        if (is_a($result, 'PEAR_Error')) {
+                            $notification->push(sprintf(_("Unable to delete file %s: %s"), $item, $result->getMessage()), 'horde.error');
+                        } else {
+                            Gollem::expireCache($old_dir);
+                            $notification->push(_("File deleted: ") . $item, 'horde.success');
+                        }
                     }
                 }
             }
@@ -86,19 +103,20 @@ case 'delete_items':
     break;
 
 case 'upload_file':
-    if (Gollem::checkPermissions('backend', PERMS_EDIT)) {
-        for ($i = 1; $i <= count($_FILES); $i++) {
+    if ($edit_perms) {
+        for ($i = 1, $l = count($_FILES); $i <= $l; ++$i) {
             $val = 'file_upload_' . $i;
-            if (isset($_FILES[$val]) &&
-                ($_FILES[$val]['error'] != 4)) {
+            if (isset($_FILES[$val]) && ($_FILES[$val]['error'] != 4)) {
                 $res = Browser::wasFileUploaded($val);
                 if (!is_a($res, 'PEAR_Error')) {
-                    $res = Gollem::writeFile($old_dir, $_FILES[$val]['name'], $_FILES[$val]['tmp_name']);
+                    $filename = Util::dispelMagicQuotes($_FILES[$val]['name']);
+                    $res = Gollem::writeFile($old_dir, $filename, $_FILES[$val]['tmp_name']);
                 }
                 if (is_a($res, 'PEAR_Error')) {
                     $notification->push($res, 'horde.error');
                 } else {
-                    $notification->push(sprintf(_("File received: %s"), $_FILES[$val]['name']), 'horde.success');
+                    Gollem::expireCache($old_dir);
+                    $notification->push(sprintf(_("File received: %s"), $filename), 'horde.success');
                 }
             }
         }
@@ -107,8 +125,7 @@ case 'upload_file':
 
 case 'copy_items':
 case 'cut_items':
-    if (Gollem::checkPermissions('backend', PERMS_EDIT) &&
-        !empty($GLOBALS['gollem_be']['clipboard'])) {
+    if ($edit_perms && !empty($GLOBALS['gollem_be']['clipboard'])) {
         $action = ($actionID == 'copy_items') ? 'copy' : 'cut';
         $items = Util::getPost('items');
 
@@ -140,8 +157,7 @@ case 'cut_items':
 
 case 'clear_items':
 case 'paste_items':
-    if (Gollem::checkPermissions('backend', PERMS_EDIT) &&
-        !empty($GLOBALS['gollem_be']['clipboard'])) {
+    if ($edit_perms && !empty($GLOBALS['gollem_be']['clipboard'])) {
         $items = Util::getPost('items');
         if (is_array($items) && count($items)) {
             foreach ($items as $val) {
@@ -156,6 +172,10 @@ case 'paste_items':
                         if (is_a($res, 'PEAR_Error')) {
                             $notification->push(sprintf(_("Cannot paste \"%s\" (file cleared from clipboard): %s"), $file['name'], $res->getMessage()), 'horde.error');
                         } else {
+                            Gollem::expireCache($old_dir);
+                            if ($file['action'] == 'cut') {
+                                Gollem::expireCache($file['path']);
+                            }
                             $notification->push(sprintf(_("%s was successfully pasted."), $file['name'], $old_dir), 'horde.success');
                         }
                     }
@@ -200,24 +220,41 @@ if (is_a($list, 'PEAR_Error')) {
         $notification->push(sprintf(_("Permission denied to folder \"%s\": %s"), $currdir, $list->getMessage()), 'horde.error');
 
         $loc = strrpos($currdir, '/');
-        if ($loc !== false) {
-            Gollem::setDir(substr($currdir, 0, $loc));
-        } else {
-            Gollem::setDir(Gollem::getHome());
-        }
+        Gollem::setDir(($loc !== false) ? substr($currdir, 0, $loc) : Gollem::getHome());
         $currdir = Gollem::getDir();
         $list = Gollem::listFolder($currdir);
     }
 }
 
+$numitem = count($list);
+$title = $GLOBALS['gollem_be']['label'];
+
 /* Image links. */
-$download_img = Horde::img('download.png', _("Download"), null, $registry->getImageDir('horde'));
-$folder_img = Horde::img('manager/folder.png', _("folder"));
-$symlink_img = Horde::img('manager/symlink.png', _("symlink"));
+$image_dir = $registry->getImageDir('horde');
+$edit_img = Horde::img('edit.png', _("Edit"), null, $image_dir);
+$download_img = Horde::img('download.png', _("Download"), null, $image_dir);
+$folder_img = Horde::img('folder.png', _("folder"));
+$symlink_img = Horde::img('folder_symlink.png', _("symlink"));
+
+/* Init some form vars. */
+$page = Util::getFormData('page', 0);
+$filter = Util::getFormData('filter', '');
+if (isset($_SESSION['gollem']['filter']) &&
+    $_SESSION['gollem']['filter'] != $filter) {
+    $page = 0;
+}
+$_SESSION['gollem']['filter'] = $filter;
 
 /* Commonly used URLs. */
-$manager_url = Horde::applicationUrl('manager.php');
 $view_url = Horde::applicationUrl('view.php');
+$edit_url = Horde::applicationUrl('edit.php');
+$manager_url = Horde::applicationUrl('manager.php');
+
+$refresh_params = array('page' => $page);
+if ($filter) {
+    $refresh_params['filter'] = $filter;
+}
+$refresh_url = Util::addParameter($manager_url, $refresh_params);
 
 /* Get the list of copy/cut files in this directory. */
 $clipboard_files = array();
@@ -229,22 +266,12 @@ if (!empty($GLOBALS['gollem_be']['clipboard'])) {
     }
 }
 
-$title = $GLOBALS['gollem_be']['label'];
-Horde::addScriptFile('stripe.js', 'gollem', true);
-require GOLLEM_TEMPLATES . '/common-header.inc';
-Gollem::menu();
-Gollem::status();
-if ($browser->hasFeature('javascript')) {
-    require_once GOLLEM_TEMPLATES . '/manager/javascript.inc';
-    require_once $registry->get('templates', 'horde') . '/contents/open_view_win.js';
-}
-
 /* Read the columns to display from the preferences. */
 $sources = Gollem::displayColumns();
 $columns = isset($sources[$backkey]) ? $sources[$backkey] : $GLOBALS['gollem_be']['attributes'];
 
 /* Prepare the template. */
-$template = &new Horde_Template();
+$template = new Gollem_Template();
 $template->setOption('gettext', true);
 
 $attrib = $GLOBALS['gollem_vfs']->getModifiablePermissions();
@@ -262,15 +289,13 @@ foreach ($all_columns as $column) {
 $template->set('save', _("Save"));
 $template->set('cancel', _("Cancel"));
 $template->set('ok', _("OK"));
-$template->set('action', $manager_url);
+$template->set('action', $refresh_url);
 $template->set('forminput', Util::formInput());
 $template->set('dir', $currdir);
-$template->set('itemcount', (count($list) == 1) ? _("1 item") : sprintf(_("%d items"), count($list)));
 $template->set('navlink', Gollem::directoryNavLink($currdir, $manager_url));
-$template->set('refresh', Horde::link(Horde::selfUrl(), sprintf("%s %s", _("Refresh"), $GLOBALS['gollem_be']['label'])) . Horde::img('reload.png', sprintf("%s %s", _("Refresh"), htmlspecialchars($GLOBALS['gollem_be']['label'])), null, $registry->getImageDir('horde')) . '</a>');
+$template->set('refresh', Horde::link($refresh_url, sprintf("%s %s", _("Refresh"), $GLOBALS['gollem_be']['label']), '', '', '', '', '', array('id' => 'refreshimg')) . Horde::img('reload.png', sprintf("%s %s", _("Refresh"), htmlspecialchars($GLOBALS['gollem_be']['label'])), null, $image_dir) . '</a>');
 
-$edit_permissions = Gollem::checkPermissions('backend', PERMS_EDIT);
-$template->set('hasclipboard', !$edit_permissions || !empty($GLOBALS['gollem_be']['clipboard']), true);
+$template->set('hasclipboard', !$edit_perms || !empty($GLOBALS['gollem_be']['clipboard']), true);
 if (!$template->get('hasclipboard') ||
     empty($_SESSION['gollem']['clipboard'])) {
     $template->set('clipboard', null);
@@ -278,50 +303,87 @@ if (!$template->get('hasclipboard') ||
     $template->set('clipboard', Horde::link(Util::addParameter(Horde::applicationUrl('clipboard.php'), 'dir', $currdir), _("View Clipboard")) . Horde::img('clipboard.png', _("View Clipboard")) . '</a>');
 }
 
-if ($edit_permissions) {
+if ($edit_perms) {
     $template->set('perms_edit', true, true);
     $template->set('upload_file', _("Upload File(s)"));
     $template->set('upload_help', Help::link('gollem', 'file-upload'));
     $template->set('perms_chmod', in_array('permission', $columns), true);
+    $template->set('create_folder', Horde::link('#', _("Create Folder"), '', '', '', '', '', array('id' => 'createfolder')) . Horde::img('folder_create.png', _("Create Folder")) . '</a>');
 } else {
     $template->set('perms_edit', false, true);
     $template->set('perms_chmod', false, true);
 }
 
-if (count($list)) {
+if ($read_perms) {
+    $template->set('change_folder', Horde::link('#', _("Change Folder"), '', '', '', '', '', array('id' => 'changefolder')) . Horde::img('folder_goto.png', _("Change Folder")) . '</a>');
+}
+
+if ($numitem) {
     $template->set('list_count', true, true);
-    if (Gollem::checkPermissions('backend', PERMS_DELETE)) {
-        $template->set('perms_delete', true, true);
-    } else {
-        $template->set('perms_edit', false, true);
-    }
+    $template->set('perms_delete', $delete_perms);
     $template->set('actions_help', Help::link('gollem', 'file-actions'));
 } else {
     $template->set('list_count', false, true);
 }
 
 $icon_cache = array();
+$total = 0;
 
-if (is_array($list) &&
-    count($list) &&
-    Gollem::checkPermissions('backend', PERMS_READ)) {
+if (is_array($list) && $numitem && $read_perms) {
     $entry = array();
-    $i = $total_files = $total_folders = $total_size = 0;
+    $page_caption = '';
+
+    require_once 'Horde/MIME/Magic.php';
+    require_once 'Horde/MIME/Viewer.php';
+    if (is_callable(array('Horde', 'loadConfiguration'))) {
+        $result = Horde::loadConfiguration('mime_drivers.php', array('mime_drivers', 'mime_drivers_map'), 'horde');
+        extract($result);
+        $result = Horde::loadConfiguration('mime_drivers.php', array('mime_drivers', 'mime_drivers_map'), 'gollem');
+        require_once 'Horde/Array.php';
+        $mime_drivers = Horde_Array::array_merge_recursive_overwrite($mime_drivers, $result['mime_drivers']);
+        $mime_drivers_map = Horde_Array::array_merge_recursive_overwrite($mime_drivers_map, $result['mime_drivers_map']);
+    } else {
+        require HORDE_BASE . '/config/mime_drivers.php';
+        require GOLLEM_BASE . '/config/mime_drivers.php';
+    }
 
     $template->set('empty_dir', false, true);
 
+    /* Set list min/max values */
+    $perpage = $prefs->getValue('perpage');
+    $min = $page * $perpage;
+    while ($min > $numitem) {
+        --$page;
+        $min = $page * $perpage;
+    }
+    $max = $min + $perpage;
+
     foreach ($list as $key => $val) {
-        $item = array();
-
-        $item['date'] = strftime($conf['manager']['date_format'], $val['date']);
-        $item['dl'] = false;
-        $item['group'] = (empty($val['group'])) ? '-' : $val['group'];
-        $item['name'] = htmlspecialchars($val['name']);
-        $item['on_clipboard'] = false;
-        $item['owner'] = (empty($val['owner'])) ? '-' : $val['owner'];
-        $item['perms'] = (empty($val['perms'])) ? '-' : $val['perms'];
-        $item['size'] = ($val['type'] == '**dir') ? '-' : number_format($val['size'], 0, '.', ',');
-        $item['type'] = $val['type'];
+        /* Check if a filter is not empty and filter matches filename. */
+        if (strlen($filter) &&
+            !preg_match('/' . preg_quote($filter, '/') . '/', $val['name'])) {
+            continue;
+        }
+
+        /* Continue if item not in min/max range. */
+        if (($total++ < $min) || ($total > $max)) {
+            continue;
+        }
+
+        $item = array(
+            'date' => htmlspecialchars(strftime($prefs->getValue('date_format'), $val['date'])),
+            'date_sort' => (int)$val['date'],
+            'dl' => false,
+            'edit' => false,
+            'group' => empty($val['group']) ? '-' : htmlspecialchars($val['group']),
+            'name' => htmlspecialchars($val['name']),
+            'on_clipboard' => false,
+            'owner' => empty($val['owner']) ? '-' : htmlspecialchars($val['owner']),
+            'perms' => empty($val['perms']) ? '-' : htmlspecialchars($val['perms']),
+            'size' => ($val['type'] == '**dir') ? '-' : number_format($val['size'], 0, '.', ','),
+            'type' => htmlspecialchars($val['type']),
+            'type_sort' => ($val['type'] == '**dir') ? '' : htmlspecialchars($val['type']),
+        );
 
         $name = str_replace(' ', '&nbsp;', $item['name']);
 
@@ -337,10 +399,6 @@ if (is_array($list) &&
             $item['graphic'] = $folder_img;
         } else {
             if (empty($icon_cache[$val['type']])) {
-                require_once 'Horde/MIME/Magic.php';
-                require_once 'Horde/MIME/Viewer.php';
-                require_once HORDE_BASE . '/config/mime_drivers.php';
-                require_once GOLLEM_BASE . '/config/mime_drivers.php';
                 $icon_cache[$val['type']] = Horde::img(MIME_Viewer::getIcon(MIME_Magic::extToMIME($val['type'])), '', '', '');
             }
             $item['graphic'] = $icon_cache[$val['type']];
@@ -349,7 +407,7 @@ if (is_array($list) &&
         /* Create proper link. */
         switch ($val['type']) {
         case '**dir':
-            $url = Util::addParameter($manager_url, 'dir', Gollem::subdirectory($currdir, $val['name']));
+            $url = Util::addParameter($manager_url, array('dir' => Gollem::subdirectory($currdir, $val['name'])));
             $item['link'] = Horde::link($url) . '<strong>' . $name . '</strong></a>';
             break;
 
@@ -368,7 +426,7 @@ if (is_array($list) &&
                     $dir = $currdir;
                 }
 
-                $url = Util::addParameter($manager_url, 'dir', Gollem::subdirectory($dir, $name));
+                $url = Util::addParameter($manager_url, array('dir' => Gollem::subdirectory($dir, $name)));
                 $item['link'] = $item['name'] . ' -> <strong>' . Horde::link($url) . $val['link'] . '</a></strong>';
             } else {
                 $item['link'] = $item['name'] . ' -> ' . $val['link'];
@@ -376,55 +434,54 @@ if (is_array($list) &&
             break;
 
         default:
-            require_once 'Horde/MIME/Magic.php';
-            $mime_part = &new MIME_Part(MIME_Magic::extToMIME($val['type']), '');
+            $mime_type = MIME_Magic::extToMIME($val['type']);
+
+            // Edit link if possible.
+            if (strpos($mime_type, 'text/') === 0) {
+                $url = Util::addParameter($edit_url, array('actionID' => 'edit_file', 'type' => $val['type'], 'file' => $val['name'], 'dir' => $currdir, 'driver' => $GLOBALS['gollem_be']['driver']));
+                $item['edit'] = Horde::link($url, '', '', '_blank', 'popup(this.href); return false;') . $edit_img . '</a>';
+            }
+
             // We can always download files.
             $item['dl'] = Horde::link(Horde::downloadUrl($val['name'], array('actionID' => 'download_file', 'dir' => $currdir, 'driver' => $GLOBALS['gollem_be']['driver'], 'file' => $val['name'])), sprintf(_("Download %s"), $val['name'])) . $download_img . '</a>';
 
             // Try a view link.
             $url = Util::addParameter($view_url, array('actionID' => 'view_file', 'type' => $val['type'], 'file' => $val['name'], 'dir' => $currdir, 'driver' => $GLOBALS['gollem_be']['driver']));
-            $item['link'] = Horde::link('#', '', '', '', "view('$url', '" . $val['name'] . "'); return false;") . $name . '</a>';
+            $item['link'] = Horde::link($url, '', '', '_blank', 'popup(this.href); return false;') . $name . '</a>';
             break;
         }
 
-        if ($val['type'] != '**dir') {
-            $total_size += $val['size'];
-            $total_files++;
-        } else {
-            $total_folders++;
-        }
-
         $entry[] = $item;
     }
 
     /* Set up the variables needed for the header row. */
-    $manager_url = Horde::selfUrl();
     $sortby = $prefs->getValue('sortby');
     $sortdir = $prefs->getValue('sortdir');
 
-    $size = round($total_size / 1000);
-    if ($total_folders == 1) {
-        if ($total_files == 1) {
-            $size_caption = sprintf(_("1 Folder and 1 File (%s Kb)"), $size);
-        } else {
-            $size_caption = sprintf(_("1 Folder and %s Files (%s Kb)"), $total_files, $size);
-        }
-    } elseif ($total_files == 1) {
-        $size_caption = sprintf(_("%s Folders and 1 File (%s Kb)"), $total_folders, $size);
-    } else {
-        $size_caption = sprintf(_("%s Folders and %s Files (%s Kb)"), $total_folders, $total_files, $size);
+    if ($total) {
+        // Set start/end items (according to current page)
+        $start = ($page * $perpage) + 1;
+        $end = min($total, $start + $perpage - 1);
+
+        require_once 'Horde/Variables.php';
+        require_once 'Horde/UI/Pager.php';
+        $vars = Variables::getDefaultVariables();
+        $vars->set('page', $page);
+        $pager = new Horde_UI_Pager('page', $vars, array('num' => $total, 'url' => $refresh_url, 'page_count' => 10, 'perpage' => $perpage));
+        $page_caption = $pager->render();
     }
 
     $headers = array();
     foreach ($columns as $head) {
-        $hdr = array('class' => 'item', 'sort' => '');
+        $hdr = array('class' => '');
         $sort = null;
 
         switch ($head) {
         case 'type':
-            $hdr['width'] = '5%';
+            $hdr['width'] = '3%';
             $hdr['label'] = _("Type");
             $hdr['align'] = 'right';
+            $hdr['id'] = 's' . GOLLEM_SORT_TYPE;
             $sort = GOLLEM_SORT_TYPE;
             break;
 
@@ -432,19 +489,29 @@ if (is_array($list) &&
             $hdr['width'] = '57%';
             $hdr['label'] = _("Name");
             $hdr['align'] = 'left';
+            $hdr['id'] = 's' . GOLLEM_SORT_NAME;
             $sort = GOLLEM_SORT_NAME;
             break;
 
+        case 'edit':
+            $hdr['width'] = '1%';
+            $hdr['label'] = '&nbsp;';
+            $hdr['align'] = 'center';
+            $hdr['class'] = 'nosort';
+            break;
+
         case 'download':
             $hdr['width'] = '1%';
             $hdr['label'] = '&nbsp;';
             $hdr['align'] = 'center';
+            $hdr['class'] = 'nosort';
             break;
 
         case 'modified':
             $hdr['width'] = '7%';
             $hdr['label'] = _("Modified");
             $hdr['align'] = 'left';
+            $hdr['id'] = 's' . GOLLEM_SORT_DATE;
             $sort = GOLLEM_SORT_DATE;
             break;
 
@@ -452,6 +519,7 @@ if (is_array($list) &&
             $hdr['width'] = '7%';
             $hdr['label'] = _("Size");
             $hdr['align'] = 'right';
+            $hdr['id'] = 's' . GOLLEM_SORT_SIZE;
             $sort = GOLLEM_SORT_SIZE;
             break;
 
@@ -459,27 +527,29 @@ if (is_array($list) &&
             $hdr['width'] = '7%';
             $hdr['label'] = _("Permission");
             $hdr['align'] = 'right';
+            $hdr['class'] = 'nosort';
             break;
 
         case 'owner':
             $hdr['width'] = '7%';
             $hdr['label'] = _("Owner");
             $hdr['align'] = 'right';
+            $hdr['class'] = 'nosort';
             break;
 
         case 'group':
             $hdr['width'] = '7%';
             $hdr['label'] = _("Group");
             $hdr['align'] = 'right';
+            $hdr['class'] = 'nosort';
             break;
         }
 
-        if (!is_null($sort)) {
+        if ($sort !== null) {
             if ($sortby == $sort) {
-                $hdr['sort'] = '<a href="' . Util::addParameter($manager_url, array('actionID' => 'change_sortdir', 'sortdir' => abs(1 - $sortdir))) . '">' . Horde::img($sortdir ? 'za.png' : 'az.png', _("Sort Direction"), null, $registry->getImageDir('horde')) . '</a>&nbsp;';
-                $hdr['class'] = 'selected';
+                $hdr['class'] = ($sortdir ? 'sortup' : 'sortdown');
             }
-            $hdr['label'] = '<a href="' . Util::addParameter($manager_url, array('actionID' => 'change_sortby', 'sortby' => $sort)) .'">' . $hdr['label'] . '</a>';
+            $hdr['label'] = '<a href="' . Util::addParameter($refresh_url, array('actionID' => 'change_sortby', 'sortby' => $sort)) . '" class="sortlink">' . htmlspecialchars($hdr['label']) . '</a>';
         }
 
         $headers[] = $hdr;
@@ -488,12 +558,26 @@ if (is_array($list) &&
     /* Set up the template tags. */
     $template->set('headers', $headers, true);
     $template->set('entry', $entry, true);
-    $template->set('size_caption', $size_caption);
+    $template->set('page_caption', $page_caption);
+    $template->set('filter_val', $filter);
     $template->set('checkall', Horde::getAccessKeyAndTitle(_("Check _All/None")));
-
 } else {
     $template->set('empty_dir', true, true);
 }
+$template->set('itemcount', sprintf(ngettext(_("%d item"), _("%d items"), $total), $total));
+
+$js_code = array(
+    'var warn_recursive = ' . intval($GLOBALS['prefs']->getValue('recursive_deletes') == 'warn'),
+);
+
+Horde::addScriptFile('prototype.js', 'gollem', true);
+Horde::addScriptFile('manager.js', 'gollem', true);
+Horde::addScriptFile('popup.js', 'gollem', true);
+Horde::addScriptFile('tables.js', 'gollem', true);
 
+require GOLLEM_TEMPLATES . '/common-header.inc';
+Gollem::addInlineScript(implode(';', $js_code));
+Gollem::menu();
+Gollem::status();
 echo $template->fetch(GOLLEM_TEMPLATES . '/manager/manager.html');
 require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/permissions.php b/permissions.php
index 3782f9b..7b8cc67 100644
--- a/permissions.php
+++ b/permissions.php
@@ -2,11 +2,11 @@
 /**
  * Gollem permissions administration page.
 
- * $Horde: gollem/permissions.php,v 1.2.2.8 2007/01/02 13:54:52 jan Exp $
+ * $Horde: gollem/permissions.php,v 1.2.2.9 2008/10/13 09:32:22 jan Exp $
  *
  * Copyright 2005-2007 Vijay Mahrra <vijay.mahrra at es.easynet.net>
  *
- * See the enclosed file COPYING for license information (GPL).  If you
+ * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
  */
 
diff --git a/po/bg_BG.po b/po/bg_BG.po
index 34a921e..9e5d62e 100644
--- a/po/bg_BG.po
+++ b/po/bg_BG.po
@@ -1,13 +1,12 @@
 # Bulgarian translations for Gollem package.
-# Copyright (C) 2002 Horde Project
+# Copyright 2002-2009 The Horde Project
 # This file is distributed under the same license as the Gollem package.
 # Miroslav Pendev <miro at cybershade.us>, 2002.
 #
 msgid ""
 msgstr ""
 "Project-Id-Version: Gollem 0.0.1-cvs\n"
-"Report-Msgid-Bugs-To: dev at lists.horde.org\n"
-"POT-Creation-Date: 2005-10-13 17:13+0200\n"
+"POT-Creation-Date: 2002-09-14 00:38-0400\n"
 "PO-Revision-Date: 2002-11-19 20:44-0500\n"
 "Last-Translator: Miroslav Pendev <miro at cybershade.us>\n"
 "Language-Team: BG i18n at lists.horde.org\n"
@@ -15,575 +14,312 @@ msgstr ""
 "Content-Type: text/plain; charset=CP1251\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: manager.php:52
-#, fuzzy, php-format
-msgid "\"%s\" renamed to \"%s\""
-msgstr "'%s' ïðåèìåíóâàíî íà '%s'"
-
-#: templates/manager/header.inc:576
-#, fuzzy, php-format
-msgid "%d items"
+#, c-format
+msgid "%d items in %s"
 msgstr "%d íåùà â %s"
 
-#: manager.php:356
-#, php-format
-msgid "%s Folders and %s Files (%s Kb)"
-msgstr ""
-
-#: manager.php:354
-#, php-format
-msgid "%s Folders and 1 File (%s Kb)"
-msgstr ""
-
-#: templates/login/login.inc:88
-#, php-format
+#, c-format
 msgid "%s Login"
 msgstr "%s Âõîä"
 
-#: manager.php:157
-#, php-format
-msgid "%s was successfully pasted."
-msgstr ""
+#, c-format
+msgid "%s Standard"
+msgstr "%s Ñòàíäàðò"
 
-#: manager.php:351
-#, php-format
-msgid "1 Folder and %s Files (%s Kb)"
-msgstr ""
-
-#: manager.php:349
-#, php-format
-msgid "1 Folder and 1 File (%s Kb)"
-msgstr ""
-
-#: templates/manager/header.inc:576
-msgid "1 item"
-msgstr ""
+#, c-format
+msgid "'%s' renamed to '%s'"
+msgstr "'%s' ïðåèìåíóâàíî íà '%s'"
 
-#: view.php:31
-#, php-format
+#, c-format
 msgid "Access denied to %s"
 msgstr "Îòêàçàí äîñòúï äî %s"
 
-#: lib/Gollem.php:115 lib/Gollem.php:389 lib/Gollem.php:442 lib/Gollem.php:463
-#: lib/Gollem.php:480
-#, fuzzy, php-format
-msgid "Access denied to folder \"%s\"."
-msgstr "Îòêàçàí äîñòúï äî %s"
-
-#: templates/manager/header.inc:601
-#, fuzzy
-msgid "Actions"
-msgstr "Îïöèè"
-
-#: selectlist.php:100
-msgid "Add"
-msgstr ""
-
-#: templates/prefs/columnselect.inc:229
-msgid "Add column"
-msgstr ""
-
-#: templates/manager/header.inc:493
-msgid "All"
-msgstr ""
-
-#: templates/manager/header.inc:180
-msgid "Are you sure you wish to continue?"
-msgstr ""
-
-#: templates/manager/header.inc:176
-msgid "Are you sure?"
-msgstr ""
-
-#: config/prefs.php.dist:69
 msgid "Ascending"
 msgstr "Íàðàñòâàùè"
 
-#: config/prefs.php.dist:93
-msgid "Ask"
-msgstr ""
+msgid "Back To Folder"
+msgstr "Îáðàòíî êúì ïàïêà"
 
-#: templates/manager/header.inc:487
-msgid "Attributes"
-msgstr ""
+msgid "Barbie"
+msgstr "Barbie"
 
-#: templates/prefs/columnselect.inc:225
-msgid "Available Columns:"
-msgstr ""
+msgid "Brown"
+msgstr "Brown"
 
-#: lib/api.php:46
-msgid "Backends"
-msgstr ""
+msgid "Burnt Orange"
+msgstr "Burnt Orange"
 
-#: templates/clipboard/clipboard.html:11
-msgid "Below is the current contents of your clipboard."
-msgstr ""
-
-#: selectlist.php:102 clipboard.php:35 templates/manager/header.inc:515
-#: templates/manager/header.inc:533 templates/manager/header.inc:546
-#: templates/manager/header.inc:559
-msgid "Cancel"
-msgstr ""
+msgid "Camouflage"
+msgstr "Camouflage"
 
-#: manager.php:72
-#, php-format
+#, c-format
 msgid "Cannot chmod %s"
 msgstr "Ãðåøêà ïðè chmod %s"
 
-#: manager.php:132
-msgid "Cannot copy items onto clipboard."
-msgstr ""
-
-#: manager.php:190
-#, php-format
-msgid "Cannot create home directory: %s"
-msgstr ""
-
-#: manager.php:134
-msgid "Cannot cut items onto clipboard."
-msgstr ""
-
-#: manager.php:80
-#, fuzzy, php-format
-msgid "Cannot delete \"%s\": %s"
+#, c-format
+msgid "Cannot delete '%s'"
 msgstr "Ãðåøêà ïðè èçòðèâàíå íà '%s'"
 
-#: manager.php:155
-#, php-format
-msgid "Cannot paste \"%s\" (file cleared from clipboard): %s"
-msgstr ""
-
-#: templates/manager/header.inc:553 lib/Gollem.php:793
-#, fuzzy
-msgid "Change Folder"
-msgstr "Ñúçäàé ïàïêà"
-
-#: selectlist.php:110 templates/menu.inc:8
-#, fuzzy
-msgid "Change Server"
-msgstr "Ñúðâúð"
-
-#: config/prefs.php.dist:21
-#, fuzzy
-msgid "Change file and folder handling settings."
+msgid "Change your file sorting options and theme setting."
 msgstr "Ñìåíåòå íà÷èíà íà ñîðòèðàíå íà ôàéëîâåòå è íàñòðîéêèòå çà òåìàòà."
 
-#: config/prefs.php.dist:14
-#, fuzzy
-msgid "Change your file sorting options."
-msgstr "Ñìåíåòå íà÷èíà íà ñîðòèðàíå íà ôàéëîâåòå è íàñòðîéêèòå çà òåìàòà."
+msgid "Checkbox"
+msgstr "Checkbox"
 
-#: manager.php:438
-msgid "Check _All/None"
-msgstr ""
+msgid "Cherry"
+msgstr "Cherry"
 
-#: templates/manager/header.inc:609
 msgid "Chmod Items"
 msgstr "Chmod íåùà"
 
-#: manager.php:70
 msgid "Chmod done: "
 msgstr "Chmod èçâúðøåí:"
 
-#: templates/prefs/columnselect.inc:218
-msgid "Choose the columns to display in the file manager."
-msgstr ""
-
-#: clipboard.php:36
-msgid "Clear"
-msgstr ""
-
-#: clipboard.php:17 templates/clipboard/clipboard.html:7
-msgid "Clipboard"
-msgstr ""
+msgid "Choose Action:"
+msgstr "Èçáåðè äåéñòâèå:"
 
-#: templates/login/login.inc:111
-#, php-format
+#, c-format
 msgid "Connect to: %s"
 msgstr "Ñâúðçâàíå êúì %s"
 
-#: clipboard.php:39
-msgid "Copy"
-msgstr ""
-
-#: templates/manager/header.inc:611
-#, fuzzy
-msgid "Copy Items"
-msgstr "Chmod íåùà"
-
-#: templates/manager/header.inc:540 lib/Gollem.php:790
 msgid "Create Folder"
 msgstr "Ñúçäàé ïàïêà"
 
-#: permissions.php:48
-#, php-format
-msgid "Created default permissions for backend \"%s\"."
-msgstr ""
-
-#: clipboard.php:40
-#, php-format
-msgid "Current directory: %s"
-msgstr ""
-
-#: clipboard.php:38
-msgid "Cut"
-msgstr ""
-
-#: templates/manager/header.inc:610
-#, fuzzy
-msgid "Cut Items"
-msgstr "Chmod íåùà"
-
-#: config/prefs.php.dist:59
 msgid "Default sorting criteria:"
 msgstr "Êðèòåðèè çà ñîðòèðàíå ïî ïîäðàçáèðàíå:"
 
-#: config/prefs.php.dist:72
 msgid "Default sorting direction:"
 msgstr "Ïîñîêà íà ñîðòèðàíå ïî ïîäðàçáèðàíå:"
 
-#: templates/manager/header.inc:606
+#, c-format
+msgid "Defaulted to: %s"
+msgstr "Ïîäðàçáèðàùî ñå: %s"
+
 msgid "Delete Items"
 msgstr "Èçòðèé íåùà"
 
-#: config/prefs.php.dist:95
-msgid "Delete folders recursively?"
-msgstr ""
-
-#: config/prefs.php.dist:70
 msgid "Descending"
 msgstr "Íàìàëÿâàùè"
 
-#: selectlist.php:101
-msgid "Done"
-msgstr ""
+msgid "Directory removed: "
+msgstr "Äèðåêòîðèÿòà ïðåìåñòåíà:"
+
+msgid "Display Options"
+msgstr "Äèñïëåé Îïöèè"
 
-#: manager.php:218
 msgid "Download"
 msgstr "Èçòåãëè"
 
-#: manager.php:323
-#, fuzzy, php-format
-msgid "Download %s"
-msgstr "Èçòåãëè"
+msgid ""
+"Either you have logged in incorrectly or your login has expired. Please "
+"login again."
+msgstr ""
+"Ãðåøêà ïðè âõîä èëè âðåìåòî çà âàøàòà ñåñèÿ å èçòåêëî. Ìîëÿ ðåãèñòðèðàéòå ñå "
+"îòíîâî."
 
-#: config/credentials.php.dist:28
 msgid "Email"
 msgstr "Ïèñìî"
 
-#: templates/manager/header.inc:508
-msgid "Execute"
-msgstr ""
+#, c-format
+msgid "Error uploading %s: %s"
+msgstr "Ãðåøêà ïðè èçïðàùàíå íà ôàéëà %s: %s"
 
-#: templates/manager/header.inc:464 templates/manager/header.inc:589
-#, fuzzy
-msgid "File"
-msgstr "Èìå íà ôàéë"
+#, c-format
+msgid "Error uploading file: %s"
+msgstr "Ãðåøêà ïðè èçïðàùàíå íà ôàéëà: %s"
 
-#: config/prefs.php.dist:13
-msgid "File Display"
-msgstr ""
-
-#: config/prefs.php.dist:56
 msgid "File Modification Time"
 msgstr "Äàòà íà ìîäèôèêàöèÿ íà ôàéëà"
 
-#: config/prefs.php.dist:55
 msgid "File Name"
 msgstr "Èìå íà ôàéë"
 
-#: config/prefs.php.dist:57
 msgid "File Size"
 msgstr "Ðàçìåð íà ôàéëà"
 
-#: config/prefs.php.dist:54
 msgid "File Type"
 msgstr "Ôàéë òèï"
 
-#: manager.php:76
 msgid "File deleted: "
 msgstr "Èçòðèò ôàéë: "
 
-#: manager.php:101
-#, php-format
+#, c-format
 msgid "File received: %s"
 msgstr "Èçòåãëåí ôàéë: %s"
 
-#: manager.php:78
-#, fuzzy
-msgid "Folder removed: "
-msgstr "Äèðåêòîðèÿòà ïðåìåñòåíà:"
+msgid "File type can't be viewed inline, download the file instead"
+msgstr "Ôàéë îò òîçè òèï íå ìîæå äà ñå ïðåãëåæäà, èçòåãëåòå ãî"
 
-#: templates/manager/header.inc:555
-msgid "Go to:"
-msgstr ""
+msgid "Gollem is not properly configured"
+msgstr "Gollem íå å êîíôèãóðèðàí êîðåêòíî"
 
-#: permissions.php:25
-msgid "Gollem Backend Permissions Administration"
-msgstr ""
+msgid "Green"
+msgstr "Green"
+
+msgid "Grey"
+msgstr "Grey"
 
-#: manager.php:413 templates/manager/header.inc:492
 msgid "Group"
 msgstr "Ãðóïà"
 
-#: lib/Gollem.php:786
+msgid "Help"
+msgstr "Ïîìîù"
+
 msgid "Home"
 msgstr "Íà÷àëî"
 
-#: manager.php:56
 msgid "Incorrect number of items."
 msgstr "Íåêîðåêòåí áðîé."
 
-#: manager.php:125
-#, php-format
-msgid "Item copied to clipboard: %s"
-msgstr ""
-
-#: manager.php:127
-#, php-format
-msgid "Item cut to clipboard: %s"
-msgstr ""
-
-#: templates/login/login.inc:136
 msgid "Language"
 msgstr "Åçèê"
 
-#: config/prefs.php.dist:81
-msgid "List folders first?"
-msgstr ""
+msgid "Light Blue"
+msgstr "Light Blue"
+
+msgid "Locked"
+msgstr "Çàêëþ÷åí"
 
-#: templates/login/login.inc:145
 msgid "Log in"
 msgstr "Âõîä"
 
-#: lib/Block/tree_menu.php:3
-msgid "Menu List"
+msgid ""
+"Login failed for some reason. Most likely your username or password was "
+"entered incorrectly."
 msgstr ""
+"Ðåãèñòðèðàíåòî ñå ïðîâàëè ïîðàäè íåèçâåñòíà ïðè÷èíà. Âåðîÿòíî ãðåøíî "
+"âúâåäåíî ïîòðåáèòåëñêî èìå èëè ïàðîëà."
+
+msgid "Logout"
+msgstr "Èçõîä"
 
-#: manager.php:387
 msgid "Modified"
 msgstr "Ïðîìåíåí"
 
-#: templates/prefs/columnselect.inc:239
-msgid "Move left"
-msgstr ""
+msgid "Mozilla"
+msgstr "Mozilla"
 
-#: templates/prefs/columnselect.inc:241
-msgid "Move right"
-msgstr ""
-
-#: manager.php:374
 msgid "Name"
 msgstr "Èìå"
 
-#: templates/manager/header.inc:529 templates/manager/header.inc:542
 msgid "Name:"
 msgstr "Èìå:"
 
-#: manager.php:35
 msgid "New folder created: "
 msgstr "Ñúçäàäåíà å íîâà ïàïêà: "
 
-#: config/prefs.php.dist:91
-msgid "No"
-msgstr ""
+msgid "Options"
+msgstr "Îïöèè"
 
-#: templates/manager/header.inc:532 templates/manager/header.inc:545
-#: templates/manager/header.inc:558
-msgid "OK"
-msgstr ""
+msgid "Orange"
+msgstr "Orange"
+
+msgid "Other Options"
+msgstr "Äðóãè Îïöèè"
 
-#: manager.php:407 templates/manager/header.inc:491
 msgid "Owner"
 msgstr "Ïðèòåæàòåë"
 
-#: config/credentials.php.dist:23
 msgid "Password"
 msgstr "Ïàðîëà"
 
-#: clipboard.php:37
-msgid "Paste"
-msgstr ""
-
-#: manager.php:401
 msgid "Permission"
 msgstr "Ïðàâà"
 
-#: selectlist.php:63
-#, fuzzy, php-format
-msgid "Permission denied to %s: %s"
-msgstr "Ïðàâî íà äîñòúï äî %s îòêàçàí: %s"
-
-#: manager.php:198
-#, fuzzy, php-format
-msgid "Permission denied to folder \"%s\": %s"
+#, c-format
+msgid "Permission denied to %s: %s."
 msgstr "Ïðàâî íà äîñòúï äî %s îòêàçàí: %s"
 
-#: lib/Gollem.php:798
-#, fuzzy
-msgid "Permissions"
-msgstr "Ïðàâà"
-
-#: templates/login/login.inc:49
-msgid "Please provide your password."
-msgstr ""
-
-#: templates/login/login.inc:44
-msgid "Please provide your username."
-msgstr ""
-
-#: templates/prefs/columnselect.inc:211
-msgid "Please select a backend:"
-msgstr ""
-
-#: templates/manager/header.inc:109
-msgid "Please select an item before this action."
-msgstr ""
-
-#: templates/manager/header.inc:435
-msgid "Please specify at least one file to upload."
-msgstr ""
-
-#: templates/manager/header.inc:496
-msgid "Read"
-msgstr ""
-
-#: templates/manager/header.inc:579
 msgid "Refresh"
 msgstr "Îïðåñíè"
 
-#: templates/prefs/columnselect.inc:231
-msgid "Remove column"
-msgstr ""
-
-#: templates/manager/header.inc:527
-#, fuzzy
-msgid "Rename"
-msgstr "Èìå"
-
-#: templates/manager/header.inc:603
 msgid "Rename Items"
 msgstr "Ïðåèìåíóâàíå"
 
-#: lib/Gollem.php:754 lib/Gollem.php:759
-#, fuzzy
-msgid "Root"
-msgstr "Èçõîä"
+msgid "Select your color scheme."
+msgstr "Èçáåðåòå âàøàòà ñõåìà çà öâåòîâåòå."
 
-#: templates/manager/header.inc:514
-#, fuzzy
-msgid "Save"
-msgstr "Ñúðâúð"
+msgid "Select your preferred language:"
+msgstr "Èçáåðåòå ïðåäïî÷èòàíèÿò îò Âàñ åçèê:"
 
-#: templates/prefs/columnselect.inc:235
-msgid "Selected Columns:"
-msgstr ""
-
-#: templates/login/login.inc:101
 msgid "Server"
 msgstr "Ñúðâúð"
 
-#: config/prefs.php.dist:20
-msgid "Settings"
-msgstr ""
+msgid "Set the language that menu items, explanations, and help are in."
+msgstr "Èçáåðåòå åçèêà, íà êîèòî ñå ïîêàçâàò ìåíþòà, îáÿñíåíèÿ è ïîìîù."
 
-#: config/prefs.php.dist:30
-msgid "Show dotfiles?"
-msgstr ""
-
-#: manager.php:394
 msgid "Size"
 msgstr "Ðàçìåð"
 
-#: manager.php:420
+msgid "Some of Gollem's configuration files are missing:"
+msgstr "Íÿêîè îò êîíôèãóðàöèîííèòå ôàéëîâå íà Gollem ëèïñâàò:"
+
 msgid "Sort Direction"
 msgstr "Ñîðòèðàíå"
 
-#: templates/manager/header.inc:180
-msgid "The following item(s) are folders: "
-msgstr ""
+msgid "Sun"
+msgstr "Sun"
 
-#: templates/manager/header.inc:176
-msgid "The following items will be permanently deleted: "
-msgstr ""
+msgid "There are no files in this directory."
+msgstr "Íÿìà ôàéëîâå â òàçè äèðåêòîðèÿ."
+
+msgid "There was an error viewing this file"
+msgstr "Ãðåøêà ïðè ïðåãëåæäàíåòî íà òîçè ôàéë."
 
-#: templates/selectlist/javascript.inc:9
-msgid "The original opener window has been closed. Exiting."
+msgid ""
+"This file controls the default preferences for Gollem, and also controls "
+"which preferences users can alter."
 msgstr ""
+"Òîçè ôàéë êîíòðîëèðà íàñòðîéêèòå ïî ïîäðàçáèð