Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bin/prove.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bin/prove.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bin/prove.php	(revision 4)
@@ -0,0 +1,8 @@
+<?php
+
+include dirname(__FILE__).'/../bootstrap/unit.php';
+
+$h = new lime_harness(new lime_output_color());
+$h->register(sfFinder::type('file')->name('*Test.php')->in(dirname(__FILE__).'/..'));
+
+exit($h->run() ? 0 : 1);
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/test/bootstrap/unit.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/test/bootstrap/unit.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/test/bootstrap/unit.php	(revision 4)
@@ -0,0 +1,15 @@
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+$_test_dir = realpath(dirname(__FILE__).'/..');
+
+require_once(dirname(__FILE__).'/../../config/ProjectConfiguration.class.php');
+$configuration = new ProjectConfiguration(realpath($_test_dir.'/..'));
+include($configuration->getSymfonyLibDir().'/vendor/lime/lime.php');
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/test/bootstrap/functional.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/test/bootstrap/functional.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/test/bootstrap/functional.php	(revision 4)
@@ -0,0 +1,26 @@
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+// guess current application
+if (!isset($app))
+{
+  $traces = debug_backtrace();
+  $caller = $traces[0];
+
+  $dirPieces = explode(DIRECTORY_SEPARATOR, dirname($caller['file']));
+  $app = array_pop($dirPieces);
+}
+
+require_once dirname(__FILE__).'/../../config/ProjectConfiguration.class.php';
+$configuration = ProjectConfiguration::getApplicationConfiguration($app, 'test', isset($debug) ? $debug : true);
+sfContext::createInstance($configuration);
+
+// remove all cache
+sfToolkit::clearDirectory(sfConfig::get('sf_app_cache_dir'));
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/factories.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/factories.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/factories.yml	(revision 4)
@@ -0,0 +1,132 @@
+prod:
+  logger:
+    class:   sfNoLogger
+    param:
+      level:   err
+      loggers: ~
+
+cli:
+  controller:
+    class: sfConsoleController
+  request:
+    class: sfConsoleRequest
+  response:
+    class: sfConsoleResponse
+
+test:
+  storage:
+    class: sfSessionTestStorage
+    param:
+      session_path: %SF_TEST_CACHE_DIR%/sessions
+
+  response:
+    class: sfWebResponse
+    param:
+      send_http_headers: false
+
+all:
+  routing:
+    class: sfPatternRouting
+    param:
+      generate_shortest_url:            true
+      extra_parameters_as_query_string: true
+
+#all:
+#  controller:
+#    class: sfFrontWebController
+#
+#  request:
+#    class: sfWebRequest
+#    param:
+#      logging:           %SF_LOGGING_ENABLED%
+#      path_info_array:   SERVER
+#      path_info_key:     PATH_INFO
+#      relative_url_root: ~
+#      formats:
+#        txt:  text/plain
+#        js:   [application/javascript, application/x-javascript, text/javascript]
+#        css:  text/css
+#        json: [application/json, application/x-json]
+#        xml:  [text/xml, application/xml, application/x-xml]
+#        rdf:  application/rdf+xml
+#        atom: application/atom+xml
+#
+#  response:
+#    class: sfWebResponse
+#    param:
+#      logging:           %SF_LOGGING_ENABLED%
+#      charset:           %SF_CHARSET%
+#      send_http_headers: true
+#
+#  user:
+#    class: myUser
+#    param:
+#      timeout:         1800
+#      logging:         %SF_LOGGING_ENABLED%
+#      use_flash:       true
+#      default_culture: %SF_DEFAULT_CULTURE%
+#
+#  storage:
+#    class: sfSessionStorage
+#    param:
+#      session_name: symfony
+#
+#  view_cache:
+#    class: sfFileCache
+#    param:
+#      automatic_cleaning_factor: 0
+#      cache_dir:                 %SF_TEMPLATE_CACHE_DIR%
+#      lifetime:                  86400
+#      prefix:                    %SF_APP_DIR%/template
+#
+#  i18n:
+#    class: sfI18N
+#    param:
+#      source:               XLIFF
+#      debug:                false
+#      untranslated_prefix:  "[T]"
+#      untranslated_suffix:  "[/T]"
+#      cache:
+#        class: sfFileCache
+#        param:
+#          automatic_cleaning_factor: 0
+#          cache_dir:                 %SF_I18N_CACHE_DIR%
+#          lifetime:                  31556926
+#          prefix:                    %SF_APP_DIR%/i18n
+#
+#  routing:
+#    class: sfPatternRouting
+#    param:
+#      load_configuration:               true
+#      suffix:                           ''
+#      default_module:                   default
+#      default_action:                   index
+#      debug:                            %SF_DEBUG%
+#      logging:                          %SF_LOGGING_ENABLED%
+#      generate_shortest_url:            false
+#      extra_parameters_as_query_string: false
+#      cache:
+#        class: sfFileCache
+#        param:
+#          automatic_cleaning_factor: 0
+#          cache_dir:                 %SF_CONFIG_CACHE_DIR%/routing
+#          lifetime:                  31556926
+#          prefix:                    %SF_APP_DIR%/routing
+#
+#  logger:
+#    class: sfAggregateLogger
+#    param:
+#      level: debug
+#      loggers:
+#        sf_web_debug:
+#          class: sfWebDebugLogger
+#          param:
+#            level: debug
+#            condition:       %SF_WEB_DEBUG%
+#            xdebug_logging:  true
+#            web_debug_class: sfWebDebug
+#        sf_file_debug:
+#          class: sfFileLogger
+#          param:
+#            level: debug
+#            file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%.log
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/app.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/app.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/app.yml	(revision 4)
@@ -0,0 +1,2 @@
+# default values
+#all:
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/settings.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/settings.yml	(revision 2649)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/settings.yml	(revision 2649)
@@ -0,0 +1,93 @@
+prod:
+  .settings:
+    no_script_name:         false
+    logging_enabled:        false
+
+dev:
+  .settings:
+    error_reporting:        <?php echo (E_ALL | E_STRICT)."\n" ?>
+    web_debug:              true
+    cache:                  false
+    no_script_name:         false
+    etag:                   false
+
+test:
+  .settings:
+    error_reporting:        <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?>
+    cache:                  false
+    web_debug:              false
+    no_script_name:         false
+    etag:                   false
+
+all:
+  .settings:
+    # Form security secret (CSRF protection)
+    csrf_secret:       apostrophePlugin     # Unique secret to enable CSRF protection or false to disable
+
+    # Output escaping settings
+    escaping_strategy:      on            # Determines how variables are made available to templates. Accepted values: on, false.
+    escaping_method:        ESC_SPECIALCHARS # Function or helper used for escaping. Accepted values: ESC_RAW, ESC_ENTITIES, ESC_JS, ESC_JS_NO_ENTITIES, and ESC_SPECIALCHARS.
+
+#all:
+#  .actions:
+#    error_404_module:       default   # To be called when a 404 error is raised
+#    error_404_action:       error404  # Or when the requested URL doesn't match any route
+#
+#    login_module:           default   # To be called when a non-authenticated user
+#    login_action:           login     # Tries to access a secure page
+#
+#    secure_module:          default   # To be called when a user doesn't have
+#    secure_action:          secure    # The credentials required for an action
+#
+#    module_disabled_module: default   # To be called when a user requests 
+#    module_disabled_action: disabled  # A module disabled in the module.yml
+#
+#  .settings:
+#    # Optional features. Deactivating unused features boots performance a bit.
+#    use_database:           on        # Enable database manager. Set to false if you don't use a database.
+#    i18n:                   false       # Enable interface translation. Set to false if your application should not be translated.
+#    check_symfony_version:  false       # Enable check of symfony version for every request. Set to on to have symfony clear the cache automatically when the framework is upgraded. Set to false if you always clear the cache after an upgrade.
+#    compressed:             false       # Enable PHP response compression. Set to on to compress the outgoing HTML via the PHP handler.
+#    check_lock:             false       # Enable the application lock system triggered by the clear-cache and disable tasks. Set to on to have all requests to disabled applications redirected to the $sf_symfony_lib_dir/exception/data/unavailable.php page.
+#
+#    # Routing settings
+#    no_script_name:         false       # Enable the front controller name in generated URLs
+#
+#    # Validation settings, used for error generation by the Validation helper
+#    validation_error_prefix:    ' &darr;&nbsp;'
+#    validation_error_suffix:    ' &nbsp;&darr;'
+#    validation_error_class:     form_error
+#    validation_error_id_prefix: error_for_
+#
+#    # Cache settings
+#    cache:                  false       # Enable the template cache
+#    etag:                   on        # Enable etag handling
+#
+#    # Logging and debugging settings
+#    web_debug:              false       # Enable the web debug toolbar
+#    error_reporting:        <?php echo (E_PARSE | E_COMPILE_ERROR | E_ERROR | E_CORE_ERROR | E_USER_ERROR)."\n" ?> # Determines which events are logged.
+#
+#    # Assets paths
+#    rich_text_js_dir:       js/tiny_mce
+#    admin_web_dir:          /sf/sf_admin
+#    web_debug_web_dir:      /sf/sf_web_debug
+#    calendar_web_dir:       /sf/calendar
+#
+#    # Helpers included in all templates by default
+#    standard_helpers:       [Partial, Cache, Form]
+#
+#    # Activated modules from plugins or from the symfony core
+#    enabled_modules:        [default]
+#
+#    # Charset used for the response
+#    charset:                utf-8
+#
+#    # Miscellaneous
+#    strip_comments:         on         # Remove comments in core framework classes as defined in the core_compile.yml
+#    max_forwards:           5
+#
+#    # Logging
+#    logging_enabled:        true
+#
+#    # i18n
+#    default_culture:        en        # Default user culture
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/frontendConfiguration.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/frontendConfiguration.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/frontendConfiguration.class.php	(revision 4)
@@ -0,0 +1,8 @@
+<?php
+
+class frontendConfiguration extends sfApplicationConfiguration
+{
+  public function configure()
+  {
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/cache.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/cache.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/cache.yml	(revision 4)
@@ -0,0 +1,4 @@
+default:
+  enabled:     false
+  with_layout: false
+  lifetime:    86400
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/routing.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/routing.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/routing.yml	(revision 4)
@@ -0,0 +1,11 @@
+# default rules
+homepage:
+  url:   /
+  param: { module: default, action: index }
+
+default_index:
+  url:   /:module
+  param: { action: index }
+
+default:
+  url:   /:module/:action/*
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/security.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/security.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/security.yml	(revision 4)
@@ -0,0 +1,2 @@
+default:
+  is_secure: false
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/filters.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/filters.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/filters.yml	(revision 4)
@@ -0,0 +1,8 @@
+rendering: ~
+security:  ~
+
+# insert your own filters here
+
+cache:     ~
+common:    ~
+execution: ~
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/view.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/view.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/config/view.yml	(revision 4)
@@ -0,0 +1,17 @@
+default:
+  http_metas:
+    content-type: text/html
+
+  metas:
+    #title:        symfony project
+    #description:  symfony project
+    #keywords:     symfony, project
+    #language:     en
+    #robots:       index, follow
+
+  stylesheets:    [main.css]
+
+  javascripts:    []
+
+  has_layout:     true
+  layout:         layout
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/lib/myUser.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/lib/myUser.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/lib/myUser.class.php	(revision 4)
@@ -0,0 +1,5 @@
+<?php
+
+class myUser extends sfBasicSecurityUser
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/templates/layout.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/templates/layout.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/apps/frontend/templates/layout.php	(revision 4)
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <?php include_http_metas() ?>
+    <?php include_metas() ?>
+    <?php include_title() ?>
+    <link rel="shortcut icon" href="/favicon.ico" />
+  </head>
+  <body>
+    <?php echo $sf_content ?>
+  </body>
+</html>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/symfony
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/symfony	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/symfony	(revision 4)
@@ -0,0 +1,14 @@
+#!/usr/bin/env php
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+chdir(dirname(__FILE__));
+require_once(dirname(__FILE__).'/config/ProjectConfiguration.class.php');
+include(sfCoreAutoload::getInstance()->getBaseDir().'/command/cli.php');
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/propel.ini
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/propel.ini	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/propel.ini	(revision 4)
@@ -0,0 +1,54 @@
+propel.targetPackage       = lib.model
+propel.packageObjectModel  = true
+propel.project             = ##PROJECT_NAME##
+propel.database            = mysql
+propel.database.driver     = mysql
+propel.database.url        = mysql:dbname=##PROJECT_NAME##;host=localhost
+propel.database.creole.url = ${propel.database.url}
+propel.database.user       = root
+propel.database.password   = 
+propel.database.encoding   = utf8
+
+; mysql options
+propel.mysql.tableType     = InnoDB
+
+propel.addVendorInfo       = true
+propel.addGenericAccessors = true
+propel.addGenericMutators  = true
+propel.addTimeStamp        = true
+propel.addValidators       = false
+
+propel.useDateTimeClass       = true
+propel.defaultTimeStampFormat = Y-m-d H:i:s
+propel.defaultTimeFormat      = H:i:s
+propel.defaultDateFormat      = Y-m-d
+
+propel.schema.validate        = false
+propel.samePhpName            = false
+propel.disableIdentifierQuoting     = false
+propel.emulateForeignKeyConstraints = true
+
+; directories
+propel.home                    = .
+propel.output.dir              = ##PROJECT_DIR##
+propel.schema.dir              = ${propel.output.dir}/config
+propel.conf.dir                = ${propel.output.dir}/config
+propel.phpconf.dir             = ${propel.output.dir}/config
+propel.sql.dir                 = ${propel.output.dir}/data/sql
+propel.runtime.conf.file       = runtime-conf.xml
+propel.php.dir                 = ${propel.output.dir}
+propel.default.schema.basename = schema
+propel.datadump.mapper.from    = *schema.xml
+propel.datadump.mapper.to      = *data.xml
+
+; builder settings
+propel.builder.peer.class              = plugins.sfPropelPlugin.lib.builder.SfPeerBuilder
+propel.builder.object.class            = plugins.sfPropelPlugin.lib.builder.SfObjectBuilder
+propel.builder.objectstub.class        = plugins.sfPropelPlugin.lib.builder.SfExtensionObjectBuilder
+propel.builder.peerstub.class          = plugins.sfPropelPlugin.lib.builder.SfExtensionPeerBuilder
+propel.builder.objectmultiextend.class = plugins.sfPropelPlugin.lib.builder.SfMultiExtendObjectBuilder
+propel.builder.mapbuilder.class        = plugins.sfPropelPlugin.lib.builder.SfMapBuilderBuilder
+
+propel.builder.addIncludes  = false
+propel.builder.addComments  = true
+propel.builder.addBehaviors = true
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/vhost.sample
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/vhost.sample	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/vhost.sample	(revision 4)
@@ -0,0 +1,21 @@
+    # Be sure to only have this line once in your configuration
+    NameVirtualHost 127.0.0.1:80
+
+    # This is the configuration for ##PROJECT_NAME##
+    Listen 127.0.0.1:80
+
+    <VirtualHost 127.0.0.1:80>
+      ServerName ##PROJECT_NAME##.localhost
+      DocumentRoot "##SYMFONY_WEB_DIR##"
+      DirectoryIndex index.php
+      <Directory "##SYMFONY_WEB_DIR##">
+        AllowOverride All
+        Allow from All
+      </Directory>
+
+      Alias /sf "##SYMFONY_SF_DIR##"
+      <Directory "##SYMFONY_SF_DIR##">
+        AllowOverride All
+        Allow from All
+      </Directory>
+    </VirtualHost>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/rsync_exclude.txt
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/rsync_exclude.txt	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/rsync_exclude.txt	(revision 4)
@@ -0,0 +1,5 @@
+.svn
+/web/uploads/*
+/cache/*
+/log/*
+/web/*_dev.php
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/databases.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/databases.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/databases.yml	(revision 4)
@@ -0,0 +1,21 @@
+dev:
+  propel:
+    param:
+      classname:  DebugPDO
+
+test:
+  propel:
+    param:
+      classname:  DebugPDO
+
+all:
+  propel:
+    class:        sfPropelDatabase
+    param:
+      classname:  PropelPDO
+      dsn:        mysql:dbname=##PROJECT_NAME##;host=localhost
+      username:   root
+      password:   
+      encoding:   utf8
+      persistent: true
+      pooling:    true
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/ProjectConfiguration.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/ProjectConfiguration.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/ProjectConfiguration.class.php	(revision 4)
@@ -0,0 +1,18 @@
+<?php
+
+if (!isset($_SERVER['SYMFONY']))
+{
+  throw new RuntimeException('Could not find symfony core libraries.');
+}
+
+require_once $_SERVER['SYMFONY'].'/autoload/sfCoreAutoload.class.php';
+sfCoreAutoload::register();
+
+class ProjectConfiguration extends sfProjectConfiguration
+{
+  public function setup()
+  {
+    $this->setPlugins(array('apostrophePlugin'));
+    $this->setPluginPath('apostrophePlugin', dirname(__FILE__).'/../../../..');
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/properties.ini
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/properties.ini	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/config/properties.ini	(revision 4)
@@ -0,0 +1,2 @@
+[symfony]
+  name=##PROJECT_NAME##
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/data/fixtures/fixtures.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/data/fixtures/fixtures.yml	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/data/fixtures/fixtures.yml	(revision 4)
@@ -0,0 +1,17 @@
+# # Populate this file with data to be loaded by your ORM's *:data-load task.
+# # You can create multiple files in this directory (i.e. 010_users.yml,
+# # 020_articles.yml, etc) which will be loaded in alphabetical order.
+# # 
+# # See documentation for your ORM's *:data-load task for more information.
+# 
+# User:
+#   fabien:
+#     username: fabien
+#     password: changeme
+#     name:     Fabien Potencier
+#     email:    fabien.potencier@symfony-project.com
+#   kris:
+#     username: Kris.Wallsmith
+#     password: changeme
+#     name:     Kris Wallsmith
+#     email:    kris.wallsmith@symfony-project.com
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/web/robots.txt
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/web/robots.txt	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/web/robots.txt	(revision 4)
@@ -0,0 +1,2 @@
+#User-agent: *
+#Disallow:
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/web/.htaccess
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/web/.htaccess	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/fixtures/project/web/.htaccess	(revision 4)
@@ -0,0 +1,22 @@
+Options +FollowSymLinks +ExecCGI
+
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+
+  # uncomment the following line, if you are having trouble
+  # getting no_script_name to work
+  #RewriteBase /
+
+  # we skip all files with .something
+  #RewriteCond %{REQUEST_URI} \..+$
+  #RewriteCond %{REQUEST_URI} !\.html$
+  #RewriteRule .* - [L]
+
+  # we check if the .html version is here (caching)
+  RewriteRule ^$ index.html [QSA]
+  RewriteRule ^([^.]+)$ $1.html [QSA]
+  RewriteCond %{REQUEST_FILENAME} !-f
+
+  # no, so we redirect to our front web controller
+  RewriteRule ^(.*)$ index.php [QSA,L]
+</IfModule>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bootstrap/unit.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bootstrap/unit.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bootstrap/unit.php	(revision 4)
@@ -0,0 +1,23 @@
+<?php
+
+if (!isset($_SERVER['SYMFONY']))
+{
+  throw new RuntimeException('Could not find symfony core libraries.');
+}
+
+require_once $_SERVER['SYMFONY'].'/autoload/sfCoreAutoload.class.php';
+sfCoreAutoload::register();
+
+$configuration = new sfProjectConfiguration(dirname(__FILE__).'/../fixtures/project');
+require_once $configuration->getSymfonyLibDir().'/vendor/lime/lime.php';
+
+function apostrophePlugin_autoload_again($class)
+{
+  $autoload = sfSimpleAutoload::getInstance();
+  $autoload->reload();
+  return $autoload->autoload($class);
+}
+spl_autoload_register('apostrophePlugin_autoload_again');
+
+require_once dirname(__FILE__).'/../../config/apostrophePluginConfiguration.class.php';
+$plugin_configuration = new apostrophePluginConfiguration($configuration, dirname(__FILE__).'/../..');
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bootstrap/functional.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bootstrap/functional.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/test/bootstrap/functional.php	(revision 4)
@@ -0,0 +1,21 @@
+<?php
+
+if (!isset($app))
+{
+  $app = 'frontend';
+}
+
+require_once $_SERVER['SYMFONY'].'/autoload/sfCoreAutoload.class.php';
+sfCoreAutoload::register();
+
+function apostrophePlugin_cleanup()
+{
+  sfToolkit::clearDirectory(dirname(__FILE__).'/../fixtures/project/cache');
+  sfToolkit::clearDirectory(dirname(__FILE__).'/../fixtures/project/log');
+}
+apostrophePlugin_cleanup();
+register_shutdown_function('apostrophePlugin_cleanup');
+
+require_once dirname(__FILE__).'/../fixtures/project/config/ProjectConfiguration.class.php';
+$configuration = ProjectConfiguration::getApplicationConfiguration($app, 'test', isset($debug) ? $debug : true);
+sfContext::createInstance($configuration);
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/LICENSE
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/LICENSE	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/LICENSE	(revision 4)
@@ -0,0 +1,22 @@
+--------------------------------------------------------------------------------
+                              apostrophePlugin
+--------------------------------------------------------------------------------
+
+Copyright (c) 2009 P'unk Avenue, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/app.yml.sample
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/app.yml.sample	(revision 1876)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/app.yml.sample	(revision 1876)
@@ -0,0 +1,42 @@
+all:
+  a:
+    # The default routing rules built into a are deliberately
+    # conservative, situating CMS pages in the /cms folder. This recipe
+    # overrides that so that we can set more interesting routing rules in
+    # routing.yml.sample that place CMS pages at / and non-CMS actions at /cms.
+    routes_register: false
+    # Enable slots added by the media plugins
+    slot_types:
+      aImage: Image
+      aSlideshow: Slideshow
+      aVideo: Video
+    # Change to true if you'd like the home page to also appear as a tab
+    home_as_tab: false
+    # Uncomment and change if your media plugin runs on a separate site
+    # media_site: "http://www.mymediasite.com/"
+    # You should change this both here and below
+    media_apikey: 'dummy'
+    # Templates for pages, with their friendly names
+    templates:
+      home: Home Page
+      default: Default Page
+    # Use the provided stylesheet (recommended)
+    use_bundled_stylesheet: true
+  # Media plugin related options
+  aMedia:
+    apikeys:
+      # Must match the API key above
+      - 'dummy'
+    # Recommended
+    apipublic: false
+    admin_credential: media_admin
+    upload_credential: media_upload
+  # Model classes to be indexed by search engine
+  aToolkit:
+    indexes:
+      - 'aPage'
+  aimageconverter:
+    # if netpbm is not in PHP's PATH when system() is invoked, specify its location here
+    # (at the command line, type 'where giftopnm' to find out what folder netpbm is in;
+    # if you do not have it you must install the netpbm utilities on your system)
+    # path: /opt/local/bin # typical netpbm location for macports
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/settings.yml.sample
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/settings.yml.sample	(revision 2649)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/settings.yml.sample	(revision 2649)
@@ -0,0 +1,118 @@
+prod:
+  .settings:
+    no_script_name:         on
+    logging_enabled:        false
+
+dev:
+  .settings:
+    error_reporting:        <?php echo (E_ALL | E_STRICT)."\n" ?>
+    web_debug:              on
+    cache:                  false
+    no_script_name:         false
+    etag:                   false
+
+test:
+  .settings:
+    error_reporting:        <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?>
+    cache:                  false
+    web_debug:              false
+    no_script_name:         false
+    etag:                   false
+
+all:
+  .settings:
+    # Form security secret (CSRF protection)
+    csrf_secret:       false     # Unique secret to enable CSRF protection or false to disable
+
+    # Output escaping settings
+    escaping_strategy:      false            # Determines how variables are made available to templates. Accepted values: on, false.
+    escaping_method:        ESC_SPECIALCHARS # Function or helper used for escaping. Accepted values: ESC_RAW, ESC_ENTITIES, ESC_JS, ESC_JS_NO_ENTITIES, and ESC_SPECIALCHARS.
+
+    # Cache settings
+    lazy_cache_key:         on        # Delays creation of a cache key until after checking whether an action or partial is cacheable
+
+#all:
+#  .actions:
+#    error_404_module:       default   # To be called when a 404 error is raised
+#    error_404_action:       error404  # Or when the requested URL doesn't match any route
+#
+#    login_module:           default   # To be called when a non-authenticated user
+#    login_action:           login     # Tries to access a secure page
+#
+#    secure_module:          default   # To be called when a user doesn't have
+#    secure_action:          secure    # The credentials required for an action
+#
+#    module_disabled_module: default   # To be called when a user requests 
+#    module_disabled_action: disabled  # A module disabled in the module.yml
+#
+#  .settings:
+#    # Optional features. Deactivating unused features boots performance a bit.
+#    use_database:           on        # Enable database manager. Set to false if you don't use a database.
+#    i18n:                   false       # Enable interface translation. Set to false if your application should not be translated.
+#    check_symfony_version:  false       # Enable check of symfony version for every request. Set to on to have symfony clear the cache automatically when the framework is upgraded. Set to false if you always clear the cache after an upgrade.
+#    compressed:             false       # Enable PHP response compression. Set to on to compress the outgoing HTML via the PHP handler.
+#    check_lock:             false       # Enable the application lock system triggered by the clear-cache and disable tasks. Set to on to have all requests to disabled applications redirected to the $sf_symfony_lib_dir/exception/data/unavailable.php page.
+#
+#    # Routing settings
+#    no_script_name:         false       # Enable the front controller name in generated URLs
+#
+#    # Validation settings, used for error generation by the Validation helper
+#    validation_error_prefix:    ' &darr;&nbsp;'
+#    validation_error_suffix:    ' &nbsp;&darr;'
+#    validation_error_class:     form_error
+#    validation_error_id_prefix: error_for_
+#
+#    # Cache settings
+#    cache:                  false       # Enable the template cache
+#    etag:                   on        # Enable etag handling
+#    lazy_cache_key:         false       # Delays creation of a cache key until after checking whether an action or partial is cacheable (defaults to false for backward compatibility)
+#
+#    # Logging and debugging settings
+#    web_debug:              false       # Enable the web debug toolbar
+#    error_reporting:        <?php echo (E_PARSE | E_COMPILE_ERROR | E_ERROR | E_CORE_ERROR | E_USER_ERROR)."\n" ?> # Determines which events are logged.
+#
+#    # Assets paths
+#    rich_text_js_dir:       js/tiny_mce
+#    admin_web_dir:          /sf/sf_admin
+#    web_debug_web_dir:      /sf/sf_web_debug
+#    calendar_web_dir:       /sf/calendar
+#
+#    # Helpers included in all templates by default
+#    standard_helpers:       [Partial, Cache, Form]
+#
+#    # Activated modules from plugins or from the symfony core
+#    enabled_modules:        [default]
+#
+#    # Charset used for the response
+#    charset:                utf-8
+#
+#    # Miscellaneous
+#    strip_comments:         on         # Remove comments in core framework classes as defined in the core_compile.yml
+#    max_forwards:           5
+#
+#    # Logging
+#    logging_enabled:        on
+#
+#    # i18n
+#    default_culture:        en        # Default user culture
+
+# You may also list other modules from other plugins here as needed see the Symfony manual
+    rich_text_fck_js_dir: apostrophePlugin/js/fckeditor
+    enabled_modules:
+      - aCleanLogin
+      - a
+      - aRichText
+      - aText
+      - aMedia
+      - aSlideshow
+      - aVideo
+      - aImage
+      - sfGuardAuth
+      - taggableComplete
+      # If you enable these you must also set up apps/frontend/modules/sfGuardUser/config/security.yml
+      # to properly secure them, otherwise anyone can come along and add more superadmins to your site
+      # - sfGuardUser
+      # - sfGuardGroup
+
+    login_module: sfGuardAuth
+    login_action: signin
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/apostrophePluginConfiguration.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/apostrophePluginConfiguration.class.php	(revision 2979)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/apostrophePluginConfiguration.class.php	(revision 2979)
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * apostrophePlugin configuration.
+ * 
+ * @package     apostrophePlugin * @subpackage  config
+ */
+class apostrophePluginConfiguration extends sfPluginConfiguration
+{
+  /**
+   * @see sfPluginConfiguration
+   */
+  public function initialize()
+  {
+    // These were merged in from the separate plugins. TODO: clean up a little.
+    
+    // There is no more "default" routing outside the sandbox project. We never used it (thus never tested it) and it stopped working 
+    // with the introduction of plugins like the blog that need routes that appear before the a_page route, so it's gone.
+    // It's very simple to write an a_page route at the project level that puts CMS pages somewhere other than
+    // the root, just edit the one that's in our sandbox project routing.yml.
+    
+    // Routes for various admin modules
+    
+    if (sfConfig::get('app_a_admin_routes_register', true))
+    {
+      $this->dispatcher->connect('routing.load_configuration', array('aRouting', 'listenToRoutingAdminLoadConfigurationEvent'));
+    }
+
+    // Allows us to reset static data such as the current CMS page.
+    // Necessary when writing functional tests that use the restart() method
+    // of the browser to start a new request - something that never happens in the
+    // lifetime of the same PHP invocation under normal circumstances
+    $this->dispatcher->connect('test.simulate_new_request', array('aTools', 'listenToSimulateNewRequestEvent'));
+
+    // Register an event so we can add our buttons to the set of global CMS back end admin buttons
+    // that appear when the apostrophe is clicked. We do it this way as a demonstration of how it
+    // can be done in other plugins that enhance the CMS
+    $this->dispatcher->connect('a.getGlobalButtons', array('aTools', 'getGlobalButtonsInternal'));
+    
+    $this->dispatcher->connect('a.getGlobalButtons', array('aMediaCMSSlotsTools', 
+      'getGlobalButtons'));
+      
+    if (sfConfig::get('app_a_media_routes_register', true) && in_array('aMedia', sfConfig::get('sf_enabled_modules', array())))
+    {
+      $this->dispatcher->connect('routing.load_configuration', array('aMediaRouting', 'listenToRoutingLoadConfigurationEvent'));
+    }
+    
+    $this->dispatcher->connect('command.post_command', array('aToolkitEvents',  'listenToCommandPostCommandEvent'));  
+
+    $this->dispatcher->connect('a.get_categorizables', array($this, 'listenToGetCategorizables'));
+    
+    $this->dispatcher->connect('a.get_count_by_category', array($this, 'listenToGetCountByCategory'));
+
+    $this->dispatcher->connect('a.merge_category', array($this, 'listenToCategoryMerge'));
+  }
+  
+  public function listenToGetCategorizables($event, $results)
+  {
+    // You must play nice and append to what is already there
+    $info = array('class' => 'aMediaItem', 'name' => 'Media', 'relation' => 'MediaItems', 'refClass' => 'aMediaItemToCategory');
+    $results['aMediaItem'] = $info;
+    return $results;
+  }
+  
+  // Also includes the above info so we know what the result is referring to
+  public function listenToGetCountByCategory($event, $results)
+  {
+    // You must play nice and append to what is already there
+    $info = array('class' => 'aMediaItem', 'name' => 'Media');
+    $counts = Doctrine::getTable('aMediaItem')->getCountByCategory();
+    $info['counts'] = $counts;
+    $results['aMediaItem'] = $info;
+    return $results;
+  }
+
+  public function listenToCategoryMerge($event)
+  {
+    $parameters = $event->getParameters();
+    Doctrine::getTable('aMediaItemToCategory')->mergeCategory($parameters['old_id'], $parameters['new_id']);
+    Doctrine::getTable('aPageToCategory')->mergeCategory($parameters['old_id'], $parameters['new_id']);
+    Doctrine::getTable('aCategoryUser')->mergeCategory($parameters['old_id'], $parameters['new_id']);
+    Doctrine::getTable('aCategoryGroup')->mergeCategory($parameters['old_id'], $parameters['new_id']);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/doctrine/schema.yml
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/doctrine/schema.yml	(revision 2972)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/doctrine/schema.yml	(revision 2972)
@@ -0,0 +1,821 @@
+# For all tables: use INNODB, which gives us better
+# foreign key handling and support for transactions
+
+options:
+  type: INNODB
+
+aPage:
+  tableName: a_page
+  actAs:
+    - Timestampable
+    - NestedSet
+    - Taggable
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+    slug:
+      # Even though we are not big fans of 1000 character URLs, they are legal in all
+      # browsers and sharp limits on slug length make it very difficult to import
+      # from other CMSes. 1000 is the limit of unique index key lengths in MySQL
+      # (NOTE: this figure must also appear in the specification of the index, below)
+      type: string(1000)
+      # NOTE: we set unique on the index, not the column. If we set it on the column
+      # Doctrine will do it as part of the create of the column, which creates an
+      # implicit index before we ever get to the index section below, and fails because
+      # the key length is not specfied
+    template:
+      type: string(100)
+    # Must log in to see. (Beginning in 1.5 only admins can see when this is true, unless you add other permissions)
+    # Starting in 1.5 this has a proper default but it can be null in older dbs, treat that as false.
+    view_is_secure: 
+      type: boolean
+      default: false
+    # If view_is_secure is true and this is true, "guests and editors" (everone with view_locked permission)
+    # can view the secured page. Otherwise only those specifically granted access can see it (and admins of course)
+    # Defaults to true for easier migration from 1.4 and because this is still the common case (note that this is
+    # only consulted at all when view_is_secure is true)
+    view_guest: 
+      type: boolean
+      default: true
+    # Must be admin to see. Overrides view_is_secure and individual view permissions,
+    # cannot be clobbered by a cascade
+    edit_admin_lock:
+      type: boolean
+      default: false
+    # Must be admin to edit/manage. Overrides individual edit/manage permissions,
+    # cannot be clobbered by a cascade
+    view_admin_lock:
+      type: boolean
+      default: false
+      
+    # Pages will not be returned by searches or the navigation get*Info methods if they have not reached their 
+    # published_at date. No UI for setting this yet in Apostrophe proper (we just set it to the current time), 
+    # but the blog plugin needs it now 
+    published_at:
+      type: timestamp
+      
+    # For historical reasons this means "unpublished" (visible only to those with editing privs for it)
+    archived: boolean
+    # 'admin' pages are hidden from all normal navigation and from search. They are linked to directly from
+    # the apostrophe menu as needed. This is useful when functionality implemented as an engine
+    # is sometimes user-accessible (sites in which the media area is directly browsable as a tab)
+    # and sometimes not (sites in which the media area is exclusively for site editors).
+    # These pages are never returned by getChildren() or the getXYZInfo() methods.
+    admin: 
+      type: boolean
+      default: false
+    author_id:
+      type: integer
+    deleter_id:
+      type: integer
+    engine:
+      type: string(255)
+  indexes:
+    slugindex:
+      fields:
+        # ACHTUNG: this must match the length of the field! Note that
+        # 1000 is the maximum in MySQL. Some databases might ignore this,
+        # that's fine if they also have no limit on unique key lengths
+        slug:
+          length: 1000
+          unique: true
+    engineindex:
+      fields: [engine]
+  relations:
+    Author:
+      class: sfGuardUser
+      foreign: id
+      local: author_id
+      type: one
+    Deleter:
+      class: sfGuardUser
+      foreign: id
+      local: deleter_id
+      type: one
+
+aArea:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  tableName: a_area
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+    page_id:
+      type: integer
+    name:
+      type: string(100)
+    culture:
+      type: string(7)
+    latest_version:
+      type: integer
+  indexes:
+    page_index:
+      fields: [page_id] 
+  relations:
+    Page:
+      type: one
+      class: aPage
+      foreign: id
+      onDelete: cascade
+      local: page_id
+      foreignAlias: Areas
+
+aAreaVersion:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  tableName: a_area_version
+  actAs:
+    - Timestampable
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+    area_id: 
+      type: integer
+    version:
+      type: integer
+    author_id:
+      type: integer
+    diff:
+      type: string(200)
+  indexes:
+    area_index:
+      fields: [area_id] 
+  relations:
+    Area:
+      type: one
+      class: aArea
+      foreign: id
+      onDelete: cascade
+      local: area_id
+      foreignAlias: AreaVersions
+    Author:
+      class: sfGuardUser
+      foreign: id
+      local: author_id
+      type: one
+      # Note that this means history display code must allow for the
+      # possibility of deleted users
+      onDelete: SET NULL
+
+aAreaVersionSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  tableName: a_area_version_slot
+  columns:
+    slot_id:  
+      type: integer
+    area_version_id:  
+      type: integer
+    # Permanently unique id of this subslot within this area
+    permid:
+      type: integer
+      # Handy in fixtures
+      default: 1
+    # Current visual ordering within this area on this page.
+    rank:
+      type: integer
+      default: 1
+  indexes:
+    area_version_index:
+      fields: [area_version_id]
+  relations:
+    AreaVersion:
+      type: one
+      class: aAreaVersion
+      foreign: id
+      onDelete: cascade
+      local: area_version_id
+      foreignAlias: AreaVersionSlots
+    Slot:
+      type: one
+      class: aSlot
+      foreign: id
+      onDelete: cascade
+      local: slot_id
+      foreignAlias: AreaVersionSlots
+
+# Slots are always stored and rendered as HTML.
+# However, templates can specify specific editor options 
+# when rendering them (e.g. the use of a particular FCK toolbar).
+
+aSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  tableName: a_slot
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+      
+    # If type is aRichText, then there
+    # MUST BE an aRichTextSlot module and an aRichTextSlot 
+    # class with Doctrine column aggregation inheritance from aSlot
+    # (although it doesn't actually have to define new columns if it's
+    # happy storing its data entirely in the value string, typically via serialize()).
+    type:
+      type: string(100)
+      
+    # When not null, and present in app_a_slot_variants['type']['variants'], this is set
+    # as a CSS class on the outermost wrapper of the slot. Also, any options specified
+    # in app_a_slot_variants['type']['variants'][$variant]['options'] are passed
+    # to the slot, allowing behavior to be influenced in other slot-dependent ways
+    variant:
+      type: string(100)
+      
+    # Most slots just use this to store their data, often via PHP's serialize() function
+    value:
+      # Allows much larger data than string
+      type: clob
+  relations:
+    MediaItems:
+      class: aMediaItem
+      foreign: media_item_id
+      local: slot_id
+      refClass: aSlotMediaItem
+
+aTextSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aText'
+
+aRichTextSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aRichText'
+
+aRawHTMLSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aRawHTML'
+
+aAccess:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  tableName: a_access
+  columns:
+    page_id: integer
+    # currently just edit or view
+    privilege: string(100)
+    user_id: integer
+  relations:
+    User:
+      class: sfGuardUser
+      foreign: id
+      local: user_id
+      type: one
+      foreignAlias: Accesses
+      onDelete: cascade
+    Page:
+      class: aPage
+      foreign: id
+      local: page_id
+      type: one
+      foreignAlias: Accesses
+      onDelete: cascade
+  indexes:
+    pageindex:
+      fields: [page_id]
+
+aGroupAccess:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  tableName: a_group_access
+  columns:
+    page_id: integer
+    # currently just edit or view
+    privilege: string(100)
+    group_id: integer
+  relations:
+    Group:
+      class: sfGuardGroup
+      foreign: id
+      local: group_id
+      type: one
+      foreignAlias: Accesses
+      onDelete: cascade
+    Page:
+      class: aPage
+      foreign: id
+      local: page_id
+      type: one
+      foreignAlias: GroupAccesses
+      onDelete: cascade
+  indexes:
+    pageindex:
+      fields: [page_id]
+            
+aLuceneUpdate:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  tableName: a_lucene_update
+  columns:
+    page_id:
+      type: integer
+    culture:
+      type: string(7)
+  indexes:
+    page_and_culture_index:
+      fields: [page_id, culture]
+  relations:
+    Page:
+      class: aPage
+      foreign: id
+      local: page_id
+      type: one
+      onDelete: cascade
+
+aImageSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aImage'
+
+aButtonSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aButton'
+
+aSlideshowSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aSlideshow'
+
+aSmartSlideshowSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aSmartSlideshow'
+
+aVideoSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aVideo'
+
+aMediaBrowserSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aMediaBrowser'
+
+aPDFSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aPDF'
+
+aFileSlot:
+  options:
+    symfony:
+      form:   false
+      filter: false
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aFile'
+
+aMediaItem:
+  tableName: a_media_item
+  actAs:
+    Timestampable: ~
+    Taggable: ~
+
+    # For virtual media items that are just croppings of others, we explicitly set slug.
+    # If this item is a crop of another item, the slug will
+    # look like this: original_slug.cropLeft.cropTop.cropWidth.cropHeight
+    # The aMediaBackend/image action then sees these extra
+    # parameters in the route and behaves accordingly
+    # and we can look directly at that file to finish the job
+    
+    # Otherwise we let the behavior do its job
+    
+    # Also, we need a wrapper method to avoid confusing aTools::slugify with
+    # additional parameters passed by the behavior that don't match its own
+
+    Sluggable:
+      fields: [title]
+      unique: true
+      builder: aMediaTools::slugify
+
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+    lucene_dirty:
+      type: boolean
+      default: false
+    # Audio is not implemented
+    type:
+      type: enum
+      notnull: true
+      values: [image, video, audio, pdf]  
+    # URL on YouTube
+    service_url:
+      type: string(200)
+    # Original image format. For video this is the format of the thumbnail
+    # (JPEG). Null if no thumbnail exists. You can ask for any size and any 
+    # format and you'll get it, as a conversion of the original (which is 
+    # then cached). If you ask for the "original" in a different format that 
+    # will result in a conversion as well.
+
+    # We don't use an enum here because this is briefly null when a 
+    # new image is first saved.
+    format:
+      type: string(10)
+      
+    # Preferred still image or video dimensions. For a still image
+    # these are the dimensions of the original. For video they are
+    # the dimensions of the video stream. For PDF they are undefined.
+    width:
+      type: integer
+    height:
+      type: integer
+      
+    # If this field is non-null, it contains HTML embed/object code to
+    # be used without alteration (except for replacing _WIDTH_ and _HEIGHT_)
+    # when embedding the video. This is used to allow embedding of 
+    # video hosted on services whose APIs are not directly supported
+    # by apostrophePlugin (i.e. anything except YouTube, as of this writing).
+    # Note that this can actually be used to embed any scalable 
+    # applet (Flash, Java, etc) supported by embed/object/applet/param tags
+    # although our intention is simply to support black-box Flash players.
+    # 
+    # The user is required to manually supply a thumbnail when 
+    # embedding a video in this way.
+    embed:
+      type: string(1000)
+
+    title:
+      type: string(200)
+      notnull: true
+    description:
+      type: string
+    credit:
+      type: string(200)
+    owner_id:
+      type: integer
+    # This is not implemented in a high-security way at all, you can
+    # still directly access media URLs. This is normal on media sites
+    # for performance reasons.
+    view_is_secure:
+      type: boolean
+      notnull: true
+      default: false
+      
+  relations:
+    Owner:
+      class: sfGuardUser
+      foreign: id
+      local: owner_id
+      type: one
+      onDelete: set null
+      foreignAlias: MediaItems
+    Slots:
+      class: aSlot
+      local: media_item_id
+      foreign: slot_id
+      refClass: aSlotMediaItem
+              
+# A simple relationship between slots and media. Since media are so universal
+# to all of our sites it makes sense to define this relationship in the database 
+# and join with it routinely. The details of the relationship, including
+# rank within a slideshow, are still serialized data in the media slots.
+# Here we just keep what we must have to achieve data integrity and good query performance.
+
+aSlotMediaItem:
+  tableName: a_slot_media_item
+  options:
+    symfony:
+      form:   false
+      filter: false
+  columns:
+    media_item_id:
+      type: integer
+      primary: true
+    slot_id:
+      type: integer
+      primary: true
+  relations:
+    aMediaItem:
+      local: media_item_id
+      onDelete: CASCADE
+    aSlot:
+      local: slot_id
+      onDelete: CASCADE
+     
+aCategory:
+  tableName: a_category
+  actAs:
+    Timestampable: ~
+    Sluggable: ~
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+    name:
+      type: string(255)
+      unique: true
+      
+    description:
+      type: string
+  relations:
+    MediaItems:
+      class: aMediaItem
+      local: category_id
+      foreign: media_item_id
+      foreignAlias: Categories
+      refClass: aMediaItemToCategory
+    # Used to implement engine pages dedicated to displaying one or more
+    # specific categories in media, the blog plugin and beyond
+    Pages:
+      class: aPage
+      local: category_id
+      foreign: page_id
+      foreignAlias: Categories
+      refClass: aPageToCategory
+    # Who is permitted to assign things to this category?
+    Users:
+      foreignAlias: Categories
+      class: sfGuardUser
+      refClass: aCategoryUser
+      local: category_id
+      foreign: user_id
+    Groups:
+      foreignAlias: Categories
+      class: sfGuardGroup
+      refClass: aCategoryGroup
+      local: category_id
+      foreign: group_id      
+      
+aCategoryUser:
+  columns:
+    category_id:
+      type: integer
+      primary: true
+    user_id:
+      type: integer
+      primary: true
+  relations:
+    Category:
+      foreignAlias: CategoryUsers
+      class: aCategory
+      local: category_id
+      onDelete: CASCADE
+    User:
+      foreignAlias: CategoryUsers
+      class: sfGuardUser
+      local: user_id
+      onDelete: CASCADE
+
+aCategoryGroup:
+  columns:
+    category_id:
+      type: integer
+      primary: true
+    group_id:
+      type: integer
+      primary: true
+  relations:
+    Category:
+      foreignAlias: CategoryGroups
+      class: aCategory
+      local: category_id
+      onDelete: CASCADE
+    Group:
+      foreignAlias: CategoryGroups
+      class: sfGuardGroup
+      local: group_id
+      onDelete: CASCADE
+
+aMediaItemToCategory:
+  tableName: a_media_item_to_category
+  options:
+    symfony:
+      form:   false
+      filter: false
+  columns:
+    media_item_id:
+      type: integer
+      primary: true
+    category_id:
+      type: integer
+      primary: true
+  relations:
+    aMediaItem:
+      local: media_item_id
+      onDelete: CASCADE
+    aCategory:
+      local: category_id
+      onDelete: CASCADE
+
+aPageToCategory:
+  tableName: a_page_to_category
+  options:
+    symfony:
+      form:   false
+      filter: false
+  columns:
+    page_id:
+      type: integer
+      primary: true
+    category_id:
+      type: integer
+      primary: true
+  relations:
+    aCategory:
+      local: category_id
+      onDelete: CASCADE
+    aPage:
+      local: page_id
+      onDelete: CASCADE
+      
+aFeedSlot:
+  # Doctrine doesn't produce useful forms with column aggregation inheritance anyway,
+  # and slots often use serialization into the value column... the Doctrine forms are not
+  # of much use here and they clutter the project
+  options:
+    symfony:
+      form:   false
+      filter: false
+
+  # columns:
+  #
+  # You can add columns here. However, if you do not need foreign key relationships it is
+  # often easier to store your data in the 'value' column via serialize(). If you do add columns, 
+  # their names must be unique across all slots in your project, so use a unique prefix 
+  # for your company.
+    
+  # This is how we are able to retrieve slots of various types with a single query from
+  # a single table
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aFeed'
+
+aRedirect:
+  tableName: a_redirect
+  actAs:
+    Timestampable: ~
+    Taggable: ~
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+    page_id:
+      type: integer
+    slug:
+      type: string(255)
+      unique: true
+  indexes:
+    slugindex:
+      fields: [slug]
+  relations:
+    Page:
+      class: aPage
+      foreign: id
+      local: page_id
+      type: one
+      onDelete: cascade
+      
+
+aNewRichTextSlot:
+  # Doctrine doesn't produce useful forms with column aggregation inheritance anyway,
+  # and slots often use serialization into the value column... the Doctrine forms are not
+  # of much use here and they clutter the project
+  options:
+    symfony:
+      form:   false
+      filter: false
+
+  # columns:
+  #
+  # You can add columns here. However, if you do not need foreign key relationships it is
+  # often easier to store your data in the 'value' column via serialize(). If you do add columns, 
+  # their names must be unique across all slots in your project, so use a unique prefix 
+  # for your company.
+    
+  # This is how we are able to retrieve slots of various types with a single query from
+  # a single table
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aNewRichText'
+
+aAudioSlot:
+  # Doctrine doesn't produce useful forms with column aggregation inheritance anyway,
+  # and slots often use serialization into the value column... the Doctrine forms are not
+  # of much use here and they clutter the project
+  options:
+    symfony:
+      form:   false
+      filter: false
+
+  # columns:
+  #
+  # You can add columns here. However, if you do not need foreign key relationships it is
+  # often easier to store your data in the 'value' column via serialize(). If you do add columns, 
+  # their names must be unique across all slots in your project, so use a unique prefix 
+  # for your company.
+    
+  # This is how we are able to retrieve slots of various types with a single query from
+  # a single table
+  inheritance:
+    extends: aSlot
+    type: column_aggregation
+    keyField: type
+    keyValue: 'aAudio'
+
+aEmbedMediaAccount:
+  columns:
+    id:
+      type: integer
+      primary: true
+      autoincrement: true
+    service:
+      type: string(100)
+      notnull: true
+    username:
+      type: string(100)
+      notnull: true
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/routing.yml.sample
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/routing.yml.sample	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/config/routing.yml.sample	(revision 4)
@@ -0,0 +1,16 @@
+# Special case for the home page
+
+homepage:
+  url:  /
+  param: { module: a, action: show, slug: / }
+
+# The non-CMS actions of your project, and the admin actions of the CMS
+a_action:
+  url:   /cms/:module/:action
+
+# Must be the LAST rule
+a_page:
+  url:   /:slug
+  param: { module: a, action: show }
+  requirements: { slug: .* }
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/package.xml.tmpl
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/package.xml.tmpl	(revision 3211)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/package.xml.tmpl	(revision 3211)
@@ -0,0 +1,528 @@
+<?xml version="1.0"?>
+<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.4.1" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
+  <name>apostrophePlugin</name>
+  <channel>plugins.symfony-project.org</channel>
+  <summary>CMS featuring in-context editing, version control, custom slots as Symfony modules</summary>
+  <description>
+Apostrophe is a Symfony and Doctrine-based CMS that emphasizes in-context editing. All slot types are implemented as Symfony modules, with all the flexibility that implies. Version control is implemented, permitting easy rollback of all edits. jQuery is used to implement AJAX features.
+  </description>
+  <lead>
+    <name>Tom Boutell</name>
+    <user>boutell</user>
+    <email>tom@punkave.com</email>
+    <active>yes</active>
+  </lead>
+  <lead>
+    <name>Alex Gilbert</name>
+    <user>agilbert</user>
+    <email>alex@punkave.com</email>
+    <active>yes</active>
+  </lead>
+  <lead>
+    <name>John Benson</name>
+    <user>johnnyoffline</user>
+    <email>johnny@punkave.com</email>
+    <active>yes</active>
+  </lead>
+  <lead>
+    <name>Rick Banister</name>
+    <user>rickybanister</user>
+    <email>rick@punkave.com</email>
+    <active>yes</active>
+  </lead>
+  <developer>
+    <name>Dan Ordille</name>
+    <user>dordille</user>
+    <email>dan@punkave.com</email>
+    <active>yes</active>
+  </developer>
+  <developer>
+    <name>Jake Hiller</name>
+    <user>jakehiller</user>
+    <email>jake@punkave.com</email>
+    <active>yes</active>
+  </developer>
+  <developer>
+    <name>Wesley John-Alder</name>
+    <user>wjohnald</user>
+    <email>wes@punkave.com</email>
+    <active>yes</active>
+  </developer>
+  <developer>
+    <name>Graham Swan</name>
+    <user>thinkswan</user>
+    <email>graham@punkave.com</email>
+    <active>yes</active>
+  </developer>
+  <lead>
+    <name>Geoff DiMasi</name>
+    <user>geoffd</user>
+    <email>geoffd@punkave.com</email>
+    <active>yes</active>
+  </lead>
+  <date>##CURRENT_DATE##</date>
+  <version>
+    <release>##PLUGIN_VERSION##</release>
+    <api>##API_VERSION##</api>
+  </version>
+  <stability>
+    <release>##STABILITY##</release>
+    <api>##STABILITY##</api>
+  </stability>
+  <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+  <notes>-</notes>
+  <contents>
+    ##CONTENTS##
+  </contents>
+  <dependencies>
+    <required>
+      <php>
+        <min>5.2.4</min>
+      </php>
+      <pearinstaller>
+        <min>1.4.1</min>
+      </pearinstaller>
+      <package>
+        <name>symfony</name>
+        <channel>pear.symfony-project.com</channel>
+        <min>1.3.0</min>
+        <max>1.5.0</max>
+        <exclude>1.5.0</exclude>
+      </package>
+    </required>
+  </dependencies>
+  <phprelease/>
+  <changelog>
+    <release>
+      <version>
+        <release>1.5.0</release>
+        <api>1.5.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <date>2011-01-18</date>
+      <notes> 
+        * Manual cropping in the media repository. You can now select images that would not have met the constraints for a particular slot previously, as long as you crop them
+        * Big improvements in performance: CSS and JavaScript are minified and compressed to reduce the number of requests and the amount of traffic, speeding page load and helping the server run more smoothly. A new JavaScript integration strategy improves overall performance as well
+        * Page permissions completely reworked: you can now set âviewâ permissions on a per-page, per-individual-or-group basis. Edit permissions can now be set on a per-group-or-individual basis. Inheritance of permissions from parent pages has been discarded in favor of explicit one-time âcascade to child pagesâ features
+        * Pages you cannot visit due to a lack of privileges are no longer shown in navigation
+        * âAudioâ media type added, allowing you to directly host MP3 files with a nice MP3 player and an audio slot that embeds it
+        * Built-in support for the LESS CSS compiler make stylesheets much easier to maintain
+        * Media types are much more open. For instance there is now an âOfficeâ type for MS Office documents, plus text files and a few other related things. You can override these types via app.yml
+        * We added a âFileâ slot and deprecated the old PDF slot. The âFileâ slot can be used for any downloadable file format you have chosen to allow on the site, such as a Word document
+        * Vimeo, Viddler and SlideShare now gets the same âspecial treatmentâ as YouTube when adding video (integrated search, support for pasting just the URL), and there is a simple way to add support for more such embedded services via plugins. We've also improved our support for "unknown" embed codes
+        * You can set up Vimeo, Viddler, SlideShare and YouTube accounts to be automatically synced to your media repository via a cron job and our new âLinked Accountsâ feature. This too can be extended
+        * Videos can be replaced with new videos, even videos from another service, without editing each slot that uses them
+        * A new âSmart Slideshowâ slot brings in images automatically via categories or tags
+        * Categories have been unified throughout the site; there is a single category admin page. This makes Apostrophe more consistent and extensible
+        * A single âUpload Mediaâ form now accepts all permitted file types, no more separate UIs for images vs. PDFs etc. Uploaded filenames are automatically âhumanizedâ to become a suggested title, saves a great deal of time if your media is already well-labeled
+        * iPhone and other cameras that save orientation hints in JPEGs are now fully compatible, Apostrophe auto-rotates these images
+        * Pages now have meta tag and description fields; Google doesnât care about tags, but our internal search feature can leverage them to produce more relevant results
+        * Eliminated confusing distinction between âtemplate-basedâ and âengineâ pages (engines still exist "under the hood" for developers). There is one page type menu and configuration has been simplified as well
+        * New import-site task accepts XML files and loads a full-fledged Apostrophe site, including conversion of HTML blocks with embedded
+        images into a series of rich text and image slots in an area
+        * Batch import of all filetypes supported by the upgraded media repository (Word, Excel, etc. in addition to images, PDFsâŠ)
+        * Button slots can now feature an optional rich text description for more flexibility
+        * Search has been enhanced. You can now search on specific fields (title, slug, tags, categories, body) and appropriate fields are given extra weight in ordinary search results. Example: title:âmonkey mittensâ
+        * The culture can be part of the page URL for better SEO of internationalized sites
+        * Apostrophe sites can be a subdirectory of an existing site (although we strongly recommend a holistic rethink of your site if you're considering this in most cases)
+        * Better pagination
+        * âThis Pageâ button eliminated in favor of separate âPage Settingsâ and âAdd Pageâ buttons. You can now set all of the properties of a new page at creation time. Managing pages is just plain pleasant
+        * Many other usability improvements
+        * Full names and email addresses are now part of the user management system, not just usernames
+      </notes>
+    </release>
+    <release>
+      <version>
+        <release>1.4.2</release>
+        <api>1.4.2</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <date>2010-09-04</date>
+      <notes> 
+ * The most important fixes here are stability-related. This is a strongly recommended upgrade for all users of the 1.x series of Apostrophe or earlier
+ * Admin: fixed a bug with the cascade page settings form from not displaying when all child pages are unpublished
+ * Button slot: Fixed a few bugs with the button slot. If you set a title and a URL it will output a simple text link. It will also by default NOT output the image's full description, because that's the majority use case. If you want the image's full description along with the button, it can be enabled as an option
+ * Deployment: the apostrophe:deploy task now instructs rsync to checksum files rather than relying on modification times. This is critical when deployment happens from multiple development workstations. As a result we no longer need to clear the APC cache on every deployment, so that feature has been removed from the aSync module. rsync checksum is supposedly slow, but on modern systems performance is quite reasonable
+ * Deployment: new apostrophe:fix-remote-permissions task asks a remote production or staging server to chmod the Symfony-writable folders recursively to address the fact that umask() settings often prevent files created by Apache from being touched by command line tasks that need to do things like rebuilding the search index or syncing media content. Use this task to fix permissions "as Apache" without root access
+ * Documentation: README updated
+ * Documentation: package.xml.tmpl updates
+ * Engine page routing fixes
+ * JavaScript: removed an addJavascript call to jquery.hotkey because we do not use it and it was creating a 404 error
+ * Media: don't try to calculate dimensions if PDF preview is turned off. With netpbm turned off we can't do PDF preview and shouldn't try to fetch the dimensions, which are unknown
+ * Media: never return attributes for logged-out users in the media repository. We might have to revisit this if we decide to offer public filters of some sort that are attribute-based, but right now our attributes are designed for the image selection/management experience and should never be active after you log out. This was not a security hole, just a source of confusion
+ * Search: fixed bug with clear search button
+ * Search: new apostrophe:optimize-search-index task should be run nightly to reoptimize the Zend Search index
+ * Security: the app_aMedia_admin_credential and app_aMedia_upload_credential options were hardcoded to media_admin and media_upload in a bunch of places. All of these cases have been fixed to respect app.yml so you can change the media credentials if you wish
+ * Security: fixed security of aSync module, since it has its own password system it doesn't make sense to lock it with security.yml (also it is disabled by default)
+ * Stability: wrapped tree lock calls around page creation and deletion to address the fact that Doctrine doesn't seem to have concurrency locks for nested set operations. We had previously locked reorganize operations for similar reasons but did not realize that the fundamental insert and delete operations did not have locks either (transactions do not address the same issue)
+ * Stability: the repair-tree task has been overhauled. The task now uses PDO to avoid memory limitations of Doctrine, and is very fast now. There is now a method option which can be set to list or slug. The list option (well-tested) doesn't reorganize a messed-up page tree, but it does correct any errors in lft and rgt values such that it is now safe to manually reorganize it. The slug option infers the page tree from page slugs; this discards order of pages at the same level and doesn't work well if you heavily edit your slugs. A third approach is to specify the csv option, which should be set to a file containing a CSV dump with id, lft, rgt and level values (in that order, with no header) from a known-good database. Pages that did not exist in that good database become archived children of the homepage for easy cleanup in 'reorganize.' See the verbose help for the task for an example of how to create such a CSV file from a known-good backup
+ * Toolkit: don't allow whitespace to balloon in repeated calls to aHtml::simplify()
+ * Toolkit: added PEAR's Date module to 1.4 branch of apostrophe for use in the blog plugin
+ * UI: icon fixes
+ * UI: fixed an IE bug w/ pagination buttons
+</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.4.1</release>
+        <api>1.4.1</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <date>2010-08-06</date>
+      <notes>
+        * Admin: aUserAdmin and aGroupAdmin now use a filter subclass that removes most fields, preventing explosive memory use when interesting relations are added. You can override to change this
+        * Browser: cross-browser cleanup
+        * CSS: CSS cleanup
+        * CSS: Fixed bug where the media library was ignoring use_bundled_stylesheet
+        * CSS: fixed slideshow CSS scoping
+        * CSS: stylesheets should be loaded before javascript files, reversed order of the include calls
+        * Configuration: app_a_default_published added as a more intuitive alternative to app_a_default_on
+        * Development: aArray::isFlat checks whether an array is a flat array: numerically indexed by consecutive integers starting from zero, no ifs ands or buts
+        * Development: aDate::mysql() method takes a PHP timestamp, MySQL datetime or MySQL date (defaulting to now) and returns a MySQL datetime format string suitable for calling setCreatedAt() or similar. Takes advantage of aDate::normalize() &amp; centralizes that pesky date() format string for MySQL in one place
+        * Development: aText::limitWords(): changed ellipsis from three periods to the unicode character
+        * Development: getRealPage fixed: no longer returns the "global" page if there is no real page
+        * Development: updated aHtml::limitWords to accept the append_ellipsis option that aString::limitWord already accepts
+        * Email: single or double quotes in the label of an obfuscated mailto link no longer show up with slashes in front of them in the page
+        * Engines: Fixed an issue with the page property of engine action classes
+        * Engines: aDoctrineRoute now also supports passing engine-slug as a parameter.
+        * Engines: engine-slug parameter now pops the target engine page properly. Thanks to Avi Block
+        * FCK: FCK layout improvements
+        * FCK: turned off the StartupFocus parameter in the FCK config we bundle with Apostrophe, which either didn't work at all or created problems depending on context
+        * Feed Slot: fixed a bug with the title being a link in the feedItem template
+        * Feed Slot: now accepts an itemTemplate similar to the slideshowItemTemplate
+        * Feed Slot: titles as links now work as expected in the feed slot
+        * Feed Slot: updated the aFeed slot to accept options for passing attributes and styles through the feed in addition to just markup
+        * Feed slot: posts limit option correctly passed as a number
+        * I18N: removed all of the text transform styles from the plugin. Those we like are now in the sandbox where you can easily remove them for better I18N
+        * JavaScript: Fixed cross-browser issue with aMultipleSelect
+        * JavaScript: New jquery ui theme
+        * JavaScript: Update jQuery UI to match last stable 1.3.x jQuery release. Moving to 1.4.x will require addressing some incompatibilities
+        * JavaScript: jQuery cleanup
+        * JavaScript: you can now hit Return to save a new item in aMultiSelect
+        * Markup: Fixed accessibility and standard compliance bugs (a few compliance issues remain)
+        * Markup: editPdfSuccess markup corrected
+        * Markup: editVideoSuccess and _editImage partial markup corrected
+        * Media: Added aMediaItem::getImgSrcUrl method
+        * Media: Deprecated use of actual_slug to bring users back from media repository in favor of actual_url; fixed our own examples
+        * Media: added width and height attributes to the image tag output by getEmbedCode in PluginaMediaItem.class.php. This speeds up rendering for all browsers across the board
+        * Media: all media slots (when in areas, not as singleton slots) now display a placeholder div where appropriate
+        * Media: alphabetized media categories in the multiple select drop down
+        * Media: fixed error with slideshows in global and virtual pages, associated with fixed bug in aTools::getRealPage()
+        * Media: fixed undefined index error in slideshow slot when no width parameter is specified
+        * Media: gd backend now recognizes more cases where an image need not be modified, which preseves transparency and improves performance
+        * Media: ghostscript automatically killed after 5 seconds if it is not able to determine PDF dimensions in that time. Works around ghostscript bugs with occasional slightly dodgy PDFs
+        * Navigation: Tab and accordion navigation now outputs ancestor classes consistently
+        * Navigation: better support for the "unpublish an ancestor to create pages that are not visible in navigation elsewhere in the site" technique
+        * Navigation: fixed bug in accordion navigation that caused improper ordering classes to be placed on nav-items when archived pages also existed in navigation element
+        * Navigation: getAccordionInfo no longer returns archived ancestors when $livingOnly is true
+        * Navigation: getAccordionInfo works in all situations
+        * Navigation: getAncestorsInfo now has an optional $livingOnly flag to return only ancestors that are not archived
+        * Performance: new app_a_search_hard_limit option prevents out of memory errors when searching very large sites. If you have 1,000's of pages see the documentation for more information about how to set this
+        * Refactoring: $aPageTable-&gt;checkUserPrivilegesBody() is the core privilege checker method; extend this, calling the base version and adding your own checks, and you won't have to worry about caching or rewriting privilege names, both of which are taken care of by the checkUserPrivileges method first
+        * Refactoring: Fixed missing parent::configure() calls in the media subtype forms. Now you can modify the behavior of *all* media subtype forms by editing the configure() method of the aMediaItemForm class at project level
+        * Refactoring: aPageTable::checkPrivileges is now a wrapper around $aPageTable-&gt;checkUserPrivileges(), which is easier to extend without static method inheritance problems
+        * Refactoring: all form classes and many other classes, such as aTools, now extend a Basea* class so you can override them and extend the base to avoid duplicating code
+        * Refactoring: the privileges portion of the page settings form is broken out to a single allPrivileges partial so that you can easily override that to add or remove parts of it when templating out the page settings form
+        * Samples: removed twoColumnTemplate from app.yml sample
+        * Search: Zend Lucene can throw exceptions if it doesn't like search syntax. Catch the exceptions and report no results
+        * Search: category names now trigger search matches in the media repository
+        * Search: don't let workloads requested by consecutive failed invocations of rebuild-search-index build up a backlog of redundant indexing to be done
+        * Security: breadcrumb partial no longer outputs archived pages when logged out
+        * Security: checkUserPrivileges is now responsible for converting 'edit' to 'edit|manage' in one consistent place
+        * Security: cleaned up logic determining when "this page" button and its contents are rendered. Editors and managers can use "this page" properly
+        * Security: consistent presentation of permissions option in media type forms
+        * Security: custom secureSuccess message in sandbox project when you are logged in with insufficient permissions, behaves like a 404 by default
+        * Security: don't show the heading for the page permissions area if both privilege widgets are disabled for this user 
+        * Security: explicit permissions are not checked for virtual pages (this introduced DQL bugs granting everyone edit permissions to them, you should be using the 'edit' flag to a_area or a_slot to programmatically determine who has rights to a virtual page)
+        * Security: improved privilege cache 
+        * Signin: removed remember me button from the signin form partial since its default behavior in sfDoctrineGuardPlugin is not what users expect (i.e. it doesn't work)
+        * Slots: Added an aUI call after slot is saved to reactivate buttons etc.
+        * Slots: app_a_new_slots_top option now works properly. Thanks to martin79
+        * Slots: editing-now class now removed from slots properly after save
+        * Slots: slots can now be nested more deeply, often needed in the blog plugin
+        * Slugs: Add checks and fixes when renaming a page creates a slug conflict
+        * Slugs: Fixed bug that caused engines to not work properly with utf-8 slugs
+        * Slugs: leading slash required when editing slugs 
+        * Slugs: new require_leading_slash option to aValidatorSlug 
+        * Testing: minor tweaks to the functional testing methods
+        * Variants: The new app_a_allowed_slot_variants setting determines which slot variants are permitted by default. You can always override it with an explicit allowed_variants option in an a_slot or a_area call
+      </notes>
+    </release>
+    <release>
+      <version>
+        <release>1.4.0</release>
+        <api>1.4.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-05-25</date>
+      <notes>
+        * Hooray, you can link directly to an engine page just by passing an engine-slug parameter as one of your route parameters in link_to and url_for! No more aRouteTools::pushTargetEnginePage() (although that is still occasionally useful and fully supported). 
+        * No more hardcoded routing.yml in apostrophePlugin to break people's backend apps. Instead we have a second event which registers when app_a_admin_routes_register is set (that defaults to true), and registers routes for the various admin modules only if they are actually enabled. Much more conservative.
+        * Added the ability to now specify a slideshowItemPartial.php using the slot options. This adds a boatload of flexibility for not overriding the plugin at the project level, but instead enhancing it. It also integrates well with Variants. I switched the compact slideshow to use this technique and it works great.
+        * a_get_option($options, 'height', 500) replaces isset($optionsheight) ? $optionsheight : 500. The former is much less bug-prone and friendlier in templates, which is important because the defaults for a presentation-related option are a presentation decision and therefore should be in the view layer (the template), but view code shouldn't be complicated to understand.
+        * Refactored permissions checks from aPage to aPageTable
+        * Improved handling of virtual pages in search results
+        * No more 404 errors if you click upload without selecting any images, you get a reasonable validation error message instead
+        * new apostrophe:import-files task pulls in JPEGs, GIFs, PNGs and PDFs in the specified folder, which defaults to web/uploads/media_import. Then it REMOVES those files from that folder. 
+        * Refactored SQL migration conveniences to aMigrate where they can be used by migration hooks in other plugins that listen to the apostrophe:migrate event 
+        * Added Fabien's workaround for plugin configuration initialize methods being invoked twice (this is a Symfony issue)
+        * The media repository now behaves reasonably when PDFs are uploaded to a server that can't render previews of PDFs. An icon is substituted, rather than a fake rendering of the PDF, and the format field is set properly. Much better. Hit this with Jake and John this morning. The preview icon is chosen based on the format and is not hardcoded to PDF. You can reliably check for a nonrenderable media item by checking whether $mediaItem-&gt;getWidth() is null. 
+        * If we got valid image info, the image size is less than 1024x768, gd is enabled, and gd supports the image type, always use gd.
+        * edit = false for slot options is no longer ignored when logged in as an admin
+        * The various get*Info methods that return information about related pages now include the page template. This is handy when you want to link to an ancestor page in a special way if it is a landing page.
+        * New aImageConverter::supportsInput($extension) method allows you to check whether aImageConverter can import a particular image format on this system. Mainly used to check for pdf support.
+        * New default behavior of navigation components is appropriate for use on large sites with many pages. On small sites it may be slightly slower, in which case you can set app_a_many_pages to false to get the old "fetch the entire page tree and reuse it in each navigation component as needed" behavior back.
+        * Tabbed navigation now accepts urls from external sites as extra options
+        * Use aTools::isPotentialEditor to determine when to include history browser div.
+        * Pretty signficant changes to the history browser. The history browser now has its own close button instead of piggy-backing on the area cancel button. The area cancel button has now gone away completely because we use the nifty dropdown for Add Slot. And History has it's own set of controls.
+        * fixed bug that caused the icons to disappear from the addSlot dropdown
+        * changed the aAdmin assets file to look for our jQ UI that comes bundled with apostrophe rather than the Lightness UI bundled with jQuery reloaded
+        * getPeerInfo now works properly when the current page is the home page
+        * Allow 1000 character slugs to handle importing existing sites with deep structure. That's the practical limit with MySQL. Carefully specify the index length so MySQL actually pays attention to this. 
+        * Warning comments before various API methods in aPage explaining the need to get the page properly with retrievePageBySlugWithSlots() first. TODO: document this entire subject in the manual. 
+        * One can now specify whether the slot should go to the top or bottom of the area when adding a slot with newAreaVersion (TODO: document this method generally and this option particularly) 
+        * Removed some obsolete, noisy logging calls
+        * the admin generator form markup was outdated and did not reflect the form markup we use via the formFormatter so we updated it to be that way.
+        * updated layout to allow for main navigation in non-cms pages
+        * made normal and alt page settings button icons
+        * revised page settings icons, smaller and brighter
+        * changed delete and history buttons to be flagging buttons with white backgrounds, big visual clutter improvement
+        * cleaned up history icon, still should probably be redone
+        * adjusted styling on button flag-left and flag-right
+        * new page settings icons
+        * added icons for the reorganize tree to display which engines pages are using
+        * created a brand new awesome a-btn.mini button (its rad)
+      </notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.11</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-03-25</date>
+      <notes>
+         * Whoops, last minute typo broke 1.0.10 plaintext slots, fixed in 5 minutes!
+      </notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.10</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-03-25</date>
+      <notes>
+         * Cancel works properly again in the media plugin
+         * Media plugin also clears stale selection attributes when starting a new selection
+         * Plain text slots were double-escaping entities. Fixed.
+         * Plain text slots are now i18n-correct (UTF8).
+         * Harmless but large extra tarballs of older releases no longer in plugin tarball
+         * Button CSS more cross-browser compatible
+      </notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.9</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-03-24</date>
+      <notes>
+         * A computer abandoned by an admin who has logged out can no longer be used to edit slots the admin previously edited using cleverly constructed URLs (only an issue on the same computer and if the PHP session has not ended). Note that you must upgrade your myUser class in apps/frontend/lib to extend aSecurityUser rather than sfGuardSecurityUser to get this fix (aSecurityUser is a subclass of the latter)
+         * Global or virtual-page media slots can be edited successfully on Symfony pages that are not CMS pages
+         * Unpublished pages no longer interfere with aNavigationAccordion layout
+         * Fixtures no longer use HTML tags our filters remove on edit
+         * Plaintext slots now autolink URLs and email address (obfuscated) as described in the manual
+         * Search engine updates refactored, search engine now updates when you save page settings
+         * 'tool' option to rich text slots now correctly activates the FCK toolbar set name you specify
+         * Slot save/cancel buttons now survive form validation passes properly (thanks to Spike)
+         * Date widget is XHTML correct (thanks Spike)
+         * Engines now work when the CMS is not mounted at the root of the site (important for those using the CMS as a subfolder of a site dominated by other Symfony modules)
+         * Attempting to attach a list of zero items to a slideshow no longer results in adding all items in the media repository
+         * Cross-browser and XHTML strictness fixes
+         * Moved lib/base to lib/action (you must symfony cc)
+         * Lost connections between existing media slots and media items when editing other media slots: fixed. Also, slideshows etc. are no longer removed on "cancel," and selecting zero media items no longer selects all media items
+         * i18n of over 99% of the admin interface (many thanks to Quentin, Galileo, Frank, Pablo and Fotis), new languages are regularly being added to the demo project's apps/frontend/i18n folder
+         * More convenient i18n of your site content (temporary titles supplied, all navigation controls work for pages whose titles are not yet translated)
+         * Aesthetic upgrades
+         * Superadmins can grant superadmin status
+         * Some demo-specific styles moved from a.css to demo.css
+         * Optional language selector in a/login partial
+         * Global admin buttons now have separate names and labels (labels can be internationalized) and a documented way to add and reorder them in app.yml
+         * Alpha channel is now preserved when rendering PNGs from a PNG original with gd (not available with netpbm)
+         * Compact PDF slot style, without inline preview (you can override this in aMediaPDF/normalView if you want it back and you have ghostscript)
+         * Better IE6 upgrade message
+         * Various private methods now protected for easier app level overrides
+      </notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.8</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-25</date>
+      <notes>Fix for custom admin generator theme to address security problem found in symfony 1.2, 1.3, and 1.4.</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.7</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-25</date>
+      <notes>Removed obsolete default layout for media repository; those not using the sandbox no longer have to explicitly override use_bundled_layout. Removed obsolete CSS files not used since the pk days. Media library cancel button is easier to see. Slideshows are saved in a way that doesn't crush additional data application-level overrides might be saving. All components and actions classes now overridable and inheritable at the app level. "Download original" and PDf viewing links now work properly.</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.6</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-19</date>
+      <notes>Fixed a problem with slot editing - thanks to Gary Smith - recommended upgrade</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.5</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-17</date>
+      <notes>Moved the manual to trac.apostrophenow.org, reorganized it into multiple pages for easy reading.</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.4</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-17</date>
+      <notes>Documented slot variants. Fixed bugs in slot variants. Variants no longer have to rigorously contradict each other, they always start from the slot's options. Added the allowed_variants option for slots and areas, which allows them to be restricted to those that are suitable to a particular context, and also reordered, changing the default if desired (the first one allowed is the default). Removed 'mkdir -p' call that made generate-slot-type unusable on Windows. Various CSS fixes.</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.3</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-13</date>
+      <notes>Corrected svn URLs in README. Added symfony cc (needed) and symfony plugin:publish-assets (just in case) to the README. Fixed add categories cancel button width, Fixed a-default-value class for selfInputLabel function, some css cleanup. Changed ordering of slots for bundled templates, fixed Edit button getting stuck on, changed button color to be more awesome. Fixed invisible video players when playing video directly in the media repository in a webkit browser.</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.2</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-11</date>
+      <notes>More packaging tweaks no code changes</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.1</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-11</date>
+      <notes>Packaged properly to announce 1.4 compatibility no code changes</notes>
+    </release>
+    <release>
+      <version>
+        <release>1.0.0</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2010-02-11</date>
+      <notes>Too many improvements to list here. Please see the README. Comprehensive renaming of all classes etc.</notes>
+    </release>
+    <release>
+      <version>
+        <release>0.9.3</release>
+        <api>0.9.3</api>
+      </version>
+      <stability>
+        <release>beta</release>
+        <api>beta</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2009-10-18</date>
+      <notes>Additional fixes and improvements inadvertently not included in version 0.9.2. Strongly recommended upgrade.</notes>
+    </release>
+    <release>
+      <version>
+        <release>0.9.2</release>
+        <api>0.9.2</api>
+      </version>
+      <stability>
+        <release>beta</release>
+        <api>beta</api>
+      </stability>
+      <license uri="http://www.apostrophenow.com/home/license">MIT license</license>
+      <date>2009-10-18</date>
+      <notes>Support for "engines" (Symfony modules grafted into the CMS page tree). "Reorganize" feature on the global toolbar allows moving pages around the site in any way you might wish. Many, many other fixes and improvements. Strongly recommended upgrade.</notes>
+    </release>
+  </changelog>
+</package>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/helper/aHelper.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/helper/aHelper.php	(revision 3064)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/helper/aHelper.php	(revision 3064)
@@ -0,0 +1,646 @@
+<?php
+
+// Loading of the a CSS, JavaScript and helpers is now triggered here 
+// to ensure that there is a straightforward way to obtain all of the necessary
+// components from any partial, even if it is invoked at the layout level (provided
+// that the layout does use_helper('a'). 
+
+function _a_required_assets()
+{
+  $response = sfContext::getInstance()->getResponse();
+  $user = sfContext::getInstance()->getUser();
+
+  sfContext::getInstance()->getConfiguration()->loadHelpers(array('Url', 'I18N'));
+
+  // Do not load redundant CSS and JS in an AJAX context. 
+  // These are already loaded on the page in which the AJAX action
+  // is operating. Please don't change this as it breaks or at least
+  // greatly slows updates
+  if (sfContext::getInstance()->getRequest()->isXmlHttpRequest())
+  {
+    return;
+  }
+
+	aTools::addStylesheetsIfDesired();
+
+  aTools::addJavascriptsIfDesired();
+}
+
+_a_required_assets();
+
+function a_slot($name, $type, $options = false)
+{
+  $options = a_slot_get_options($options);
+  $options['type'] = $type;
+	$options['singleton'] = true;
+  aTools::globalSetup($options);
+  include_component("a", "area", 
+    array("name" => $name, "options" => $options)); 
+  aTools::globalShutdown();
+}
+
+function a_area($name, $options = false)
+{
+  $options = a_slot_get_options($options);
+  $options['infinite'] = true; 
+  aTools::globalSetup($options);
+  include_component("a", "area", 
+    array("name" => $name, "options" => $options)); 
+  aTools::globalShutdown();
+}
+
+function a_slot_get_options($options)
+{
+  if (!is_array($options))
+  {
+    if ($options === false)
+    {
+      $options = array();
+    }
+    else
+    {
+      $options = aTools::getSlotOptionsGroup($options);
+    }
+  }
+  return $options;
+}
+
+function a_slot_body($name, $type, $permid, $options, $validationData, $editorOpen, $updating = false)
+{
+  $page = aTools::getCurrentPage();
+  $slot = $page->getSlot($name);
+  $parameters = array("options" => $options);
+  $parameters['name'] = $name;
+  $parameters['type'] = $type;
+  $parameters['permid'] = $permid;
+  $parameters['validationData'] = $validationData;
+  $parameters['showEditor'] = $editorOpen;
+  $parameters['updating'] = $updating;
+  $user = sfContext::getInstance()->getUser();
+  $controller = sfContext::getInstance()->getController();
+  $moduleName = $type . 'Slot';
+  if ($controller->componentExists($moduleName, "executeSlot"))
+  {
+    include_component($moduleName, "slot", $parameters);
+  }
+  else
+  {
+    include_component("a", "slot", $parameters);
+  }
+}
+
+// Frequently convenient when you want to check an option in a template.
+// Doing the isset() ? foo : bar dance over and over is bug-prone and confusing
+
+function a_get_option($array, $key, $default = false)
+{
+  if (isset($array[$key]))
+  {
+    return $array[$key];
+  }
+  else
+  {
+    return $default;
+  }
+}
+
+// THESE ARE DEPRECATED, use the aNavigationComponent instead
+
+function a_navtree($depth = null)
+{
+  $page = aTools::getCurrentPage();
+  $children = $page->getTreeInfo(true, $depth);
+  return a_navtree_body($children);
+}
+
+function a_navtree_body($children)
+{
+  $s = "<ul>\n";
+  foreach ($children as $info)
+  {
+    $s .= '<li>' . link_to($info['title'], aTools::urlForPage($info['slug']));
+    if (isset($info['children']))
+    {
+      $s .= a_navtree_body($info['children']);
+    }
+    $s .= "</li>\n";
+  }
+  $s .= "</ul>\n";
+  return $s;
+}
+
+function a_navaccordion()
+{
+  $page = aTools::getCurrentPage();
+  $children = $page->getAccordionInfo(true);
+  return a_navtree_body($children);
+}
+
+function a_get_stylesheets()
+{
+  $newStylesheets = array();
+  $response = sfContext::getInstance()->getResponse();
+  foreach ($response->getStylesheets() as $file => $options)
+  {
+    if (preg_match('/\.less$/', $file))
+    {
+      $absolute = false;
+      if (isset($options['absolute']) && $options['absolute'])
+      {
+        unset($options['absolute']);
+        $absolute = true;
+      }
+      if (!isset($options['raw_name']))
+      {
+        $file = stylesheet_path($file, $absolute);
+      }
+      $path = sfConfig::get('sf_web_dir') . $file;
+      
+      $dir = aFiles::getUploadFolder(array('asset-cache'));
+      $name = md5($file) . '.less.css';
+      $compiled = "$dir/" . md5($file) . '.less.css';
+      
+      // When minify is turned on we already have a policy that you are responsible for
+      // hitting it with a 'symfony cc' to clear the asset cache if you make changes; so the
+      // only thing we check for is whether the compiled CSS file exists
+      
+      // When minify is not turned on (usually in dev) we should do everything we can to be as 
+      // tolerant as hitting refresh on a page with plain .css files in it would be, so we need to
+      // check the modification time of the .less file against the compiled file
+      
+      if ((!file_exists($compiled)) || ((!sfConfig::get('app_a_minify')) && (filemtime($compiled) < filemtime($path))))
+      {
+        if (!isset($lessc))
+        {
+          $lessc = new lessc();
+        }
+        $lessc->importDir = dirname($path).'/';
+        file_put_contents($compiled, $lessc->parse(file_get_contents($path)));
+      }
+      $newStylesheets['/uploads/asset-cache/' . $name] = $options;
+    }
+    else
+    {
+      $newStylesheets[$file] = $options;
+    }
+  }
+  return _a_get_assets_body('stylesheets', $newStylesheets);
+}
+
+function a_get_javascripts()
+{
+  if (sfConfig::get('app_a_minify', false))
+  {
+    $response = sfContext::getInstance()->getResponse();
+    return _a_get_assets_body('javascripts', $response->getJavascripts());
+  }
+  else
+  {
+    return get_javascripts();
+  }
+}
+
+function _a_get_assets_body($type, $assets)
+{
+  $gzip = sfConfig::get('app_a_minify_gzip', false);
+  sfConfig::set('symfony.asset.' . $type . '_included', true);
+
+  $html = '';
+
+  // We need our own copy of the trivial case here because we rewrote the asset list
+  // for stylesheets after LESS compilation, and there is no way to
+  // reset the list in the response object
+  if (!sfConfig::get('app_a_minify', false))
+  {
+		// This branch is seen only for CSS, because javascript calls the original Symfony
+		// functionality when minify is off
+    foreach ($assets as $file => $options)
+    {
+      $html .= stylesheet_tag($file, $options);
+    }
+    return $html;
+  }
+  
+  $sets = array();
+  foreach ($assets as $file => $options)
+  {
+		if (preg_match('/^http(s)?:/', $file))
+		{
+			// Nonlocal URL. Don't get cute with it, otherwise things
+			// like Addthis don't work
+			if ($type === 'stylesheets')
+			{
+      	$html .= stylesheet_tag($file, $options);
+			}
+			else
+			{
+      	$html .= javascript_include_tag($file, $options);
+			}
+			continue;
+		}
+    /*
+     *
+     * Guts borrowed from stylesheet_tag and javascript_tag. We still do a tag if it's
+     * a conditional stylesheet
+     *
+     */
+
+    $absolute = false;
+    if (isset($options['absolute']) && $options['absolute'])
+    {
+      unset($options['absolute']);
+      $absolute = true;
+    }
+
+    $condition = null;
+    if (isset($options['condition']))
+    {
+      $condition = $options['condition'];
+      unset($options['condition']);
+    }
+
+    if (!isset($options['raw_name']))
+    {
+      if ($type === 'stylesheets')
+      {
+        $file = stylesheet_path($file, $absolute);
+      }
+      else
+      {
+        $file = javascript_path($file, $absolute);
+      }
+    }
+    else
+    {
+      unset($options['raw_name']);
+    }
+
+    if ($type === 'stylesheets')
+    {
+      $options = array_merge(array('rel' => 'stylesheet', 'type' => 'text/css', 'media' => 'screen', 'href' => $file), $options);
+    }
+    else
+    {
+      $options = array_merge(array('type' => 'text/javascript', 'src' => $file), $options);
+    }
+    
+    if (null !== $condition)
+    {
+      $tag = tag('link', $options);
+      $tag = comment_as_conditional($condition, $tag);
+      $html .= $tag . "\n";
+    }
+    else
+    {
+      unset($options['href'], $options['src']);
+      $optionGroupKey = json_encode($options);
+      $set[$optionGroupKey][] = $file;
+    }
+    // echo($file);
+    // $html .= "<style>\n";
+    // $html .= file_get_contents(sfConfig::get('sf_web_dir') . '/' . $file);
+    // $html .= "</style>\n";
+  }
+  
+  // CSS files with the same options grouped together to be loaded together
+
+  foreach ($set as $optionsJson => $files)
+  {
+    $groupFilename = '';
+    foreach ($files as $file)
+    {
+      $groupFilename .= $file;
+      // If your CSS files depend on clever aliases that won't work
+      // through the filesystem, we can get them by http. We're caching
+      // so that's not terrible, but it's usually simpler faster and less
+      // buggy to grab the file content.
+    }
+    // I tried just using $groupFilename as is (after stripping dangerous stuff) 
+    // but it's too long for the OS if you include enough to make it unique
+    $groupFilename = md5($groupFilename);
+    $groupFilename .= (($type === 'stylesheets') ? '.css' : '.js');
+    if ($gzip)
+    {
+      $groupFilename .= 'gz';
+    }
+    $dir = aFiles::getUploadFolder(array('asset-cache'));
+    if (!file_exists($dir . '/' . $groupFilename))
+    {
+      $content  = '';
+      foreach ($files as $file)
+      {
+        $path = null;
+        if (sfConfig::get('app_a_stylesheet_cache_http', false))
+        {
+          $url = sfContext::getRequest()->getUriPrefix() . $file;
+          $fileContent = file_get_contents($url);
+        }
+        else
+        {
+          $path = sfConfig::get('sf_web_dir') . $file;
+          $fileContent = file_get_contents($path);
+        }
+        if ($type === 'stylesheets')
+        {
+          $options = array();
+          if (!is_null($path))
+          {
+            // Rewrite relative URLs in CSS files.
+            // This trick is available only when we don't insist on
+            // pulling our CSS files via http rather than the filesystem
+            
+            // dirname would resolve symbolic links, we don't want that
+            $fdir = preg_replace('/\/[^\/]*$/', '', $path);
+            $options['currentDir'] = $fdir;
+            $options['docRoot'] = sfConfig::get('sf_web_dir');
+          }
+          if (sfConfig::get('app_a_minify', false))
+          {
+            $fileContent = Minify_CSS::minify($fileContent, $options);
+          }
+        }
+        else
+        {
+          // Trailing carriage return makes behavior more consistent with
+          // JavaScript's behavior when loading separate files. For instance,
+          // a missing trailing semicolon should be tolerated to the same
+          // degree it would be with separate files. The minifier is not
+          // a lint tool and should not surprise you with breakage
+          $fileContent = JSMin::minify($fileContent) . "\n";
+        }
+        $content .= $fileContent;
+      }
+      if ($gzip)
+      {
+        _gz_file_put_contents($dir . '/' . $groupFilename . '.tmp', $content);
+      }
+      else
+      {
+        file_put_contents($dir . '/' . $groupFilename . '.tmp', $content);
+      }
+      @rename($dir . '/' . $groupFilename . '.tmp', $dir . '/' . $groupFilename);
+    }
+    $options = json_decode($optionsJson, true);
+    // Use stylesheet_path and javascript_path so we can respect relative_root_dir
+    if ($type === 'stylesheets')
+    {
+      $options['href'] = stylesheet_path('/uploads/asset-cache/' . $groupFilename);
+      $html .= tag('link', $options);
+    }
+    else
+    {
+      $options['src'] = javascript_path('/uploads/asset-cache/' . $groupFilename);
+      $html .= content_tag('script', '', $options); 
+    }
+  }
+  return $html;
+}
+
+function a_include_stylesheets()
+{
+  echo(a_get_stylesheets());
+}
+
+function a_include_javascripts()
+{
+  echo(a_get_javascripts());
+}
+
+function _gz_file_put_contents($file, $contents)
+{
+  $fp = gzopen($file, 'wb');
+  gzwrite($fp, $contents);
+  gzclose($fp);
+}
+
+// Call like this:
+
+// a_js_call('object.property[?].method(?, ?)', 5, 'name', 'bob')
+
+// That is, use ?'s to insert correctly json-encoded arguments into your JS call.
+
+// Another, less-contrived example:
+
+// a_js_call('apostrophe.slideshowSlot(?)', array('id' => 'et-cetera', ...))
+
+// Notice that arguments can be strings, numbers, or arrays - JSON can handle all of them.
+
+// All calls made in this way are accumulated into a jQuery domready block which
+// appears at the end of the body element in our standard layout.php via a_include_js_calls.
+// We also insert these at the end when adding or updating a slot via AJAX. You can invoke it
+// yourself in other layouts etc.
+
+function a_js_call($callable /* , $arg1, $arg2, ... */ )
+{
+  $args = array_slice(func_get_args(), 1);
+  a_js_call_array($callable, $args);
+}
+
+function a_js_call_array($callable, $args)
+{
+  aTools::$jsCalls[] = array('callable' => $callable, 'args' => $args);
+}
+
+function a_include_js_calls()
+{
+  echo(a_get_js_calls());
+}
+
+function a_get_js_calls()
+{
+  $html = '';
+  if (count(aTools::$jsCalls))
+  {
+    $html .= '<script type="text/javascript" charset="utf-8">' . "\n";
+    $html .= '$(function() {' . "\n";
+    foreach (aTools::$jsCalls as $call)
+    {
+      $html .= _a_js_call($call['callable'], $call['args']);
+    }
+    $html .= '});' . "\n";
+    $html .= '</script>' . "\n";
+  }
+  return $html;
+}
+
+function _a_js_call($callable, $args)
+{
+  $clauses = preg_split('/(\?)/', $callable, null, PREG_SPLIT_DELIM_CAPTURE);
+  $code = '';
+  $n = 0;
+  $q = 0;
+  foreach ($clauses as $clause)
+  {
+    if ($clause === '?')
+    {
+      $code .= json_encode($args[$n++]);
+    }
+    else
+    {
+      $code .= $clause;
+    }
+  }
+  if ($n !== count($args))
+  {
+    throw new sfException('Number of arguments does not match number of ? placeholders in js call');
+  }
+  return $code . ";\n";
+}
+
+// i18n with less effort. Also more flexibility for the future in how we choose to do it  
+function a_($s, $params = null)
+{
+  return __($s, $params, 'apostrophe');
+}
+
+// One consistent encoding is needed for non-HTML output in our templates, since we do not assume
+// that Symfony is in escaping mode, and the correct statement is so verbose
+
+function a_entities($s)
+{
+  return htmlentities($s, ENT_COMPAT, 'UTF-8');
+}
+
+function a_link_button($label, $symfonyUrl, $options = array(), $classes = array(), $id = null)
+{
+  return a_button($label, url_for($symfonyUrl, $options), $classes, $id);
+}
+
+function a_button($label, $url, $classes = array(), $id = null, $name = null, $title = null)
+{
+  $hasIcon = in_array('icon', $classes);
+	$aLink = in_array('a-link', $classes);
+	$arrowBtn = in_array('a-arrow-btn', $classes);
+	
+	// if it's an a-events button, grab the date and append it as a class
+	$aEvents = in_array('a-events', $classes);
+	if ($aEvents) {
+		$classes[] = 'day-'.date('j');
+	}
+	
+  $s = '<a ';
+  if (!is_null($name))
+  {
+    $s .= 'name="' . a_entities($name) . '" ';
+  }
+  if (!is_null($title))
+  {
+    $s .= 'title="' . a_entities($title) . '" ';
+  }
+  $s .= 'href="' . a_entities($url) . '" ';
+  if (!is_null($id))
+  {
+    $s .= 'id="' . a_entities($id) . '" ';
+  }
+
+	if (!$aLink && !$arrowBtn) {
+	  $s .= 'class="a-btn ' . implode(' ', $classes) . '">';
+	}
+	else
+	{
+		// a-link shares similar physical characteristic to a-btn
+		// but they avoid the aeshetic styling of a-btn entirely
+  	$s .= 'class="' . implode(' ', $classes) . '">';
+	}
+
+  if ($hasIcon)
+  {
+    $s .= '<span class="icon"></span>';
+  }
+  $s .= a_($label) . '</a>';
+  return $s;
+}
+
+// For a button that will have an icon, specify the icon class.
+
+// Common cases to be aware of: 
+
+// For a cancel button use the a-cancel class (if you also specify the icon class you get an x)
+
+// Do not use for submit buttons. Due to longstanding problems with JS submit() 
+// calls not being able to invoke both JavaScript handlers and the native submit 
+// behavior in the correct way it is usually eventually necessary to use a real 
+// submit button. Use a_submit_button to get one of those styled in the standard 
+// Apostrophe way.
+
+function a_js_button($label, $classes = array(), $id = null)
+{
+  return a_button($label, '#', $classes, $id);
+}
+
+// Even more convenient way to do a cancel button based on the above
+function a_js_cancel_button($label = null, $classes = array(), $id = null)
+{
+  if (is_null($label))
+  {
+    $label = a_('Cancel');
+  }
+  $classes[] = 'a-cancel';
+  return a_js_button($label, $classes, $id);
+}
+
+// A real submit button, styled for Apostrophe.
+// Should not need an id - we style these things by
+// class so there can be more than one on a page, right?
+
+function a_submit_button($label, $classes = array(), $name = null)
+{
+  $s = '<input type="submit" value="' . a_entities($label) . '" class="a-btn a-submit ' . implode(' ', $classes) . '" ';
+  if (!is_null($name))
+  {
+    $s .= 'name="' . a_entities($name) . '" ';
+  }
+  $s .= '/>';
+  return $s;
+}
+
+// TODO: having the options here be the reverse of the options to
+// a_button is absurd and we need an options array for both of them.
+// For now this is more backward compatible
+
+// An anchor tag 'submit button', styled for Apostrophe
+// and configured behind the scenes to autosubmit the form when clicked 
+// like a real submit button would. However, this should
+// NOT be used in AJAX forms, because there is no consistent
+// way to avoid triggering the native submit behavior of
+// the form. For AJAX forms use real submit buttons
+// or attach the desired submit behavior directly to the button
+
+// A submit button should never need an id because you style them
+// by class - on the other hand it often needs a name so it can
+// be distinguished from other submit buttons when the form submission
+// is received, just like a normal submit button
+
+// You will often want to add the a-submit class, but not always as it's
+// not always the visual impact you want
+
+function a_anchor_submit_button($label, $classes = array(), $name = null, $id = null)
+{
+  $classes[] = 'a-btn';
+  $classes[] = 'a-act-as-submit';
+  return a_button($label, '#', $classes, $id, $name);
+}
+
+// A button that removes a filter (parameter) from the given URL.
+// Uses the "label followed by an x" style. $parameter can be an array of
+// several parameter names. Calls link_to on the URL. This means you can pass an easily manipulated 
+// Symfony URL with &-separated params but get a user friendly routed URL as final output.
+// This ought to call a_button but I'm wrestling with the incompatibility of inline
+// content and a_button's CSS. Notice that it's playing out rather well in the blog engine. -Tom
+
+function a_remove_filter_button($label, $url, $parameter)
+{
+  if (!is_array($parameter))
+  {
+    $parameter = array($parameter);
+  }
+  $remove = array();
+  foreach ($parameter as $p)
+  {
+    // aUrl::addParams removes when the value is blank
+    $remove[$p] = '';
+  }
+  $url = aUrl::addParams($url, $remove);
+  return link_to($label . image_tag('/apostrophePlugin/images/a-icon-close-small-simple.png'), url_for($url), array('class' => 'a-filter-link', 'title' => 'Remove Filter'));
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/helper/FeedHelper.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/helper/FeedHelper.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/helper/FeedHelper.php	(revision 4)
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * feedHelper
+ * http://spindrop.us/2006/07/04/dynamic-linking-to-syndication-feeds-with-symfony/
+ *
+ * @author Dave Dash 
+ */
+
+function include_feeds()
+{
+  $type = 'rss';
+  $already_seen = array();
+  foreach (sfContext::getInstance()->getRequest()->getAttribute('helper/asset/auto/feed', array()) as $files)
+  {
+    if (!is_array($files))
+    {
+      $files = array($files);
+    }
+    foreach ($files as $file)
+    {
+      if (isset($already_seen[$file])) continue;
+      $already_seen[$file] = 1;
+      echo tag('link', array('rel' => 'alternate', 'type' => 'application/'.$type.'+xml', 'title' => ucfirst($type), 'href' => url_for($file, true)));
+    }
+  }
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aTestTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aTestTools.class.php	(revision 786)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aTestTools.class.php	(revision 786)
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * A collection of static methods useful for making tests easier to write.
+ */
+class aTestTools
+{
+  public static function randomString($length)
+  {
+    $string = '';
+    for ($i=0;$i<$length;$i++)
+    {
+      $string = chr(rand(97,122));
+    }
+  }
+  
+  static protected $test;
+  static protected $configuration;
+  
+  static public function loadData($test = null, $configuration = null)
+  {
+    self::$test = $test;
+    self::$configuration = $configuration;
+    
+    // Load ALL the fixtures, including plugin fixtures, not just the app level fixtures!
+  
+    // ALSO: we only load the fixtures on the first functional test in a set. The rest of the time we
+    // cheat and use a mysqldump file. There are various reasons why we cannot assume it is safe to 
+    // do this in the general case, including: modified fixtures, modified schemas, modified behaviors,
+    // and modified Doctrine. But for consecutive runs in the same PHP invocation, we definitely
+    // don't need to start from scratch!
+    
+    $root = sfConfig::get('sf_root_dir');
+    $writable = aFiles::getWritableDataFolder();
+    $cache = "$writable/test-fixtures-cache.sql";
+
+    $reload = false;
+  
+    // TODO: I should check to see if a PID still exists somewhere to figure out if this is stale
+    if (file_exists("$writable/test_set_pid"))
+    {
+      $processes = self::getProcesses();
+      $pid = trim(file_get_contents("$writable/test_set_pid"));
+      if (!isset($processes[$pid]))
+      {
+        // Stale
+        unlink("$writable/test_set_pid");
+        if (file_exists("$writable/test_set_first"))
+        {
+          unlink("$writable/test_set_first");
+        }
+        self::info("$writable/test_set_pid is stale, reloading fixtures");
+        $reload = true;
+      }
+      else
+      {
+        if (file_exists("$writable/test_set_first"))
+        {
+          $reload = true;
+          self::info("first test in set, reloading fixtures");
+          unlink("$writable/test_set_first");
+        }
+        else
+        {
+          self::info("later test in set, will not reload fixtures");
+        }
+      }
+    }
+    else
+    {
+      self::info("Not part of a test set, will reload fixtures");
+      $reload = true;
+    }
+
+    if (!$reload)
+    {
+      $reload = (!file_exists($cache)) || (filesize($cache) == 0);
+    }
+  
+    $params = self::getDatabaseParams();
+
+    // Attempt at autodetect that a reload is needed. This doesn't work well enough
+    // because of modified schemas, modified behaviors, and modified Doctrine
+  
+    // if (!$reload)
+    // {
+    //   $fixtures = glob("$root/data/fixtures/*.yml");
+    //   $plugins = sfContext::getInstance()->getConfiguration()->getPlugins();
+    //   foreach ($plugins as $plugin)
+    //   {
+    //     $pluginFixtures = glob("$root/plugins/$plugin/data/fixtures/*.yml");
+    //     $fixtures = array_merge($fixtures, $pluginFixtures);
+    //   }
+    //   foreach ($fixtures as $fixture)
+    //   {
+    //     if (filemtime($cache) < filemtime($fixture))
+    //     {
+    //       $reload = true;
+    //       break;
+    //     }
+    //   }
+    // }
+
+    if ($reload)
+    {
+      self::info("Reloading fixtures and rebuilding model, filters and forms");
+      self::info("(We would rather just reload fixtures but data-load does not clean up tables if there are are no entries for those tables in the fixtures, and that leaves traces of things created in those tables by previous tests. A doctrine:data-reload task is needed.)");
+      system(escapeshellarg(sfConfig::get('sf_root_dir') . '/symfony') . ' doctrine:build --all --db --and-load --env=test --no-confirmation', $result);
+      self::info("Reloaded fixtures");
+      if ($result !== 0)
+      {
+        throw new sfException("Error loading data");
+      }
+      // Cache for next time
+      $sh = 'mysqldump ' . self::shellDBParams($params) . ' > ' . escapeshellarg($cache);
+      system($sh, $result);
+      self::info("Cached fixtures with $sh");
+      if ($result != 0)
+      {
+        throw new sfException("Error dumping fixtures to cache");
+      }
+    }
+    else
+    {
+      self::info("Reloading fixtures data from $cache");
+      system('mysql ' . self::shellDBParams($params) . ' < ' . escapeshellarg($cache), $result);
+      if ($result != 0)
+      {
+        throw new sfException("Error reloading fixtures from cache");
+      }
+    }
+    return;
+  }
+
+  static public function getDatabaseParams()
+  {
+    echo("appConfig\n");
+    $appConfig = self::$configuration ? self::$configuration : sfContext::getInstance()->getConfiguration();
+    echo("appManager\n");
+    $dbManager = new sfDatabaseManager($appConfig);
+    echo("after appManager\n");
+    $names = $dbManager->getNames();
+    $db = $dbManager->getDatabase($names[0]);
+    $username = $db->getParameter('username');  //root
+    $dsn = $db->getParameter('dsn');  //mysql:dbname=mydbtest;host=localhost because it's test config
+    $password = $db->getParameter('password');  //password
+    if (!preg_match('/^mysql:(.*)\s*$/', $dsn, $matches))
+    {
+      throw new sfException("I don't understand the DSN $dsn, sorry");
+    }
+    $pairs = explode(';', $matches[1]);
+    $data = array();
+    foreach ($pairs as $pair)
+    {
+      list($key, $val) = explode('=', $pair);
+      $data[$key] = $val;
+    }
+    $data['username'] = $username;
+    $data['password'] = $password;
+    return $data;
+  }
+  
+  static public function shellDBParams($params)
+  {
+    return '-u ' . escapeshellarg($params['username']) . ' -p' . escapeshellarg($params['password']) . ' -h ' . escapeshellarg($params['host']) . ' ' . escapeshellarg($params['dbname']);
+  }
+  
+  // Yes, this is a hack
+  static public function info($m)
+  {
+    if (isset(self::$test))
+    {
+      self::$test->info($m);
+    }
+    else
+    {
+      // Unit test, we have no context to call anything else
+      echo("$m\n");
+    }
+  }
+  
+  // Bound to be useful somewhere else
+  static public function getProcesses()
+  {
+    $processes = array();
+    $in = popen("ps -eo pid,command", "r");
+    $data = stream_get_contents($in);
+    pclose($in);
+    $data = preg_split("/\n/", $data);
+    foreach ($data as $line)
+    {
+      if (preg_match("/^\s*(\d+)\s+(.*?)\s*$/", $line, $matches))
+      {
+        // Works across Linux and MacOS X mileage elsewhere may vary
+        $processes[$matches[1]] = $matches[2];        
+      }
+    }
+    return $processes;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aTestFunctional.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aTestFunctional.class.php	(revision 786)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aTestFunctional.class.php	(revision 786)
@@ -0,0 +1,91 @@
+<?php
+
+class aTestFunctional extends sfTestFunctional
+{
+  public function __construct(sfBrowserBase $browser, lime_test $lime = null, $testers = array())
+  {
+    parent::__construct($browser, $lime, $testers);
+    aTestTools::loadData($this);
+  }
+  
+  protected $options = array(
+    'login-button-text' => 'Sign In',
+    'login-url' => '/login',
+    'default-prefix' => '/cms/'
+  );
+  
+  public function setOptions($options = array())
+  {
+    $this->options = array_merge($this->options, $options);
+  }
+
+  // This isn't full-scale routing, it just prepends the appropriate prefix to the
+  // URL. That's /cms/ if we're running with the default route as a mere plugin, 
+  // or /admin/ if we're running from the sandbox project
+  public function route($route)
+  {
+    return $this->options['default-prefix'] . $route;
+  }
+  
+  public function loadData($path = null)
+  {
+    if (!$path)
+    {
+      $path = sfConfig::get('sf_test_dir').'/fixtures';
+    }
+    
+    Doctrine::loadData($path);
+ 
+    return $this;
+  }
+
+  public function login($username = 'admin', $password = null)
+  {
+    if (!$password)
+    {
+      $password = $username;
+    }
+    
+    return $this->
+      get($this->options['login-url'])->
+      setField('signin[username]', $username)->
+      setField('signin[password]', $password)->
+      click($this->options['login-button-text'], array('_with_csrf' => true))->
+      with('response')->isRedirected()->
+      followRedirect()
+    ;
+  }
+  
+  public function loginFailed($username = 'user_1', $password = null)
+  {
+    if (!$password)
+    {
+      $password = $username;
+    }
+    
+    return $this->
+      get($this->options['login-url'])->
+      setField('signin[username]', $username)->
+      setField('signin[password]', $password)->
+      click('sign in', array('_with_csrf' => true))->
+      with('response')->begin()->
+        isStatusCode(200)->
+        contains('The username and/or password is invalid')->
+      end()
+    ; 
+  }
+
+  public function createPage($parentSlug, $pageTitle)
+  {
+    // submit parent (a slug) and title to aContextCMS/create via POST
+    return $this->
+      post($this->route('a/create'), array('parent' => $parentSlug, 'title' => $pageTitle))->
+      with('response')->begin()->
+        isRedirected()->followRedirect()->
+      end()->
+      with('request')->begin()->
+        isParameter('module', 'a')->
+        isParameter('action', 'show')->
+      end();
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aBrowser.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aBrowser.class.php	(revision 640)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/test/aBrowser.class.php	(revision 640)
@@ -0,0 +1,39 @@
+<?php
+
+class aBrowser extends sfBrowser
+{
+  public function restart()
+  {
+    parent::restart();
+    $this->newRequestReset();
+    
+    return $this;
+  }
+  
+  public function call($uri, $method = 'get', $parameters = array(), $changeStack = true)
+  {
+    parent::call($uri, $method, $parameters, $changeStack);
+    $this->newRequestReset();
+    
+    return $this;
+  }
+  
+  public function newRequestReset()
+  {
+    $this->clearTableIdentityMaps();
+    $dispatcher = sfContext::getInstance()->getConfiguration()->getEventDispatcher();
+    $dispatcher->notify(new sfEvent(null, 'test.simulate_new_request'));
+  }
+    
+  protected function clearTableIdentityMaps()
+  {
+    $c = Doctrine_Manager::getInstance()->getCurrentConnection();
+
+    $tables = $c->getTables();
+
+    foreach ($tables as $table) 
+    {
+      $table->clear();
+    }
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryDateTime.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryDateTime.class.php	(revision 2696)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryDateTime.class.php	(revision 2696)
@@ -0,0 +1,71 @@
+<?php
+
+
+class aWidgetFormJQueryDateTime extends sfWidgetFormDateTime
+{
+  
+  protected $dateWidget;
+  protected $timeWidget;
+
+    
+  protected function configure($options = array(), $attributes = array())
+  {    
+    $this->addOption('date', array());
+    $this->addOption('time', array());
+    $this->addOption('with_time', true);
+    $this->addOption('format', '%date% %time%');
+  }
+  
+  public function render($name, $value = null, $attributes = array(), $errors = array())
+  {
+		$date = $this->getDateWidget($attributes)->render($name, $value);
+
+    if(!$this->getOption('with_time', true))
+    {
+      $value = '';
+    }
+
+    return strtr($this->getOption('format'), array(
+      '%date%' => $date,
+      '%time%' => $this->getTimeWidget()->render($name, $value, $this->getAttributesFor('time', $attributes)),
+    ));
+  }
+
+  /**
+   * Returns the date widget.
+   *
+   * @param  array $attributes  An array of attributes
+   *
+   * @return sfWidgetForm A Widget representing the date
+   */
+  protected function getDateWidget($attributes = array())
+  {
+    return new aWidgetFormJQueryDate($this->getOptionsFor('date'), $this->getAttributesFor('date', $attributes));
+  }
+  /**
+   * Returns the time widget.
+   *
+   * @param  array $attributes  An array of attributes
+   *
+   * @return sfWidgetForm A Widget representing the time
+   */
+  protected function getTimeWidget($attributes = array())
+  {
+    return new aWidgetFormJQueryTime($this->getOptionsFor('time'), $this->getAttributesFor('time', $attributes));
+  }
+
+  /**
+   * Returns an array of HTML attributes for the given type.
+   *
+   * @param  string $type        The type (date or time)
+   * @param  array  $attributes  An array of attributes
+   *
+   * @return array  An array of HTML attributes
+   */
+  protected function getAttributesFor($type, $attributes)
+  {
+    $defaults = isset($this->attributes[$type]) ? $this->attributes[$type] : array();
+
+    return isset($attributes[$type]) ? array_merge($defaults, $attributes[$type]) : $defaults;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormStaticText.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormStaticText.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormStaticText.class.php	(revision 4)
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * aWidgetFormStaticText presents static text in a widget. This should always be paired with an sfValidatorPass validator.
+ *
+ * These widgets are used to provide detailed information interleaved with widgets without the need to template out the entire form.
+ *
+ * Sample usage:
+ *
+ * $housingDetails = $this->event->getHousingDetails();
+ * if ($housingDetails)
+ * {
+ *   $this->setWidget('housing_details', new aWidgetFormStaticText($housingDetails));
+ *   $this->setValidator('housing_details', new sfValidatorPass());
+ *   $this->widgetSchema->moveField('housing_details', sfWidgetFormSchema::BEFORE, 'housing');
+ * }
+ *
+ */
+class aWidgetFormStaticText extends sfWidgetFormInput
+{
+  protected $label;
+  
+  // The label is the only reason this widget exists, so don't bother using an option for it,
+  // the constructor is more succinct
+  public function __construct($label)
+  {
+    $this->label = $label;
+    parent::__construct();
+  }
+  
+  /**
+   * @param  string $name        The element name
+   * @param  string $value       The value displayed in this widget
+   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes (the name attribute will be removed)
+   * @param  array  $errors      An array of errors for the field (ignored here)
+   *
+   * @return string An HTML tag string
+   *
+   * @see sfWidgetForm
+   */
+  public function render($name, $value = null, $attributes = array(), $errors = array())
+  {
+    unset($attributes['name']);
+    // Always ignore the passed value, which will vary if there are multiple validation passes.
+    // We just want to output the label we were created with.
+    return $this->renderContentTag('div', htmlspecialchars($this->label), $attributes);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryDate.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryDate.class.php	(revision 2761)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryDate.class.php	(revision 2761)
@@ -0,0 +1,186 @@
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * aWidgetFormJQueryDate represents a date widget rendered by JQuery UI.
+ *
+ * This widget needs JQuery and JQuery UI to work.
+ *
+ * @package    symfony
+ * @subpackage widget
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id: aWidgetFormJQueryDate.class.php 12875 2008-11-10 12:22:33Z fabien $
+ */
+class aWidgetFormJQueryDate extends sfWidgetFormDate
+{
+  /**
+   * Configures the current widget.
+   *
+   * Available options:
+   *
+   *  * image:   The image path to represent the widget (false by default)
+   *  * config:  A JavaScript array that configures the JQuery date widget
+   *  * culture: The user culture
+   *
+   * @param array $options     An array of options
+   * @param array $attributes  An array of default HTML attributes
+   *
+   * @see sfWidgetForm
+   */
+  protected function configure($options = array(), $attributes = array())
+  {
+    parent::configure($options, $attributes);
+
+    $this->addOption('image', false);
+    $this->addOption('config', '{}');
+    $this->addOption('culture', '');
+		$years = range(date('Y') - 150, date('Y') + 150);
+		$this->addOption('years', array_combine($years, $years));
+		
+    $classes = preg_split('/\s+/', $this->getAttribute('class'));
+    $classes[] = 'a-date-field';
+    $this->setAttribute('class', implode(' ', $classes));
+    if ('en' == $this->getOption('culture'))
+    {
+      $this->setOption('culture', 'en');
+    }
+  }
+
+  /**
+   * @param  string $name        The element name
+   * @param  string $value       The date displayed in this widget
+   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
+   * @param  array  $errors      An array of errors for the field
+   *
+   * @return string An HTML tag string
+   *
+   * @see sfWidgetForm
+   */
+  public function render($name, $value = null, $attributes = array(), $errors = array())
+  {
+    $prefix = $this->generateId($name);
+    // Spike Broehm: hyphens are not valid in function names
+    $prefix = str_replace('-', '_', $prefix);
+    $image = '';
+    if (false !== $this->getOption('image'))
+    {
+      $image = sprintf(', buttonImage: "%s", buttonImageOnly: true', $this->getOption('image'));
+    }
+
+    $beforeShow = '';
+    if (isset($attributes['beforeShow']))
+    {
+      $beforeShow = sprintf(', beforeShow: %s', $attributes['beforeShow']);
+    }
+
+    $onClose = '';
+    if (isset($attributes['onClose']))
+    {
+      $onClose = sprintf(', onClose: %s', $attributes['onClose']);
+    }
+
+    return 
+      // Outer div with the prefix ID allows efficient jQuery - Firefox can delay for
+      // as much as two full seconds trying to process the :has selectors that are otherwise
+      // necessary to locate all of the controls in here
+      '<div class="a-date-wrapper" id="' . $prefix . '">' .
+      // Parent class select controls, our interface to Symfony
+      '<span style="display: none">' . parent::render($name, $value, $attributes, $errors) . '</span>' .
+      // Autopopulated by jQuery.Datepicker, we also allow direct editing and have hooks relating to that
+      $this->renderTag('input', array('type' => 'text', 'size' => 10, 'id' => $id = $this->generateId($name).'_jquery_control', 'class' => isset($attributes['class']) ? $attributes['class'] : '', 'onBlur' => $prefix . "_update_linked($('#$id').val())")) .
+           sprintf(<<<EOF
+<script type="text/javascript">
+
+function %s_read_linked()
+{
+  var sel = '#%s';
+  var month = '#%s';
+  var day = '#%s';
+  var year = '#%s';
+  val = \$(month).val() + "/" + \$(day).val() + "/" + \$(year).val();
+  if (val === '//')
+  {
+    val = '';
+  }
+  \$(sel).val(val);
+  return {};
+}
+
+function %s_update_linked(date)
+{
+  var components = date.match(/(\d+)\/(\d+)\/(\d\d\d\d)/);
+  var month = "#%s";
+  var day = "#%s";
+  var year = "#%s";
+  if (!components)
+  {
+    if (date.length)
+    {
+      alert("The date must be in MM/DD/YYYY format. Example: 09/29/2009. Hint: select a date from the calendar.");
+      $('#$id').focus();
+    }
+    // TODO: an option to make it mandatory
+    \$(month).val('');
+    \$(day).val('');
+    \$(year).val('');
+    return;
+  }
+  \$(month).val(parseInt(components[1]));
+  \$(day).val(parseInt(components[2]));
+  \$(year).val(parseInt(components[3]));
+  
+  // Something we can bind to update other fields 
+  $('#$id').trigger('aDateUpdated');
+}
+
+$(function()
+{
+
+  %s_read_linked();
+  
+  \$("#%s").datepicker(\$.extend({}, {
+    dateFormat: "mm/dd/yyyy",
+    minDate:    new Date(%s, 1 - 1, 1),
+    maxDate:    new Date(%s, 12 - 1, 31),
+    beforeShow: %s_read_linked,
+    onSelect:   %s_update_linked,
+    showOn:     "both",
+		showAnim: 	"fadeIn"
+		%s
+		%s
+    %s
+  }, \$.datepicker.regional["%s"], %s));
+
+	// General useability stuff that the original date widget was lacking because it was made by robots and not actual human beings
+	$('.ui-datepicker-trigger').attr('title','Choose A Date').hover(function(){
+		$(this).fadeTo(0,.5);
+	},function(){
+		$(this).fadeTo(0,1);	
+	});
+		
+});
+
+</script>
+
+EOF
+      ,
+      $prefix, $id,
+      $this->generateId($name.'[month]'), $this->generateId($name.'[day]'), $this->generateId($name.'[year]'),
+      $prefix,
+      $this->generateId($name.'[month]'), $this->generateId($name.'[day]'), $this->generateId($name.'[year]'),
+      $prefix,
+      $id,
+      min($this->getOption('years')), max($this->getOption('years')),
+      $prefix, $prefix, $beforeShow, $onClose, $image, $this->getOption('culture'), $this->getOption('config')
+    ) . 
+    // Close wrapper div
+    '</div>';
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryTime.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryTime.class.php	(revision 2584)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormJQueryTime.class.php	(revision 2584)
@@ -0,0 +1,55 @@
+<?php
+
+class aWidgetFormJQueryTime extends sfWidgetFormTime
+{
+  protected function configure($options = array(), $attributes = array())
+  {
+		$jq_path = '/apostrophePlugin/js/timepicker.js';	
+		sfContext::getInstance()->getResponse()->addJavascript($jq_path, 'first');
+		
+    parent::configure($options, $attributes);
+
+    $this->addOption('format', 'g:iA');
+
+  }
+
+  /**
+   * @param  string $name        The element name
+   * @param  string $value       The time displayed in this widget
+   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
+   * @param  array  $errors      An array of errors for the field
+   *
+   * @return string An HTML tag string
+   *
+   * @see sfWidgetForm
+   */
+  public function render($name, $value = null, $attributes = array(), $errors = array())
+  {
+    if(!empty($value))
+    {
+      // Allow both array and string syntax
+      if (is_array($value))
+      {
+        $value = $value['hour'] . ':' . $value['minute'];
+        if (isset($value['second']))
+        {
+          $value .= ':' . $value['second'];
+        }
+      }
+      $value = date($this->getOption('format'), strtotime($value));
+    }
+
+    $attributes['id'] = $this->generateId($name);
+    $html = parent::render($name, $value, $attributes, $errors);
+		$wrapperID = $attributes['id'] . rand(0, 10000);
+		$html = $this->wrapInDiv($html, $wrapperID);
+    $html.= "<script type='text/javascript'>$(document).ready(function() { timepicker2('#" . $wrapperID . "', " . json_encode($attributes) . ") });</script>";
+
+    return $html;
+  }
+
+	protected function wrapInDiv($html, $id)
+	{
+		return '<div id="' . $id . '">' . $html . '</div>';
+	}
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormRadio.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormRadio.class.php	(revision 2567)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormRadio.class.php	(revision 2567)
@@ -0,0 +1,45 @@
+<?php
+
+class aWidgetFormRadio extends sfWidgetFormSelectRadio
+{
+  protected function configure($options = array(), $attributes = array())
+  {		
+    parent::configure($options, $attributes);
+  }
+
+  /**
+   * @param  string $name        The element name
+   * @param  string $value       The time displayed in this widget
+   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
+   * @param  array  $errors      An array of errors for the field
+   *
+   * @return string An HTML tag string
+   *
+   * @see sfWidgetForm
+   */
+	protected function formatChoices($name, $value, $choices, $attributes)
+	{	
+		$inputs = array();
+		foreach ($choices as $key => $option)
+		{
+			$baseAttributes = array(
+				'name'  => substr($name, 0, -2),
+				'type'  => 'radio',
+				'value' => self::escapeOnce($key),
+				'id'    => $id = $this->generateId($name, self::escapeOnce($key)),
+				);
+
+			if (strval($key) == strval($value === false ? 0 : $value))
+			{
+				$baseAttributes['checked'] = 'checked';
+			}
+
+			$inputs[$id] = array(
+				'input' => $this->renderTag('input', array_merge($baseAttributes, $attributes)),
+				'label' => $this->renderContentTag('label', self::escapeOnce($option), array('for' => $id, 'id' => $id.'label', 'class' => $id)),
+				);
+		}
+
+		return call_user_func($this->getOption('formatter'), $this, $inputs);
+	}
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormInputFilePersistent.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormInputFilePersistent.class.php	(revision 3059)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormInputFilePersistent.class.php	(revision 3059)
@@ -0,0 +1,220 @@
+<?php
+
+// Copyright 2009 P'unk Ave, LLC. Released under the MIT license.
+
+/**
+ * aWidgetFormInputFilePersistent represents an upload HTML input tag
+ * that doesn't lose its contents when the form is redisplayed due to 
+ * a validation error in an unrelated field. Instead, the previously
+ * submitted and successfully validated file is kept in a cache
+ * managed on behalf of each user, and automatically reused if the
+ * user doesn't choose to upload a new file but rather simply corrects
+ * other fields and resubmits.
+ */
+class aWidgetFormInputFilePersistent extends sfWidgetForm
+{
+  
+  /**
+   * @param array $options     An array of options
+   * @param array $attributes  An array of default HTML attributes
+   *
+   * @see sfWidgetFormInput
+   *
+   *
+   * In reality builds an array of two controls using the [] form field
+   * name syntax
+   */
+  protected function configure($options = array(), $attributes = array())
+  {
+    parent::configure($options, $attributes);
+
+    $this->addOption('type', 'file');
+    $this->addOption('existing-html', false);
+    // Provides an inline preview. You can also call getPreviewUrl() to get the
+    // current preview URL for the image if there is one, which allows you to
+    // preview outside the widget
+    $this->addOption('image-preview', null);
+    $this->addOption('default-preview', null);
+    $this->setOption('needs_multipart', true);
+  }
+
+  /**
+   * @param  string $name        The element name
+   * @param  string $value       The value displayed in this widget
+   *                             (i.e. the browser-side filename submitted
+   *                             on a previous partially successful
+   *                             validation of this form)
+   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
+   * @param  array  $errors      An array of errors for the field
+   *
+   * @return string An HTML tag string
+   *
+   * @see sfWidgetForm
+   */
+
+  public function render($name, $value = null, $attributes = array(), $errors = array())
+  {
+    list($exists, $persistid, $extension) = $this->getExistsPersistidAndExtension($value);
+    $result = '';
+    $preview = $this->hasOption('image-preview') ? $this->getOption('image-preview') : false;
+    
+    if ($exists)
+    {
+      $result .= $this->getOption('existing-html');
+    }
+    
+    if ($preview)
+    {
+      if (isset($imagePreview['markup']))
+      {
+        $markup = $imagePreview['markup'];
+      }
+      else
+      {
+        $markup = '<img src="%s" />';
+      }
+      $previewUrl = $this->getPreviewUrl($value, $preview);
+      if ($previewUrl !== false)
+      {
+        $result .= sprintf($markup, $previewUrl);
+      }
+    }
+
+    return $result .
+      $this->renderTag('input',
+        array_merge(
+          array(
+            'type' => $this->getOption('type'),
+            'name' => $name . '[newfile]'),
+          $attributes)) .
+      $this->renderTag('input',
+        array(
+          'type' => 'hidden',
+          'name' => $name . '[persistid]',
+          'value' => $persistid));
+  }
+  
+  public function getFormat($value)
+  {
+    list($exists, $persistid, $extension) = $this->getExistsPersistidAndExtension($value);
+    return $extension;
+  }
+  
+  public function getPreviewUrl($value, $imagePreview = array())
+  {
+    list($exists, $persistid, $extension) = $this->getExistsPersistidAndExtension($value);
+    
+    // hasOption just verifies that the option is valid, it doesn't check what,
+    // if anything, was passed. Thanks to Lucjan Wilczewski 
+    $defaultPreview = $this->hasOption('default-preview') ? $this->getOption('default-preview') : false;
+    if ($exists)
+    {
+      $defaultPreview = false;
+    }
+    if ($exists || $defaultPreview)
+    {
+      // Note change of key
+      $urlStem = sfConfig::get('app_aPersistentFileUpload_preview_url', '/uploads/uploaded_image_preview');
+      $urlStem = sfContext::getInstance()->getRequest()->getRelativeUrlRoot() . $urlStem;
+      
+      // This is the corresponding directory path. You have to override one
+      // if you override the other. You override this one by setting
+      // app_aToolkit_upload_uploaded_image_preview_dir
+      $dir = aFiles::getUploadFolder("uploaded_image_preview");
+      // While we're here age off stale previews
+      aValidatorFilePersistent::removeOldFiles($dir);
+      if ($exists)
+      {
+        $info = aValidatorFilePersistent::getFileInfo($persistid);
+        $source = $info['tmp_name'];
+      }
+      else
+      {
+        $source = $defaultPreview;
+      }
+      $info = aImageConverter::getInfo($source, array('format-only' => true));
+      $previewable = false;
+      if ($info && in_array($info['format'], array('jpg', 'png', 'gif')))
+      {
+        $previewable = true;
+        $info = aImageConverter::getInfo($source);
+      }
+      if ($previewable)
+      {
+        $iwidth = $info['width'];
+        $iheight = $info['height'];
+        // This is safe - based on sniffed file contents and not a user supplied extension
+        $format = $info['format'];
+        $dimensions = aDimensions::constrain($iwidth, $iheight, $format, $imagePreview);
+        // A simple filename reveals less
+        $imagename = "$persistid.$format";
+        $url = "$urlStem/$imagename";
+        $output = "$dir/$imagename";
+        if ((isset($info['newfile']) && $info['newfile']) || (!file_exists($output)))
+        {
+          if ($imagePreview['resizeType'] === 'c')
+          {
+            $method = 'cropOriginal';
+          }
+          else
+          {
+            $method = 'scaleToFit';
+          }
+          sfContext::getInstance()->getLogger()->info("YY calling converter method $method width " . $dimensions['width'] . ' height ' . $dimensions['height']);
+          aImageConverter::$method(
+            $source,
+            $output,
+            $dimensions['width'],
+            $dimensions['height']);
+          sfContext::getInstance()->getLogger()->info("YY after converter");
+        }
+      }
+      else
+      {
+        // Don't try to provide an icon alternative to the preview here,
+        // it's better to do that at the project and/or apostrophePlugin level
+        // where we can style it better... the less we fake templating inside
+        // a widget the better. See getFormat
+        $url = false;
+      }
+      return $url;
+    }
+    return false;
+  }
+  
+  protected function getExistsPersistidAndExtension($value)
+  {
+    // TODO: should cache this
+    $exists = false;
+    $extension = false;
+    if (isset($value['persistid']) && strlen($value['persistid']))
+    {
+      $persistid = $value['persistid'];
+      $info = aValidatorFilePersistent::getFileInfo($persistid);
+      if ($info)
+      {
+        $exists = true;
+        if (isset($info['extension']))
+        {
+          $extension = $info['extension'];
+        }
+      }
+    }
+    else
+    {
+      // One implementation, not two (to inevitably drift apart)
+      $persistid = aGuid::generate();
+    }
+    
+    if (!$exists)
+    {
+      $defaultPreview = $this->hasOption('default-preview') ? $this->getOption('default-preview') : false;
+      if ($defaultPreview)
+      {
+        $extension = pathinfo($defaultPreview, PATHINFO_EXTENSION);
+      }
+    }
+    
+    return array($exists, $persistid, $extension);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormRichTextarea.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormRichTextarea.class.php	(revision 2696)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormRichTextarea.class.php	(revision 2696)
@@ -0,0 +1,159 @@
+<?php
+
+ /**
+ * aWidgetFormRichTextarea represents a rich text editor.
+ * The FCK editor is always used in this implementation. 
+ *
+ * Originally based on Dominic Scheirlinck's implementation. However now
+ * it is pretty much a thin wrapper around code ported from the old 
+ * Symfony 1.x FCK rich text editor class (which is gone in 1.4).
+ * 
+ * NOTE: THE ID IS IGNORED, FCK always sets the name and id attributes
+ * of the hidden input field or fallback textarea to the same value. We
+ * must use name for that value to produce results the forms framework
+ * can understand.
+ *
+ * This is a misfeature of FCK, not something we can fix here without
+ * breaking the association between the hidden field and the rich text
+ * editor. ALWAYS USE setNameFormat() in your form class to give your
+ * form fields names that will distinguish them from any other forms
+ * in the same page, otherwise your rich text fields will behave in
+ * unexpected ways. (Yes, this does mean IDs with brackets in them are in
+ * use due to this limitation of FCK, however all modern browsers 
+ * allow that in practice.) This is rarely an issue unless you have
+ * numerous forms in the same page and they have the same name format string
+ * (or the default %s).
+ *
+ * @author     Tom Boutell <tom@punkave.com>
+ */
+class aWidgetFormRichTextarea extends sfWidgetFormTextarea 
+{
+  /**
+   * @param array $options     An array of options
+   * @param array $attributes  An array of default HTML attributes
+   *
+   * @see sfWidgetForm
+   */
+  protected function configure($options = array(), $attributes = array())
+  {
+    $this->addOption('editor', 'fck');
+    $this->addOption('css', false);
+		$this->addOption('tool','Default');
+		$this->addOption('height','225');
+		$this->addOption('width','100%');
+    
+    parent::configure($options, $attributes);
+  }
+  
+  /**
+   * @param  string $name        The element name
+   * @param  string $value       The value displayed in this widget
+   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
+   * @param  array  $errors      An array of errors for the field
+   *
+   * @return string An HTML tag string
+   *
+   * This is mostly borrowed from the Symfony 1.3 sf Rich Text Editor FCK class,
+   * (don't want to say it out loud and make project:validate worry),
+   * which is gone in Symfony 1.4 and not autoloadable in Symfony 1.3.
+   * Note that we are now officially FCK-specific. That was pretty much
+   * true already (notice our fckextraconfig.js trick below). 
+   *
+   * NOTE: THE ID IS IGNORED, FCK always sets the name and id attributes
+   * of the hidden input field or fallback textarea to the same value. 
+   * This is a misfeature of FCK, not something we can fix here without
+   * breaking the association between the hidden field and the rich text
+   * editor. ALWAYS USE setNameFormat() in your form class to give your
+   * form fields names that will distinguish them from any other forms
+   * in the same page, otherwise your rich text fields will behave in
+   * unexpected ways.
+   *
+   * @see sfWidgetForm
+   */
+  public function render($name, $value = null, $attributes = array(), $errors = array())
+  {
+    $attributes = array_merge($attributes, $this->getOptions());
+    $attributes = array_merge($attributes, array('name' => $name));
+    // This is good form, but doesn't really work with FCK, which
+    // does not support a name that is distinct from the id
+    $attributes = $this->fixFormId($attributes);
+    
+    // TBB: a sitewide additional config settings file is used, if it
+    // exists and a different one has not been explicitly specified
+    if (isset($attributes['editor']) && (strtolower($attributes['editor']) === 'fck'))
+    {
+      if (!isset($attributes['config']))
+      {
+        if (file_exists(sfConfig::get('sf_web_dir') . '/js/fckextraconfig.js'))
+        {
+          $attributes['config'] = '/js/fckextraconfig.js'; 
+        }
+      }
+    }
+    
+    // Merged in from Symfony 1.3's FCK rich text editor implementation,
+    // since that is no longer available in 1.4
+    
+    $options = $attributes;
+    $id = $options['id'];
+
+    // sf_web_dir already contains the relative root, don't append it twice
+    $php_file = '/'.sfConfig::get('sf_rich_text_fck_js_dir').DIRECTORY_SEPARATOR.'fckeditor.php';
+
+    if (!is_readable(sfConfig::get('sf_web_dir').DIRECTORY_SEPARATOR.$php_file))
+    {
+      throw new sfConfigurationException('You must install FCKEditor to use this widget (see rich_text_fck_js_dir settings). ' . sfConfig::get('sf_web_dir').DIRECTORY_SEPARATOR.$php_file);
+    }
+
+    // FCKEditor.php class is written with backward compatibility of PHP4.
+    // This reportings are to turn off errors with public properties and already declared constructor
+    $error_reporting = error_reporting(E_ALL);
+
+    require_once(sfConfig::get('sf_web_dir').DIRECTORY_SEPARATOR.$php_file);
+
+    // turn error reporting back to your settings
+    error_reporting($error_reporting);
+
+    // What if the name isn't an acceptable id? 
+    $fckeditor           = new FCKeditor($options['name']);
+    $fckeditor->BasePath = sfContext::getInstance()->getRequest()->getRelativeUrlRoot().'/'.sfConfig::get('sf_rich_text_fck_js_dir').'/';
+    $fckeditor->Value    = $value;
+
+    if (isset($options['width']))
+    {
+      $fckeditor->Width = $options['width'];
+    }
+    elseif (isset($options['cols']))
+    {
+      $fckeditor->Width = (string)((int) $options['cols'] * 10).'px';
+    }
+
+    if (isset($options['height']))
+    {
+      $fckeditor->Height = $options['height'];
+    }
+    elseif (isset($options['rows']))
+    {
+      $fckeditor->Height = (string)((int) $options['rows'] * 10).'px';
+    }
+
+    if (isset($options['tool']))
+    {
+      $fckeditor->ToolbarSet = $options['tool'];
+    }
+
+    if (isset($options['config']))
+    {
+      // We need the asset helper to load things via javascript_path
+      sfContext::getInstance()->getConfiguration()->loadHelpers(array('Asset'));
+      $fckeditor->Config['CustomConfigurationsPath'] = javascript_path($options['config']);
+    }
+
+    $content = $fckeditor->CreateHtml();
+
+    // Skip the braindead 'type='text'' hack that breaks Safari
+    // in 1.0 compat mode, since we're in a 1.2+ widget here for sure
+
+    return $content;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/sfWidgetFormSchemaFormatterAAdmin.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/sfWidgetFormSchemaFormatterAAdmin.class.php	(revision 2675)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/sfWidgetFormSchemaFormatterAAdmin.class.php	(revision 2675)
@@ -0,0 +1,31 @@
+<?php
+
+class sfWidgetFormSchemaFormatterAAdmin extends sfWidgetFormSchemaFormatter 
+{
+  protected
+		$aRowClassName = "a-form-row",
+    $rowFormat = "<div class=\"%a_row_class%\">\n  %label%\n  <div class=\"a-form-field\">%field%</div> %error% \n %help%%hidden_fields%\n</div>\n",
+    $errorRowFormat = '%errors%',
+    $helpFormat = '<div class="a-form-help-text">%help%</div>',
+    $decoratorFormat ="<div class=\"a-admin-form-container\">\n %content%\n</div>",
+		$errorListFormatInARow     = "<div class='a-form-errors'>\n<ul class=\"a-error-list error_list\">\n%errors%</ul>\n</div>\n",
+		$errorRowFormatInARow      = "<li>%error%</li>\n",
+		$namedErrorRowFormatInARow = "<li>%name%: %error%</li>\n";
+
+	public function formatRow($label, $field, $errors = array(), $help = '', $hiddenFields = null)
+  {
+    return strtr($this->getRowFormat(), array(
+			'%a_row_class%' 	=> (count($errors)) ? $this->getARowClassName().' has-errors': $this->getARowClassName(), 
+      '%label%'         => $label,
+      '%field%'         => $field,
+      '%error%'         => $this->formatErrorsForRow($errors),
+      '%help%'          => $this->formatHelp($help),
+      '%hidden_fields%' => null === $hiddenFields ? '%hidden_fields%' : $hiddenFields,
+    ));
+ 	}
+
+	public function getARowClassName()
+	{
+    return $this->aRowClassName;
+	}	
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormChoice.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormChoice.class.php	(revision 2567)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/aWidgetFormChoice.class.php	(revision 2567)
@@ -0,0 +1,138 @@
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfWidgetFormChoice represents a choice widget.
+ *
+ * @package    symfony
+ * @subpackage widget
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class aWidgetFormChoice extends sfWidgetFormChoice
+{
+  /**
+   * Constructor.
+   *
+   * Available options:
+   *
+   *  * choices:          An array of possible choices (required)
+   *  * multiple:         true if the select tag must allow multiple selections
+   *  * expanded:         true to display an expanded widget
+   *                        if expanded is false, then the widget will be a select
+   *                        if expanded is true and multiple is false, then the widget will be a list of radio
+   *                        if expanded is true and multiple is true, then the widget will be a list of checkbox
+   *  * renderer_class:   The class to use instead of the default ones
+   *  * renderer_options: The options to pass to the renderer constructor
+   *  * renderer:         A renderer widget (overrides the expanded and renderer_options options)
+   *                      The choices option must be: new sfCallable($thisWidgetInstance, 'getChoices')
+   * @param array $options     An array of options
+   * @param array $attributes  An array of default HTML attributes
+   *
+   * @see sfWidgetFormChoiceBase
+   */
+  protected function configure($options = array(), $attributes = array())
+  {
+    parent::configure($options, $attributes);
+
+    $this->addOption('multiple', false);
+    $this->addOption('expanded', false);
+    $this->addOption('renderer_class', false);
+    $this->addOption('renderer_options', array());
+    $this->addOption('renderer', false);
+  }
+
+  /**
+   * Sets the format for HTML id attributes. This is made avaiable to the renderer,
+   * as this widget does not render itself, but delegates to the renderer instead.
+   *
+   * @param string $format  The format string (must contain a %s for the id placeholder)
+   *
+   * @see sfWidgetForm
+   */
+  public function setIdFormat($format)
+  {
+    $this->options['renderer_options']['id_format'] = $format;
+  }
+
+  /**
+   * Renders the widget.
+   *
+   * @param  string $name        The element name
+   * @param  string $value       The value selected in this widget
+   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
+   * @param  array  $errors      An array of errors for the field
+   *
+   * @return string An HTML tag string
+   *
+   * @see sfWidgetForm
+   */
+  public function render($name, $value = null, $attributes = array(), $errors = array())
+  {
+    if ($this->getOption('multiple'))
+    {
+      $attributes['multiple'] = 'multiple';
+
+      if ('[]' != substr($name, -2))
+      {
+        $name .= '[]';
+      }
+    }
+
+    if (!$this->getOption('renderer') && !$this->getOption('renderer_class') && $this->getOption('expanded'))
+    {
+      unset($attributes['multiple']);
+    }
+
+    return $this->getRenderer()->render($name, $value, $attributes, $errors);
+  }
+
+  /**
+   * Gets the stylesheet paths associated with the widget.
+   *
+   * @return array An array of stylesheet paths
+   */
+  public function getStylesheets()
+  {
+    return $this->getRenderer()->getStylesheets();
+  }
+
+  /**
+   * Gets the JavaScript paths associated with the widget.
+   *
+   * @return array An array of JavaScript paths
+   */
+  public function getJavaScripts()
+  {
+    return $this->getRenderer()->getJavaScripts();
+  }
+
+  public function getRenderer()
+  {
+    if ($this->getOption('renderer'))
+    {
+      return $this->getOption('renderer');
+    }
+
+    if (!$class = $this->getOption('renderer_class'))
+    {
+      $type = !$this->getOption('expanded') ? '' : ($this->getOption('multiple') ? 'checkbox' : 'radio');
+			if ($type == 'radio') {
+      	$class = sprintf('aWidgetForm%s', ucfirst($type));
+			}
+			else
+			{
+      	$class = sprintf('sfWidgetFormSelect%s', ucfirst($type));				
+			}
+
+    }
+    return new $class(array_merge(array('choices' => new sfCallable(array($this, 'getChoices'))), $this->options['renderer_options']), $this->getAttributes());
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/sfWidgetFormSchemaFormatterAPageSettings.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/sfWidgetFormSchemaFormatterAPageSettings.class.php	(revision 2680)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/widget/sfWidgetFormSchemaFormatterAPageSettings.class.php	(revision 2680)
@@ -0,0 +1,31 @@
+<?php
+
+class sfWidgetFormSchemaFormatterAPageSettings extends sfWidgetFormSchemaFormatter 
+{
+  protected
+		$aRowClassName = "a-form-row",
+    $rowFormat = "<div class=\"%a_row_class%\">\n  %label%\n  <div class=\"a-form-field\">%field% %help%</div> %error% \n %hidden_fields%\n</div>\n",
+    $errorRowFormat = '%errors%',
+    $helpFormat = '<div class="a-form-help-text">%help%</div>',
+    $decoratorFormat ="<div class=\"a-admin-form-container\">\n %content%\n</div>",
+		$errorListFormatInARow     = "<div class='a-form-errors'>\n<ul class=\"a-error-list error_list\">\n%errors%</ul>\n</div>\n",
+		$errorRowFormatInARow      = "<li>%error%</li>\n",
+		$namedErrorRowFormatInARow = "<li>%name%: %error%</li>\n";
+
+	public function formatRow($label, $field, $errors = array(), $help = '', $hiddenFields = null)
+  {
+    return strtr($this->getRowFormat(), array(
+			'%a_row_class%' 	=> (count($errors)) ? $this->getARowClassName().' has-errors': $this->getARowClassName(), 
+      '%label%'         => "<h4>".$label.'</h4>',
+      '%field%'         => $field,
+      '%error%'         => $this->formatErrorsForRow($errors),
+      '%help%'          => $this->formatHelp($help),
+      '%hidden_fields%' => null === $hiddenFields ? '%hidden_fields%' : $hiddenFields,
+    ));
+ 	}
+
+	public function getARowClassName()
+	{
+    return $this->aRowClassName;
+	}	
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aPageFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aPageFormFilter.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aPageFormFilter.class.php	(revision 4)
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * aPage filter form.
+ *
+ * @package    ##PROJECT_NAME##
+ * @subpackage filter
+ * @author     ##AUTHOR_NAME##
+ * @version    SVN: $Id: sfPropelFormFilterTemplate.php 11675 2008-09-19 15:21:38Z fabien $
+ */
+class aPageFormFilter extends BaseaPageFormFilter
+{
+  public function configure()
+  {
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aGroupAdminFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aGroupAdminFilter.class.php	(revision 1738)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aGroupAdminFilter.class.php	(revision 1738)
@@ -0,0 +1,5 @@
+<?php
+
+class aGroupAdminFilter extends BaseaGroupAdminFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryUserFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryUserFormFilter.class.php	(revision 2266)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryUserFormFilter.class.php	(revision 2266)
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * PluginaCategoryUser form.
+ *
+ * @package    ##PROJECT_NAME##
+ * @subpackage filter
+ * @author     ##AUTHOR_NAME##
+ * @version    SVN: $Id: sfDoctrineFormFilterPluginTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
+ */
+abstract class PluginaCategoryUserFormFilter extends BaseaCategoryUserFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaPageFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaPageFormFilter.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaPageFormFilter.class.php	(revision 4)
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * PluginaPage form.
+ *
+ * @package    filters
+ * @subpackage aPage *
+ * @version    SVN: $Id: sfDoctrineFormTemplate.php 6174 2007-11-27 06:22:40Z fabien $
+ */
+abstract class PluginaPageFormFilter extends BaseaPageFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryFormFilter.class.php	(revision 2972)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryFormFilter.class.php	(revision 2972)
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * PluginaCategory form.
+ *
+ * @package    ##PROJECT_NAME##
+ * @subpackage filter
+ * @author     ##AUTHOR_NAME##
+ * @version    SVN: $Id: sfDoctrineFormFilterPluginTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
+ */
+abstract class PluginaCategoryFormFilter extends BaseaCategoryFormFilter
+{
+  protected function getUseFields()
+  {
+    $useFields = array();
+    $useFields[] = 'groups_list';
+    $useFields[] = 'users_list';
+    return $useFields;
+  }
+  
+  public function setup()
+  {
+    parent::setup();    
+    $this->useFields($this->getUseFields());
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaMediaCategoryFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaMediaCategoryFormFilter.class.php	(revision 175)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaMediaCategoryFormFilter.class.php	(revision 175)
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * PluginaMediaCategory form.
+ *
+ * @package    ##PROJECT_NAME##
+ * @subpackage filter
+ * @author     ##AUTHOR_NAME##
+ * @version    SVN: $Id: sfDoctrineFormFilterPluginTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
+ */
+abstract class PluginaMediaCategoryFormFilter extends BaseaMediaCategoryFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaMediaItemFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaMediaItemFormFilter.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaMediaItemFormFilter.class.php	(revision 4)
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * PluginaMediaItem form.
+ *
+ * @package    filters
+ * @subpackage aMediaItem *
+ * @version    SVN: $Id: sfDoctrineFormTemplate.php 6174 2007-11-27 06:22:40Z fabien $
+ */
+abstract class PluginaMediaItemFormFilter extends BaseaMediaItemFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaRedirectFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaRedirectFormFilter.class.php	(revision 846)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaRedirectFormFilter.class.php	(revision 846)
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * PluginaRedirect form.
+ *
+ * @package    ##PROJECT_NAME##
+ * @subpackage filter
+ * @author     ##AUTHOR_NAME##
+ * @version    SVN: $Id: sfDoctrineFormFilterPluginTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
+ */
+abstract class PluginaRedirectFormFilter extends BaseaRedirectFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaEmbedMediaAccountFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaEmbedMediaAccountFormFilter.class.php	(revision 2302)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaEmbedMediaAccountFormFilter.class.php	(revision 2302)
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * PluginaEmbedMediaAccount form.
+ *
+ * @package    ##PROJECT_NAME##
+ * @subpackage filter
+ * @author     ##AUTHOR_NAME##
+ * @version    SVN: $Id: sfDoctrineFormFilterPluginTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
+ */
+abstract class PluginaEmbedMediaAccountFormFilter extends BaseaEmbedMediaAccountFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryGroupFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryGroupFormFilter.class.php	(revision 2266)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaCategoryGroupFormFilter.class.php	(revision 2266)
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * PluginaCategoryGroup form.
+ *
+ * @package    ##PROJECT_NAME##
+ * @subpackage filter
+ * @author     ##AUTHOR_NAME##
+ * @version    SVN: $Id: sfDoctrineFormFilterPluginTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
+ */
+abstract class PluginaCategoryGroupFormFilter extends BaseaCategoryGroupFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaSmartSlideshowSlotFormFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaSmartSlideshowSlotFormFilter.class.php	(revision 2414)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/doctrine/PluginaSmartSlideshowSlotFormFilter.class.php	(revision 2414)
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * PluginaSmartSlideshowSlot form.
+ *
+ * @package    filters
+ * @subpackage aSmartSlideshowSlot *
+ * @version    SVN: $Id: sfDoctrineFormTemplate.php 6174 2007-11-27 06:22:40Z fabien $
+ */
+abstract class PluginaSmartSlideshowSlotFormFilter extends BaseaSmartSlideshowSlotFormFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aUserAdminFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aUserAdminFilter.class.php	(revision 1737)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/aUserAdminFilter.class.php	(revision 1737)
@@ -0,0 +1,5 @@
+<?php
+
+class aUserAdminFilter extends BaseaUserAdminFilter
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/base/BaseaGroupAdminFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/base/BaseaGroupAdminFilter.class.php	(revision 1738)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/base/BaseaGroupAdminFilter.class.php	(revision 1738)
@@ -0,0 +1,12 @@
+<?php
+
+class BaseaGroupAdminFilter extends sfGuardGroupFormFilter
+{
+  public function configure()
+  {
+    // TODO: it would be nice to have blog_categories_list and other things without writing 
+    // code specific to other plugins here. Use an event? My main goal in limiting this list
+    // was to prevent memory usage from exploding when you add more relations
+    $this->useFields(array('name', 'created_at'));
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/base/BaseaUserAdminFilter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/base/BaseaUserAdminFilter.class.php	(revision 3170)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/filter/base/BaseaUserAdminFilter.class.php	(revision 3170)
@@ -0,0 +1,25 @@
+<?php
+
+class BaseaUserAdminFilter extends sfGuardUserFormFilter
+{
+  public function configure()
+  {
+    // TODO: it would be nice to have blog_categories_list and other things without writing 
+    // code specific to other plugins here. Use an event? My main goal in limiting this list
+    // was to prevent memory usage from exploding when you add more relations
+    $this->useFields(array('username', 'is_active', 'is_super_admin', 'last_login', 'created_at', 'groups_list'));
+    $this->widgetSchema->setLabel('username', 'Name');
+  }
+  
+  public function addUsernameColumnQuery(Doctrine_Query $query, $field, $value)
+  {
+    // You get an associative array with an sfWidgetFormFilterInput
+    if ((!isset($value['text'])) || (!strlen($value['text'])))
+    {
+      return;
+    }
+    $like = '%' . $value['text'] . '%';
+    $r = $query->getRootAlias();
+    $query->addWhere("($r.username LIKE ?) OR concat($r.first_name, $r.last_name) LIKE ?", array($like, $like));
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineActions.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineActions.class.php	(revision 1574)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineActions.class.php	(revision 1574)
@@ -0,0 +1,9 @@
+<?php
+
+class aEngineActions extends sfActions
+{
+  public function preExecute()
+  {
+    aEngineTools::preExecute($this);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aZendSearch.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aZendSearch.class.php	(revision 2949)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aZendSearch.class.php	(revision 2949)
@@ -0,0 +1,466 @@
+<?php
+
+class aZendSearch
+{
+  // Returns just the IDs. See addSearchQuery for a better method to use if you're
+  // pulling the actual objects from Doctrine. See searchLuceneWithScores if you
+  // need the actual scores so that you can merge results from searches of
+  // multiple tables
+  
+  static public function searchLucene(Doctrine_Table $table, $luceneQuery, $culture = null)
+  {
+    $raw = self::searchLuceneWithScores($table, $luceneQuery, $culture);
+    return array_keys($raw);
+  }
+
+  static public function searchLuceneWithScores(Doctrine_Table $table, $luceneQueryString, $culture = null)
+  {
+    $results = self::searchLuceneWithValues($table, $luceneQueryString, $culture);
+    $nresults = array();
+    foreach ($results as $a => $result)
+    {
+      $nresults[$a] = $result->score;
+    }
+    return $nresults;
+  }
+  
+  static public function searchLuceneWithValues(Doctrine_Table $table, $luceneQueryString, $culture = null, $andLuceneQuery = null)
+   {
+     // Ugh: UTF8 Lucene is case sensitive work around this
+     if (function_exists('mb_strtolower'))
+     {
+       $luceneQueryString = mb_strtolower($luceneQueryString);
+     }
+     else
+     {
+       $luceneQueryString = strtolower($luceneQueryString);
+     }
+     
+     // We have to register the autoloader before we can use these classes
+     self::registerZend();
+
+     // Specify character set. Apostrophe is always UTF-8
+     $luceneQuery = Zend_Search_Lucene_Search_QueryParser::parse($luceneQueryString, 'utf-8');
+     $query = new Zend_Search_Lucene_Search_Query_Boolean();
+     $query->addSubquery($luceneQuery, true);
+     if (!is_null($culture))
+     {
+       $culture = self::normalizeCulture($culture);
+       $cultureTerm = new Zend_Search_Lucene_Index_Term($culture, 'culture'); 
+       // Oops, this said $aTerm before. Thanks to Quentin Dugauthier
+       $cultureQuery = new Zend_Search_Lucene_Search_Query_Term($cultureTerm);
+       $query->addSubquery($cultureQuery, true);
+     }
+     
+     if (!is_null($andLuceneQuery))
+     {
+      $query->addSubquery($andLuceneQuery, true); 
+     }
+     
+     $index = $table->getLuceneIndex();
+
+     $hits = $index->find($query);
+
+     // Never look at more than 1000 results, no matter what. This is necessary
+     // to avoid out of memory errors on large sites. Note that if 1,000 locked
+     // pages precede the first unlocked page and you are logged out, you could
+     // theoretically not get your result. In practice unlocked pages tend to be
+     // prominent and come up early. A deeper fix for this would be problematic
+     // since Zend won't let you unset the document for a query hit, permanently
+     // attaching lots of memory to a hit once you peek at it to determine things
+     // like the user's eligibility to see it based on other information in
+     // Doctrine tables
+
+     if (sfConfig::get('app_a_search_hard_limit', false))
+     {
+       $hits = array_splice($hits, 0, sfConfig::get('app_a_search_hard_limit'));
+     }
+
+     $ids = array();
+
+     foreach ($hits as $hit)
+     {
+       $ids[$hit->primarykey] = $hit;
+     }
+     return $ids;
+   }
+  
+  static public function addSearchQuery(Doctrine_Table $table, Doctrine_Query $q = null, $luceneQuery, $culture = null)
+  {
+    $name = $table->getOption('name');
+
+    if (is_null($q))
+    {
+      $q = Doctrine_Query::create()
+        ->from($name);
+    }
+    
+    $results = $table->searchLucene($luceneQuery, $culture);
+    
+    if (count($results))
+    {
+      
+      $alias = $q->getRootAlias();
+      // Call addSelect so that we don't trash existing queries.
+      $q->addSelect($alias.'.*');
+      aDoctrine::orderByList($q, $results);
+      $q->whereIn($alias.'.id', $results);
+      return $q;
+    }
+    else
+    {
+      // Don't just let everything through when there are no hits!
+      // Careful, be cross-database compatible
+      $q->andWhere('0 = 1');
+    }
+    
+    return $q;
+  }
+
+  // $scores becomes (assignment by reference) an associative array in which
+  // the keys are your object IDs and the values are scores from Lucene. This is
+  // useful in rare situations where you need to merge results from multiple
+  // Lucene searches and preserve their relative scores. It's also useful if you
+  // just want to display the scores.
+  //
+  // THIS ARRAY WILL CONTAIN EVERYTHING RETURNED BY LUCENE, which may include
+  // object IDs that are excluded by other parameters of your Doctrine search. Refer
+  // to your Doctrine results to determine which objects are relevant. Use 
+  // $resultsWithScores to look up the scores of those objects.
+  //
+  // If you specify null for $q, a doctrine query will be created for you.
+  // If you specify null for $culture, no culture will be specified in the
+  // Lucene query.
+  
+  static public function addSearchQueryWithScores(Doctrine_Table $table, Doctrine_Query $q = null, $luceneQuery, $culture, &$scores)
+  {
+    $name = $table->getOption('name');
+
+    if (is_null($q))
+    {
+      $q = Doctrine_Query::create()
+        ->from($name);
+    }
+    
+    $scores = $table->searchLuceneWithScores($luceneQuery, $culture);
+    
+    $results = array_keys($scores);
+    if (count($results))
+    {
+      $alias = $q->getRootAlias();
+      // Contrary to Jobeet the above is NOT enough, the results will
+      // not be in Lucene result order. Use aDoctrine::orderByList to fix
+      // that up in a portable way with a SQL92-compatible CASE statement.
+
+      // Call addSelect so that we don't trash existing queries.
+      $q->addSelect($alias.'.*');
+      aDoctrine::orderByList($q, $results);
+      $q->whereIn($alias.'.id', $results);
+    }
+    else
+    {
+      // Don't just let everything through when there are no hits!
+      // Don't use just 'false', that is not guaranteed to be cross-database compatible.
+      $q->andWhere('0 = 1');
+    }
+        
+    return $q;
+  }
+
+  static public function purgeLuceneIndex(Doctrine_Table $table)
+  {
+    $file = $table->getLuceneIndexFile();
+
+    if (file_exists($file))
+    {
+      sfToolkit::clearDirectory($file);
+      rmdir($file);
+    }
+  }
+
+  static public function rebuildLuceneIndex(Doctrine_Table $table)
+  {
+    self::purgeLuceneIndex($table);
+    $index = $table->getLuceneIndex();
+    
+    // TODO: hydrate these one at a time once Doctrine supports
+    // doing that efficiently
+    $all = $table->findAll();
+    foreach ($all as $item)
+    {
+      $item->updateLuceneIndex();
+    }
+
+    return $table->optimizeLuceneIndex();
+  }
+  
+  static public function optimizeLuceneIndex(Doctrine_Table $table)
+  {
+    $index = $table->getLuceneIndex();
+
+    return $index->optimize();
+  }
+
+  // If you're storing different search text for different cultures, but
+  // at delete time you want to trash ALL the cultures for this object,
+  // that's fine: just don't pass a culture to delete. That's appropriate
+  // if, for instance, you are deleting a page from a CMS entirely, all
+  // localizations included.
+
+  // If you do pass a culture this method will remove the object from the
+  // potential search results for that particular culture.
+
+  static public function deleteFromLuceneIndex(Doctrine_Record $object, $culture = null)
+  {
+    $index = $object->getTable()->getLuceneIndex();
+   
+    // remove an existing entry
+    $id = $object->getId();
+    // 20090506: we can't use a regular query string here because
+    // numbers (such as IDs) will get stripped from it. So we have
+    // to build a query using the Zend Search API. Note that this means
+    // the Jobeet sample code is incorrect.
+    // http://framework.zend.com/manual/en/zend.search.lucene.searching.html#zend.search.lucene.searching.query_building
+
+    $aTerm = new Zend_Search_Lucene_Index_Term($id, 'primarykey'); 
+    $aQuery = new Zend_Search_Lucene_Search_Query_Term($aTerm);
+    $query = new Zend_Search_Lucene_Search_Query_Boolean();
+    $query->addSubquery($aQuery, true);
+    if (!is_null($culture))
+    {
+      $culture = self::normalizeCulture($culture);
+      $cultureTerm = new Zend_Search_Lucene_Index_Term($culture, 'culture'); 
+      // Oops, this said $aTerm before. Thanks to Quentin Dugauthier
+      $cultureQuery = new Zend_Search_Lucene_Search_Query_Term($cultureTerm);
+      $query->addSubquery($cultureQuery, true);
+    }
+    if ($hits = $index->find($query))
+    {
+      // id is correct. This is the internal Zend search index id which is
+      // not the same thing as the id of our object.
+
+      // There should actually be only one hit for a specific id and culture
+      foreach ($hits as $hit)
+      {
+        $index->delete($hit->id);
+      }
+    }
+  }
+
+  // You can use this directly, but also see below for a wrapper that 
+  // saves in both doctrine and Zend, wrapping the whole thing
+  // in a Doctrine transaction and rolling back on any Lucene exceptions.
+
+  // The arguments are a bit messy for historical reasons (TODO: fix this in 2.0 with a nice options array).
+  // Note that Lucene is not your database.
+  
+  // For things that must be searchable, use $fields. For things that must be stored for display as part of the
+  // presentation of the search result, use $storedFields. Note that a searchable field is not stored for retrieval.
+  // IF YOU WISH TO HAVE IT BOTH WAYS, you must store the field under a DIFFERENT NAME than that used to
+  // index it, otherwise the storage overrides the indexing. Drove me nuts trying to figure this one out
+
+  static public function updateLuceneIndex($options)
+  {
+    // NEW WAY: options as a single array
+    if (is_array($options))
+    {
+      $object = $options['object'];
+      $culture = isset($options['culture']) ? $options['culture'] : null;
+      $fields = isset($options['indexed']) ? $options['indexed'] : array();
+      $storedFields = isset($options['stored']) ? $options['stored'] : array();
+      $keywords = isset($options['keywords']) ? $options['keywords'] : array();
+      $boostsByField = isset($options['boosts']) ? $options['boosts'] : array();
+    }
+    else
+    {
+      throw new sfException("updateLuceneIndex now expects a single array of options, see aZendSearch::updateLuceneIndex");
+    }
+    self::deleteFromLuceneIndex($object, $culture);
+    $index = self::getLuceneIndex($object->getTable());
+    $doc = new Zend_Search_Lucene_Document();
+   
+    // store item id so we can retrieve the corresponding object
+    $doc->addField(Zend_Search_Lucene_Field::Keyword('primarykey', $object->getId(), 'UTF-8'));
+    if (!is_null($culture))
+    {
+      $doc->addField(Zend_Search_Lucene_Field::Keyword('culture', $culture, 'UTF-8'));
+    }
+
+    // Index the search fields
+    foreach ($fields as $key => $value)
+    {
+      // Ugh: UTF8 Lucene is case sensitive work around this
+      if (function_exists('mb_strtolower'))
+      {
+        $value = mb_strtolower($value);
+      }
+      else
+      {
+        $value = strtolower($value);
+      }
+      $field = Zend_Search_Lucene_Field::UnStored($key, $value, 'UTF-8');
+      if (isset($boostsByField[$key]))
+      {
+      	$field->boost = $boostsByField[$key];
+      }
+      $doc->addField($field);
+    }
+    
+    // Index the keyword fields
+    foreach ($keywords as $key => $value)
+    {
+      // Ugh: UTF8 Lucene is case sensitive work around this
+      if (function_exists('mb_strtolower'))
+      {
+        $value = mb_strtolower($value);
+      }
+      else
+      {
+        $value = strtolower($value);
+      }
+      $field = Zend_Search_Lucene_Field::Keyword($key, $value, 'UTF-8');
+      if (isset($boostsByField[$key]))
+      {
+      	$field->boost = $boostsByField[$key];
+      }
+      $doc->addField($field);
+    }
+    
+    // store the data fields (a big performance win over hydrating things with Doctrine)
+    foreach ($storedFields as $key => $value)
+    {
+      $doc->addField(Zend_Search_Lucene_Field::UnIndexed($key, $value, 'UTF-8'));
+    }
+   
+    // add item to the index
+    $index->addDocument($doc);
+    $index->commit();
+  }
+  
+  // This does a clean job of saving the object in both doctrine and zend
+  // without a lot of duplicated code, reducing the potential for
+  // bugs. However if you use it your class must implement 
+  // doctrineSave($conn), which is usually just a trivial wrapper around
+  // a call to parent::save($conn). 
+
+  // "What if I need to save additional related objects to some other
+  // table as part of the save() operation for this object, and I want
+  // that to be part of the transaction?" Do those things in 
+  // your doctrineSave() method.
+
+  static public function saveInDoctrineAndLucene($object, $culture = null, Doctrine_Connection $conn = null)
+  {
+    $conn = $conn ? $conn : $object->getTable()->getConnection();
+    $conn->beginTransaction();
+    try
+    {
+      $ret = $object->doctrineSave($conn);
+      $object->updateLuceneIndex($culture);
+      $conn->commit();
+      return $ret;
+    }
+    catch (Exception $e)
+    {
+      $conn->rollBack();
+      throw $e;
+    }
+  }
+
+  // This does a clean job of deleting the object from both doctrine and 
+  // zend without a lot of duplicated code, reducing the potential for
+  // bugs. However if you use it your class must implement 
+  // doctrineDelete($conn), which is a trivial wrapper around
+  // a call to parent::delete($conn) (unless you need to delete
+  // additional related objects from some other table perhaps, in
+  // which case you should do that work in doctrineDelete too).
+
+  static public function deleteFromDoctrineAndLucene($object, $culture = null, Doctrine_Connection $conn = null)
+  {
+    $conn = $conn ? $conn : $object->getTable()->getConnection();
+    $conn->beginTransaction();
+    try
+    {
+      $ret = $object->doctrineDelete($conn);
+      aZendSearch::deleteFromLuceneIndex($object, $culture); 
+      $conn->commit();
+      return $ret;
+    } 
+    catch (Exception $e)
+    {
+      $conn->rollBack();
+      throw $e;
+    }
+  }
+
+  // Implementation details
+
+  static protected $zendLoaded = false;
+  static public function registerZend()
+  {
+    if (self::$zendLoaded)
+    {
+      return;
+    }
+    
+    // Zend 1.8.0 and thereafter
+    include_once('Zend/Loader/Autoloader.php');
+    $loader = Zend_Loader_Autoloader::getInstance();
+    // NOT the default autoloader, Symfony's is the default.
+    // Thanks to Guglielmo Celata
+    // $loader->setFallbackAutoloader(true);
+    $loader->suppressNotFoundWarnings(false);
+    
+    // Before Zend 1.8.0
+    // require_once 'Zend/Loader.php';
+    // Zend_Loader::registerAutoload();
+    
+    self::$zendLoaded = true;
+    
+    // UTF8 tokenizer can be turned off if you don't have now off by default because it is really, really ignorant of English,
+    // it can't even cope with plural vs singular, much less stemming
+    
+    // Thanks to Fotis. Also thanks to the Zend Lucene source 
+    // for the second bit. iconv doesn't mean that PCRE was compiled
+    // with support for Unicode character classes, which the Lucene
+    // cross-language tokenizer requires to work. Lovely
+    if (function_exists('iconv') && (@preg_match('/\pL/u', 'a') == 1))
+    {
+      Zend_Search_Lucene_Analysis_Analyzer::setDefault(new Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8());
+    }
+  }
+
+  static public function getLuceneIndex(Doctrine_Table $table)
+  {
+    self::registerZend();
+   
+    if (file_exists($index = $table->getLuceneIndexFile()))
+    {
+      return Zend_Search_Lucene::open($index);
+    }
+    else
+    {
+      // We don't have to worry about creating the parent anymore because
+      // we're using aFiles::getWritableDataFolder()
+      
+      return Zend_Search_Lucene::create($index);
+    }
+  }
+   
+  static public function getLuceneIndexFile(Doctrine_Table $table)
+  {
+    return aFiles::getWritableDataFolder(array('zend_indexes')) .
+      DIRECTORY_SEPARATOR . 
+      $table->getOption('name').'.'.sfConfig::get('sf_environment').'.index';
+  }
+
+  static public function normalizeCulture($culture)
+  {
+    if (!strlen($culture))
+    {
+      $culture = sfConfig::get('sf_default_culture', 'en');
+    }
+    return $culture;
+  }
+}
+
+?>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMigrate.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMigrate.class.php	(revision 3140)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMigrate.class.php	(revision 3140)
@@ -0,0 +1,231 @@
+<?php
+
+// A wrapper for simple MySQL-based schema updates. See the apostrophe:migrate task for 
+// an example of usage
+
+class aMigrate
+{
+  protected $conn;
+  protected $commandsRun;
+  
+  public function __construct($conn)
+  {
+    $this->conn = $conn;
+  }
+  
+  // Used to run a series of queries where you don't need parameters or results
+  public function sql($commands)
+  {
+    foreach ($commands as $command)
+    {
+      echo("SQL statement:\n\n$command\n\n");
+      $this->conn->query($command);
+      $this->commandsRun++;
+    }
+  }
+  
+  // Runs a single query, with parameters. If :foo appears in the query it gets
+  // substituted correctly (via PDO) with $params['foo']. Extra stuff in
+  // $params is allowed, which is very helpful with toArray(). 
+  public function query($s, $params = array())
+  {
+    $pdo = $this->conn;
+    $nparams = array();
+    // I like to use this with toArray() while not always setting everything,
+    // so I tolerate extra stuff. Also I don't like having to put a : in front 
+    // of everything
+    foreach ($params as $key => $value)
+    {
+      if (strpos($s, ":$key") !== false)
+      {
+        $nparams[":$key"] = $value;
+      }
+    }
+    echo("SQL query:\n\n$s\n\n");
+    
+    $statement = $pdo->prepare($s);
+    try
+    {
+      $statement->execute($nparams);
+    }
+    catch (Exception $e)
+    {
+      echo($e);
+      echo("Statement: $s\n");
+      echo("Parameters:\n");
+      var_dump($params);
+      exit(1);
+    }
+    $result = true;
+    try
+    {
+      $result = $statement->fetchAll();
+    } catch (Exception $e)
+    {
+      // Oh no, we tried to fetchAll on a DELETE statement, everybody panic!
+      // Seriously PDO, you need to relax
+    }
+    $this->commandsRun++;
+    return $result;
+  }
+  
+  public function lastInsertId()
+  {
+    return $this->conn->lastInsertId();
+  }
+  
+  public function getCommandsRun()
+  {
+    return $this->commandsRun;
+  }
+  
+  public function tableExists($tableName)
+  {
+    if (!preg_match('/^\w+$/', $tableName))
+    {
+      die("Bad table name in tableExists: $tableName\n");
+    }
+    $data = array();
+    try
+    {
+      $data = $this->conn->query("SHOW CREATE TABLE $tableName")->fetchAll();
+    } catch (Exception $e)
+    {
+    }
+    return (isset($data[0]['Create Table']));    
+  }
+  
+  public function constraintExists($tableName, $constraintName)
+  {
+    if (!preg_match('/^\w+$/', $tableName))
+    {
+      die("Bad table name in tableExists: $tableName\n");
+    }
+    $data = array();
+    try
+    {
+      $data = $this->conn->query("SHOW CREATE TABLE $tableName")->fetchAll();
+    } catch (Exception $e)
+    {
+    }
+    if (!isset($data[0]['Create Table'])) 
+    {
+      return false;
+    }    
+    return (strpos($data[0]['Create Table'], 'CONSTRAINT `' . $constraintName . '`') !== false);
+  }
+  
+  public function columnExists($tableName, $columnName)
+  {
+    if (!preg_match('/^\w+$/', $tableName))
+    {
+      die("Bad table name in columnExists: $tableName\n");
+    }
+    if (!preg_match('/^\w+$/', $columnName))
+    {
+      die("Bad table name in columnExists: $columnName\n");
+    }
+    $data = array();
+    try
+    {
+      $data = $this->conn->query("SHOW COLUMNS FROM $tableName LIKE '$columnName'")->fetchAll();
+    } catch (Exception $e)
+    {
+    }
+    return (isset($data[0]['Field']));
+  }
+  
+  public function getTables()
+  {
+    return array_map(array($this, 'takeFirst'), $this->query('SHOW TABLES'));
+  }
+  
+  public function takeFirst($val)
+  {
+    return $val[0];
+  }
+  
+  public function upgradeCharsets()
+  {
+    $tables = $this->getTables();
+    foreach ($tables as $table)
+    {
+      $r = $this->query('SHOW CREATE TABLE ' . $table);
+      $c = $r[0]['Create Table'];
+      if (strpos($c, 'DEFAULT CHARSET=utf8') === false)
+      {
+        $this->query("alter table `$table` convert to character set utf8 collate utf8_general_ci");
+      }
+    }
+  }
+  
+  // Drop all integer foreign key constraints, turn both columns involved into BIGINTs, 
+  // and reestablish the constraints
+  public function upgradeIds()
+  {
+    $tables = $this->getTables();
+    $constraints = array();
+    $locals = array();
+    $foreigns = array();
+    foreach ($tables as $table)
+    {
+      $r = $this->query('SHOW CREATE TABLE ' . $table);
+      $c = $r[0]['Create Table'];
+      if (preg_match_all('/\sCONSTRAINT `(\w+)` FOREIGN KEY \(`(\w+)`\) REFERENCES `(\w+)` \(`(\w+)`\).*?\n/s', $c, $matches, PREG_SET_ORDER))
+      {
+        for ($i = 0; ($i < count($matches)); $i++)
+        {
+          list($constraint, $name, $local, $foreignTable, $foreign) = $matches[$i];
+          $constraint = preg_replace('/,\s*$/', '', $constraint);
+          
+          // If it isn't an old fashioned 4 byte int, it's none of our business
+          if (!preg_match("/`$local` int\(11\)/", $c))
+          {
+            echo("Skipping $local\n");
+            continue;
+          }
+          else
+          {
+            echo("NOT skipping $local\n");
+          }
+          
+          $constraints[$table][$name] = $constraint;
+          $locals[$table][] = $local;
+          $foreigns[$foreignTable][$foreign] = true;
+        }
+      }
+    }
+    
+    foreach ($constraints as $table => $tableConstraints)
+    {
+      foreach ($tableConstraints as $name => $constraint)
+      {
+        // There is no DROP CONSTRAINT for some strange reason
+        $this->query("ALTER TABLE $table DROP FOREIGN KEY `$name`");
+      }
+    }
+    
+    foreach ($locals as $table => $locals)
+    {
+      foreach ($locals as $foreignId)
+      {
+        $this->query("ALTER TABLE $table CHANGE $foreignId $foreignId BIGINT");
+      }
+    }
+    foreach ($foreigns as $table => $names)
+    {
+      foreach ($names as $id => $dummy)
+      {
+        // By default MySQL will toss out AUTO_INCREMENT if you change the type
+        $this->query("ALTER TABLE $table CHANGE $id $id BIGINT AUTO_INCREMENT");
+      }
+    }
+    foreach ($constraints as $table => $tableConstraints)
+    {
+      foreach ($tableConstraints as $name => $constraint)
+      {
+        $this->query("ALTER TABLE $table ADD $constraint");
+      }
+    }
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaCMSSlotsTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaCMSSlotsTools.class.php	(revision 2097)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaCMSSlotsTools.class.php	(revision 2097)
@@ -0,0 +1,21 @@
+<?php
+
+class aMediaCMSSlotsTools
+{
+  // You too can do this in a plugin dependent on a, see the provided stylesheet 
+  // for how to correctly specify an icon to go with your button. See the 
+  // apostrophePluginConfiguration class for the registration of the event listener.
+  static public function getGlobalButtons()
+  {
+    // Only if we have suitable credentials
+    if (aMediaTools::userHasUploadPrivilege())
+    {
+      aTools::addGlobalButtons(array(
+        new aGlobalButton('media', 'Media', 'aMedia/index', 'a-media', '/admin/media', 'aMedia')));
+    }
+  }
+  static private function i18nDummy()
+  {
+    __('Media');
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aGuid.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aGuid.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aGuid.class.php	(revision 9)
@@ -0,0 +1,19 @@
+<?php
+
+// GUIDs (Globally Unique IDentifiers) are randomly generated hexadecimal IDs 
+// with enough uniqueness for any situation. 16 hexadecimal digits are good 
+// enough for Microsoft and safe to be stored as text etc. They come in
+// handy all over the place: temporary filenames, verification codes, etc.
+
+class aGuid
+{
+  static public function generate()
+  {
+    $guid = "";
+    for ($i = 0; ($i < 8); $i++) {
+      $guid .= sprintf("%02x", mt_rand(0, 255));
+    }
+    return $guid;
+  }
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSubCrudTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSubCrudTools.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSubCrudTools.class.php	(revision 9)
@@ -0,0 +1,10 @@
+<?php
+
+class aSubCrudTools
+{ 
+  static public function getFormClass($model, $subtype)
+  {
+    return $model . ucfirst($subtype) . 'Form';
+  }
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/BaseaMediaTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/BaseaMediaTools.class.php	(revision 3134)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/BaseaMediaTools.class.php	(revision 3134)
@@ -0,0 +1,652 @@
+<?php
+
+class BaseaMediaTools
+{
+  // These are used internally. See aMediaSelect for the methods you probably want
+
+  static public function setSelecting($after, $multiple, $selection, 
+    $options = array())
+  {
+    $items = aMediaItemTable::retrieveByIds($selection);
+    $ids = array();
+    $imageInfo = array();
+    $selection = array();
+    foreach ($items as $item)
+    {
+      $croppingInfo = array();
+      if ($item->isCrop())
+      {
+        $croppingInfo = $item->getCroppingInfo();
+        $item = $item->getCropOriginal();
+      }
+      $id = $item->id;
+      $selection[] = $id;
+      $info = array('width' => $item->width, 'height' => $item->height);
+      $info = array_merge($info, $croppingInfo);
+      $imageInfo[$item->id] = $info;
+    }
+    
+    $cropping = isset($options['cropping']) && $options['cropping'];
+
+    aMediaTools::clearSelecting();
+    aMediaTools::setAttribute("selecting", true);
+    aMediaTools::setAttribute("after", $after);
+    aMediaTools::setAttribute("multiple", $multiple);
+    aMediaTools::setAttribute("cropping", $cropping);
+    aMediaTools::setAttribute("selection", $selection);
+    aMediaTools::setAttribute("imageInfo", $imageInfo);
+    foreach ($options as $key => $val)
+    {
+      aMediaTools::setAttribute($key, $val);
+    }
+  }
+  static public function clearSelecting()
+  {
+    aMediaTools::removeAttributes();
+  }
+  static public function isSelecting()
+  {
+    return aMediaTools::getAttribute("selecting");
+  }
+  static public function isMultiple()
+  {
+    return aMediaTools::getAttribute("multiple");
+  }
+  static public function getSelection()
+  {
+    return aMediaTools::getAttribute("selection", array());
+  }
+  static public function setSelection($array)
+  {
+    aMediaTools::setAttribute("selection", $array);
+  }
+  static public function getAfter()
+  {
+    return aMediaTools::getAttribute("after");
+  }
+  static public function isSelected($item)
+  {
+    if (is_object($item))
+    {
+      $id = $item->id;
+    }
+    else
+    {
+      $id = $item;
+    }
+    $selection = aMediaTools::getSelection();
+    return (array_search($id, $selection) != false);
+  }
+  static public function setSearchParameters($array)
+  {
+    aMediaTools::setAttribute("search-parameters", $array); 
+  }
+
+  static public function getSearchParameters($default = false)
+  {
+    if ($default === false)
+    {
+      $default = array();
+    }
+    return aMediaTools::getAttribute("search-parameters", $default);
+  }
+
+  static public function getSearchParameter($p, $default = false)
+  {
+    $parameters = aMediaTools::getSearchParameters();
+    if (isset($parameters[$p]))
+    {
+      return $parameters[$p];
+    }
+    return $default;
+  }
+
+  static public function getType()
+  {
+    return aMediaTools::getAttribute('type');
+  }
+  
+  static public function getBestTypeLabel()
+  {
+    $type = aMediaTools::getType();
+    if ($type)
+    {
+      if ($type === '_downloadable')
+      {
+        return 'File';
+      }
+      elseif (substr($type, 0, 1))
+      {
+        return 'Media';
+      }
+      $typeInfo = aMediaTools::getTypeInfo($type);
+      return $typeInfo['label'];
+    }
+    else
+    {
+      return 'Media';
+    }
+  }
+
+  static public function userHasUploadPrivilege()
+  {
+    $user = sfContext::getInstance()->getUser();
+    if (!$user->isAuthenticated())
+    {
+      return false;
+    }
+    $uploadCredential = aMediaTools::getOption('upload_credential');
+    if ($uploadCredential)
+    {
+      return $user->hasCredential($uploadCredential);
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+  static public function userHasAdminPrivilege()
+  {
+    $user = sfContext::getInstance()->getUser();
+    if (!$user->isAuthenticated())
+    {
+      return false;
+    }
+    $adminCredential = aMediaTools::getOption('admin_credential');
+    if ($adminCredential)
+    {
+      return $user->hasCredential($adminCredential);
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+  static protected function getUser()
+  {
+    return sfContext::getInstance()->getUser();
+  }
+
+  static public function getAttribute($attribute, $default = null)
+  {
+    $attribute = "aMedia-$attribute";
+    return aMediaTools::getUser()->getAttribute($attribute, $default, 'apostrophe_media');
+  }
+  
+  static public function setAttribute($attribute, $value = null)
+  {
+    $attribute = "aMedia-$attribute";
+    aMediaTools::getUser()->setAttribute($attribute, $value, 'apostrophe_media');
+  }
+  
+  static public function removeAttributes()
+  {
+    $user = aMediaTools::getUser();
+    $user->getAttributeHolder()->removeNamespace('apostrophe_media');
+  }
+  
+  // This is a good convention for plugin options IMHO
+  static protected $options = array(
+    "batch_max" => 6,
+    "per_page" => 20,
+    'linked_accounts' => true,
+    "popular_tags" => 10,
+    "video_search_per_page" => 9,
+    "video_search_preview_width" => 220,
+    "video_search_preview_height" => 170,
+    "video_account_preview_width" => 220,
+    "video_account_preview_height" => 170,
+    "upload_credential" => "media_upload",
+    "admin_credential" => "media_admin",
+    "gallery_constraints" => array(
+        "width" => 340,
+        "height" => false,
+        "resizeType" => "s"),
+    "selected_constraints" => array(
+        "width" => 100,
+        "height" => false,
+        "resizeType" => "c",),
+    "show_constraints" => array(
+        "width" => 720,
+        "height" => false,
+        "resizeType" => "s"),
+    "crop_constraints" => array(
+        "width" => 679,
+        "height" => 400,
+        "resizeType" => "s"),
+    'routes_register' => true,
+    'apipublic' => false,
+    'embed_codes' => false,
+    'apikeys' => array(),
+    'enabled_layouts' => array('one-up', 'two-up', 'four-up'),
+    // All mime types that are acceptable for upload to the media repository,
+    // keyed by the file extensions we save them under (regardless of the original name)
+    
+    'mime_types' => array(
+      "gif" => "image/gif",
+      "png" => "image/png",
+      "jpg" => "image/jpeg",
+      "pdf" => "application/pdf",
+      "mp3" => "audio/mpeg",
+      'xls' => 'application/vnd.ms-excel',
+      'ppt' => 'application/vnd.ms-powerpoint',
+      'doc' => 'application/msword',
+      'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+      'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+      'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+      'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+      'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+      'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+      'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+      'txt' => 'text/plain',
+      'rtf' => 'text/rtf'
+      ),
+      
+    // You can override these to add more types to the system. These are the 
+    // major types one can filter by in the media repository. Adding something here
+    // doesn't necessarily mean browsers can display it or our slots are designed
+    // to render it, in particular don't add new audio formats to 'audio' without
+    // overriding our audio slots to play them (and keep in mind the browser probably
+    // knows nothing about them)
+    
+    // Video has no extensions because we don't provide processing of video uploads,
+    // which are in a dizzying array of formats most browsers won't play. That's why
+    // YouTube exists. Videos are brought into the system via "Embed Media," not "Upload Media"
+
+    // Typically the only section you'll override here is 'file'. You can add more
+    // accepted extensions and/or break it up into 'Office' and 'Other' etc
+
+    // Also see 'getDownloadable' and 'getEmbeddable' in aMediaItem
+
+    'types' => array(
+      // You must have an image type
+      'image' => array('label' => 'Image', 'extensions' => array('gif', 'png', 'jpg'), 'embeddable' => false, 'downloadable' => true),
+      'pdf' => array('label' => 'PDF', 'extensions' => array('pdf'), 'embeddable' => false, 'downloadable' => true),
+      'audio' => array('label' => 'Audio', 'extensions' => array('mp3'), 'embeddable' => false, 'downloadable' => true),
+      // You must have a video type
+      'video' => array('label' => 'Video', 'extensions' => array(), 'embeddable' => true, 'downloadable' => false, 'embedServices' => array('YouTube', 'Vimeo')),
+      
+      // A long whitelist of file formats that are usually benign and useful.
+      // No .exe, no .zip. You can add them via app.yml if you really want them.
+      // We list only the non-macro-enabled Microsoft extensions in an effort to
+      // honor their good-faith attempt to label more dangerous files
+      
+      'office' => array('label' => 'Office', 'extensions' => array('txt', 'rtf', 'csv', 'doc', 'docx', 'xls', 'xlsx', 'xlsb', 'ppt', 'pptx', 'ppsx'), 'embeddable' => false, 'downloadable' => true)),
+    'embed_services' => array(
+      array('class' => 'aYoutube', 'media_type' => 'video'),
+      array('class' => 'aVimeo', 'media_type' => 'video'),
+      array('class' => 'aViddler', 'media_type' => 'video'),
+      array('class' => 'aSlideShare', 'media_type' => 'video'),
+		));
+
+  static protected $layouts = array(
+    'one-up' => array(
+        "name" => "one-up",
+        "image" => "/apostrophePlugin/images/a-icon-media-single.png",
+        "gallery_constraints" => array(
+          "width" => 340,
+          "height" => false,
+          "resizeType" => "s"),
+		    "show_constraints" => array(
+		        "width" => 720,
+		        "height" => false,
+		        "resizeType" => "s"),
+        "columns" => 1,
+        "fields" => array("controls" => 1,"thumbnail" => 1,"title" => 1, "description" => 1, 'dimensions'=> 1, "credit" => 1, "categories" => 1, "tags" => 1, 'view_is_secure' => 1, 'link' => 1, 'downloadable' => 1)
+      ),
+    'two-up' => array(
+        "name" => "two-up",
+        "image" => "/apostrophePlugin/images/a-icon-media-two-up.png",
+        "gallery_constraints" => array(
+          "width" => 340,
+          "height" => false,
+          "resizeType" => "s"),
+		    "show_constraints" => array(
+		        "width" => 720,
+		        "height" => false,
+		        "resizeType" => "s"),
+        "columns" => 2,
+        "fields" => array("controls" => 1,"thumbnail" => 1,"title" => 1, "description" => 1, 'dimensions' => 1, "credit" => 1, "categories" => 1, "tags" => 1, 'view_is_secure' => 1, 'link' => 1, 'downloadable' => 1)
+      ),
+      'four-up' => array(
+        "name" => "four-up",
+        "image" => "/apostrophePlugin/images/a-icon-media-grid.png",
+        "gallery_constraints" => array(
+          "width" => 340,
+          "height" => false,
+          "resizeType" => "s"),
+		    "show_constraints" => array(
+		        "width" => 720,
+		        "height" => false,
+		        "resizeType" => "s"),
+        "columns" => 4,
+        "fields" => array("controls" => 1,"thumbnail" => 1,'title' => 1)
+      ),
+      'thumbnail' => array(
+        "name" => "thumbnail",
+        "image" => "a-media-browse-thumbnail.png",
+        "gallery_constraints" => array(
+          "width" => 85,
+          "height" => false,
+          "resizeType" => "s"),
+		    "show_constraints" => array(
+		        "width" => 720,
+		        "height" => false,
+		        "resizeType" => "s"),
+        "columns" => 8,
+        "fields" => array("thumbnail" => 1)
+      )
+    );
+
+  static public function getOption($name)
+  {
+    if (isset(aMediaTools::$options[$name]))
+    {
+      $name = preg_replace("/[^\w]/", "", $name);
+      $key = "app_aMedia_$name";
+      return sfConfig::get($key, aMediaTools::$options[$name]);
+    }
+    else
+    {
+      throw new Exception("Unknown option in apostrophePlugin: $name");
+    }
+  }
+
+  static public function getLayout($name)
+  {
+    return aMediaTools::$layouts[$name];
+  }
+
+  static public function getEnabledLayouts()
+  {
+    return array_intersect_key(aMediaTools::$layouts, array_flip(aMediaTools::getOption('enabled_layouts')));
+  }
+  
+  static public function getTypeInfo($name)
+  {
+    $types = aMediaTools::getOption('types');
+    return $types[$name];
+  }
+
+  // Returns an array of type infos, just the one if you specify a type, all if you don't.
+  // Handy when filtering
+  static public function getTypeInfos($type = null)
+  {
+    if (preg_match('/^_(\w+)$/', $type, $matches))
+    {
+      $attribute = $matches[1];
+      $infos = aMediaTools::getTypeInfos();
+      $withAttribute = array();
+      foreach ($infos as $name => $info)
+      {
+        if (isset($info[$attribute]) && $info[$attribute])
+        {
+          $withAttribute[$name] = $info;
+        }
+      }
+      return $withAttribute;
+    }
+    $types = aMediaTools::getOption('types');
+    if (is_null($type))
+    {
+      return $types;
+    }
+    return array($type => $types[$type]);
+  }
+  
+	static public function getEmbedAllowed()
+	{
+	  foreach (aMediaTools::getTypeInfos(aMediaTools::getType()) as $typeInfo)
+	  {
+	    if ($typeInfo['embeddable'])
+	    {
+	      return true;
+	    }
+	  }
+		return false;
+	}
+
+	static public function getUploadAllowed()
+	{
+	  foreach (aMediaTools::getTypeInfos(aMediaTools::getType()) as $typeInfo)
+	  {
+	    if (count($typeInfo['extensions']))
+	    {
+				return true;
+	    }
+	  }
+		return false;
+	}
+
+  // Implementation conveniences shared by the engine and backend media actions classes
+  
+  // All actions using this method will accept either a slug or an id,
+  // for convenience
+  static public function getItem(sfActions $actions)
+  {
+    if ($actions->hasRequestParameter('slug'))
+    {
+      // Not sure why we're tolerant about this, but let's stay compatible with that
+      $slug = aTools::slugify($actions->getRequestParameter('slug'));
+      $item = Doctrine_Query::create()->
+        from('aMediaItem')->
+        where('slug = ?', array($slug))->
+        fetchOne();
+    }
+    else
+    {
+      $id = $actions->getRequestParameter('id');
+      $item = Doctrine::getTable('aMediaItem')->find($id);
+    }  
+    $actions->forward404Unless($item);
+    return $item;
+  }
+  
+  // refactored this into this static method from executeMultipleList() because it is now needed
+  // for executeUpdateMultiplePreview() for cropping slideshow items
+  static public function getSelectedItems()
+  {
+    $selection = aMediaTools::getSelection();
+    if (!is_array($selection))
+    {
+      throw new Exception("selection is not an array");
+    }
+    // Work around the fact that whereIn doesn't evaluate to AND FALSE
+    // when the array is empty (it just does nothing; which is an
+    // interesting variation on MySQL giving you an ERROR when the 
+    // list is empty, sigh)
+    if (count($selection))
+    {
+      // Work around the unsorted results of whereIn. You can also
+      // do that with a FIELD function
+      $unsortedItems = Doctrine_Query::create()->
+        from('aMediaItem i')->
+        whereIn('i.id', $selection)->
+        execute();
+      $itemsById = array();
+      foreach ($unsortedItems as $item)
+      {
+        $itemsById[$item->getId()] = $item;
+      }
+      $items = array();
+      foreach ($selection as $id)
+      {
+        if (isset($itemsById[$id]))
+        {
+          $items[] = $itemsById[$id];
+        }
+      }
+    }
+    else
+    {
+      $items = array();
+    }
+    
+    return $items;
+  }
+  
+  static public function getAspectRatio()
+  {
+    if (aMediaTools::getAttribute('aspect-width') && aMediaTools::getAttribute('aspect-width'))
+    {
+      return aMediaTools::getAttribute('aspect-width') / aMediaTools::getAttribute('aspect-height');
+    }
+    return 0;
+  }
+  
+  static public function getSelectedThumbnailHeight()
+  {
+    $selectedConstraints = aMediaTools::getOption('selected_constraints');
+    if (false === $selectedConstraints['height'])
+    {
+      if ($aspectRatio = aMediaTools::getAspectRatio())
+      {
+        return $selectedConstraints['width'] / $aspectRatio;
+      }
+      return 0; // Let's not divide by zero.
+    }
+    return $selectedConstraints['height'];
+  }
+  
+  /**
+   * This mirrors the default size math in aCrop.setAspectMask() in aCrop.js
+   */
+  static public function setDefaultCropDimensions($mediaItem)
+  {
+    $imageInfo = aMediaTools::getAttribute('imageInfo');
+    $aspectRatio = aMediaTools::getAspectRatio();
+    
+    if ($aspectRatio)
+    {    
+      if ($aspectRatio > 1)
+      {
+        $imageInfo[$mediaItem->id]['cropWidth'] = $mediaItem->getWidth();
+        $imageInfo[$mediaItem->id]['cropHeight'] = floor($mediaItem->getWidth() / $aspectRatio);
+      }
+      else
+      {
+        $imageInfo[$mediaItem->id]['cropHeight'] = $mediaItem->getHeight();
+        $imageInfo[$mediaItem->id]['cropWidth'] = floor($mediaItem->getHeight() * $aspectRatio);
+      }
+    }
+    else
+    {
+      $imageInfo[$mediaItem->id]['cropWidth'] = $mediaItem->getWidth();
+      $imageInfo[$mediaItem->id]['cropHeight'] = $mediaItem->getHeight();
+    }
+    
+    $imageInfo[$mediaItem->id]['cropLeft'] = 0;
+    $imageInfo[$mediaItem->id]['cropTop'] = floor(($mediaItem->getHeight() - $imageInfo[$mediaItem->id]['cropHeight']) / 2);
+        
+    aMediaTools::setAttribute('imageInfo', $imageInfo);
+  }
+  
+  static public function getNiceTypeName()
+  {
+    $type = aMediaTools::getAttribute('type', 'media item');
+    // The names of types are meant to be user friendly (in English), except for
+    // the metatypes like _downloadable which can't be user friendly and unique at the same time.
+    // I can't think of a nicer phrase for "all embeddable things" than "media item"
+    $niceNames = array('_downloadable' => 'file', '_embeddable' => 'media item');
+    if (isset($niceNames[$type]))
+    {
+      return $niceNames[$type];
+    }
+    return $type;
+  }
+  
+  // Safe for use with the sluggable behavior (aTools::slugify() has additional arguments, which get
+  // confused by the $item second parameter that we safely ignore here)
+  static public function slugify($path, $item)
+  {
+    return aTools::slugify($path);
+  }
+  
+  static protected $embedServices = array();
+  
+  // Default is to return only services that are ready to be used.
+  // If you pass boolean false, you'll get services that are NOT ready to be used.
+  // If you pass null, you'll get all services
+  static public function getEmbedServices($configured = true)
+  {
+    if (!isset(aMediaTools::$embedServices[$configured]))
+    {
+      aMediaTools::$embedServices[$configured] = array();
+      $serviceInfos = aMediaTools::getOption('embed_services');
+      foreach ($serviceInfos as $serviceInfo)
+      {
+        $class = $serviceInfo['class'];
+        $service = new $class;
+        $service->setType($serviceInfo['media_type']);
+        if ($configured)
+        {
+          if (!$service->configured())
+          {
+            continue;
+          }
+        }
+        elseif ($configured === false)
+        {
+          if ($service->configured())
+          {
+            continue;
+          }
+        }
+        else
+        {
+          // null = all
+        }
+        aMediaTools::$embedServices[$configured][] = $service;
+      }
+    }
+    return aMediaTools::$embedServices[$configured];
+  }
+  
+  static public function getEmbedService($nameUrlOrEmbed)
+  {
+    $services = aMediaTools::getEmbedServices();
+    foreach ($services as $service)
+    {
+      if ($service->getName() === $nameUrlOrEmbed)
+      {
+        return $service;
+      }
+    }
+    foreach ($services as $service)
+    {
+      if ($service->getIdFromUrl($nameUrlOrEmbed))
+      {
+        return $service;
+      }
+    }
+    foreach ($services as $service)
+    {
+      if ($service->getIdFromEmbed($nameUrlOrEmbed))
+      {
+        return $service;
+      }
+    }
+    return null;
+  }
+  
+  static public function getEmbedServiceNames()
+  {
+    $results = array();
+    $services = aMediaTools::getEmbedServices();
+    foreach ($services as $service)
+    {
+      $results[] = $service->getName();
+    }
+    return $results;
+  }
+
+  static public function filenameToTitle($filename)
+  {
+    $title = preg_replace('/\.\w+$/', '', $filename);
+    // *Not* aMediaTools::slugify, which is specifically for the slug of the media item
+    return aTools::slugify($title, false, false, ' ');
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aCategoryAdminGenerator.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aCategoryAdminGenerator.class.php	(revision 2333)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aCategoryAdminGenerator.class.php	(revision 2333)
@@ -0,0 +1,16 @@
+<?php
+
+class aCategoryAdminGenerator extends sfDoctrineGenerator
+{
+  public function renderField($field)
+  {
+    if($field->getType() == 'Category')
+    {
+      return sprintf('$helper->getCount(\'%s\', $a_category->id)', $field->getName());
+    }
+    else
+    {
+      return parent::renderField($field);
+    }
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTools.class.php	(revision 1635)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTools.class.php	(revision 1635)
@@ -0,0 +1,7 @@
+<?php
+
+// You can extend me at the project level by overriding this file with your own version and extending BaseaTools
+class aTools extends BaseaTools
+{
+  
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaImporter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaImporter.class.php	(revision 2453)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaImporter.class.php	(revision 2453)
@@ -0,0 +1,130 @@
+<?php
+
+class aMediaImporter
+{
+  // dir option must be the path to the folder to be imported. Note that the contents
+  // will be removed after import (folders and unsupported files will not be removed)
+  
+  // feedback option must be a callable. This callable will be invoked with three 
+  // arguments: $category, $message, and sometimes $file. $category will be 
+  // info, warning, error or completed. The first three receive a string as the $message
+  // and sometimes a related filename as the $file argument. The fourth, completed,
+  // receives a total number of files converted so far as the $message argument.
+  
+  public $feedback;
+  public $dir;
+  
+  public function __construct($options = array())
+  {
+    if (!isset($options['feedback']))
+    {
+      throw new sfException("Feedback option should be a valid callable");
+    }
+    $this->feedback = $options['feedback'];
+    if (!isset($options['dir']))
+    {
+      throw new sfException('dir option is mandatory');
+    }
+    $this->dir = $options['dir'];
+  }
+  
+  public function go()
+  {
+    $dir_iterator = new RecursiveDirectoryIterator($this->dir);
+    $iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
+    $count = 0;
+    $mimeTypes = aMediaTools::getOption('mime_types');
+    // It comes back as a mapping of extensions to types, get the types
+    $extensions = array_keys($mimeTypes);
+    $mimeTypes = array_values($mimeTypes);
+    foreach ($iterator as $sfile)
+    {
+      if ($sfile->isFile())
+      {
+        $file = $sfile->getPathname();
+        if (preg_match('/(^|\/)\./', $file))
+        {
+          # Silently ignore all dot folders to avoid trouble with svn and friends
+          $this->giveFeedback("info", "Ignoring dotfile", $file);
+          continue;
+        }
+        $pathinfo = pathinfo($file);
+        // basename and filename seem backwards to me, but that's how it is in the PHP docs and
+        // sure enough that's how it behaves
+        if ($pathinfo['basename'] === 'Thumbs.db')
+        {
+          continue;
+        }
+        $vfp = new aValidatorFilePersistent(
+          array('mime_types' => $mimeTypes,
+            'validated_file_class' => 'aValidatedFile',
+            'required' => false),
+          array('mime_types' => 'The following file types are accepted: ' . implode(', ', $extensions)));
+        $guid = aGuid::generate();
+        try
+        {
+          $vf = $vfp->clean(
+           array(
+             'newfile' => 
+               array('tmp_name' => $file, 'name' => $pathinfo['basename']), 
+             'persistid' => $guid)); 
+        } catch (Exception $e)
+        {
+          $this->giveFeedback("warning", "Not supported or corrupt", $file);
+          continue;
+        }
+        
+        $item = new aMediaItem();
+        
+        // Split it up to make tags out of the portion of the path that isn't dir (i.e. the folder structure they used)
+        $dir = $this->dir;
+        $dir = preg_replace('/\/$/', '', $dir) . '/';
+        $relevant = preg_replace('/^' . preg_quote($dir, '/') . '/', '', $file);
+        // TODO: not Microsoft-friendly, might matter in some setting
+        $components = preg_split('/\//', $relevant);
+        $tags = array_slice($components, 0, count($components) - 1);
+        foreach ($tags as &$tag)
+        {
+          // We don't strictly need to be this harsh, but it's safe and definitely
+          // takes care of some things we definitely can't allow, like periods
+          // (which cause mod_rewrite problems with pretty Symfony URLs).
+          // TODO: clean it up in a nicer way without being UTF8-clueless
+          // (aTools::slugify is UTF8-safe)
+          $tag = aTools::slugify($tag);
+        }
+        $item->title = aMediaTools::filenameToTitle($pathinfo['basename']);
+        $item->setTags($tags);
+        if (!strlen($item->title))
+        {
+          $this->giveFeedback("error", "Files must have a basename", $file);
+          continue;
+        }
+        // The preSaveImage / save / saveImage dance is necessary because
+        // the sluggable behavior doesn't kick in until save and the image file
+        // needs a slug based filename.
+        if (!$item->preSaveFile($vf))
+        {
+          $this->giveFeedback("error", "Save failed", $file);
+          continue;
+        }
+        $item->save();
+        if (!$item->saveFile($vf))
+        {
+          $this->giveFeedback("error", "Save failed", $file);
+          $item->delete();
+          continue;
+        }
+        unlink($file);
+        $count++;
+        $this->giveFeedback("completed", $count, $file);
+      }
+    }
+    $this->giveFeedback("total", $count);
+  }
+  
+  public function giveFeedback($category, $message, $file = null)
+  {
+    // Yes it IS silly that 'callable' arrays don't work as variable functions grr
+    call_user_func($this->feedback, $category, $message, $file);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImporter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImporter.class.php	(revision 3067)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImporter.class.php	(revision 3067)
@@ -0,0 +1,338 @@
+<?php
+
+class aImporter
+{
+  
+  protected $connection;
+  /**
+   *
+   * @var aSql
+   */
+  protected $sql;
+  protected $pageFiles = array();
+  protected $pagesDir;
+  protected $imagesDir;
+  protected $pseudoSlotTypes = array('foreignHtml' => 'aRichText');
+  protected $failedMedia = array();
+
+  public function __construct(Doctrine_Connection $connection, $params = array())
+  {
+    $this->connection = $connection;
+    $this->sql = new aSql($connection->getDbh());
+    $this->initialize($params);
+  }
+
+  public function initialize($params)
+  {
+    $this->root = simplexml_load_file($params['xmlFile']);
+    $this->pagesDir = $params['pagesDir'];
+    $this->imagesDir = $params['imagesDir'];
+  }
+
+  public function import()
+  {
+    $this->sql->query('DELETE FROM a_page where slug <> "global"');
+    $this->sql->query('DELETE FROM a_media_item');
+    foreach ($this->root->Page as $page)
+    {
+      $this->parsePage($page);
+    }
+    //Add admin pages
+    $root = current($this->sql->query('SELECT * FROM a_page where slug = :slug', array('slug' => '/')));
+    $admin = array('slug' => '/admin', 'admin' => '1');
+    $this->sql->insertPage($admin, 'Admin', $root['id']);
+    $adminMedia = array('slug' => '/admin/media', 'admin' => '1', 'engine' => 'aMedia');
+    $this->sql->insertPage($adminMedia, 'Media', $admin['id']);
+
+    foreach ($this->pageFiles as $id => $info)
+    {
+      $file = $this->pagesDir . "/$id.xml";
+      if(file_exists($file))
+      {
+        $root = simplexml_load_file($file);
+        if ($root)
+        {
+          $this->parseAreas($root, $info['id']);
+        }
+      }
+    }
+  }
+
+  public function parsePage(SimpleXMLElement $root, $parentId = null)
+  {
+    $info = array();
+    $info['slug'] = $root['slug']->__toString();
+    $info['template'] = isset($root['template']) ? $root['template']->__toString() : 'default';
+    $title = $root['title']->__toString();  
+
+    $this->sql->insertPage($info, $title, $parentId);
+
+    if (isset($root['file-id']))
+    {
+      $this->pageFiles[$root['file-id']->__toString()] = $info;
+    } else
+    {
+      $this->parseAreas($root, $info['id']);
+    }
+
+    foreach ($root->Page as $page)
+    {
+      $this->parsePage($page, $info['id']);
+    }
+
+    return $info;
+  }
+
+  public function parseAreas($root, $pageId)
+  {
+    foreach ($root->Area as $area)
+    {
+      $name = $area['name'];
+      if (count($area->AreaVersion))
+      {
+        //We are importing history also
+      } else
+      {
+        $slotInfos = array();
+        foreach ($area->Slot as $slot)
+        {
+          $type = $slot['type']->__toString();
+          $method = 'parseSlot' . $type;
+          if (method_exists($this, $method))
+          {
+            $slotImport = $this->$method($slot);
+            if($slotImport)
+            {
+              $slotInfos = array_merge($slotInfos, $slotImport);
+            }
+          }
+        }
+        if($slotInfos)
+          $this->sql->insertArea($pageId, $name, $slotInfos);
+      }
+    }
+  }
+
+  protected function getSlotType($type)
+  {
+    if (isset($this->pseudoSlotTypes[$type]))
+      return $this->pseudoSlotTypes[$type];
+
+    return $type;
+  }
+
+  protected function parseSlotARichText(SimpleXMLElement $slot)
+  {
+    $info = array();
+    $info['type'] = 'aRichText';
+    $info['value'] = aHtml::simplify($slot->value->__toString());
+
+    return array($info);
+  }
+
+  protected function parseSlotAText(SimpleXMLElement $slot)
+  {
+    $info = array();
+    $info['type'] = 'aText';
+    $info['value'] = aHtml::simplify($slot->value->__toString());
+
+    return array($info);
+  }
+
+  protected function parseSlotAButton(SimpleXMLElement $slot)
+  {
+    $info = array();
+    $info['type'] = 'aButton';
+    $value = array();
+    $value['title'] = (string) $slot->title;
+    $value['url'] = (string) $slot->url;
+
+    $ids = $this->getMediaItems($slot);
+    
+    if(count($ids))
+    {
+      $info['mediaId'] = $ids[0];
+    }
+    $info['value'] = $value;
+    return array($info);
+  }
+
+  protected function parseSlotAImage(SimpleXMLElement $slot)
+  {
+    $info = array();
+    foreach($this->getMediaItems($slot) as $id)
+    {
+      $info[] = array('type' => 'aImage', 'mediaId' => $id);
+    }
+
+    if(count($info))
+      return $info;
+
+    return false;
+  }
+
+  protected function parseSlotASlideshow(SimpleXMLElement $slot)
+  {
+    $info = array();
+    $value = array();
+    $order = array();
+    foreach($this->getMediaItems($slot) as $id)
+    {
+      $order[] = $id;
+    }
+    $value['order'] = $order;
+    $info = array('type' => 'aSlideshow', 'value' => $value);
+    return array($info);
+  }
+
+  public function getMediaItems(SimpleXMLElement $slot)
+  {
+    $ids = array();
+    foreach($slot->MediaItem as $item)
+    {
+      $id = $this->findOrAddMediaItem($item['src']);
+      if($id) 
+      {
+        $ids[] = $id;
+      }
+    }
+
+    return $ids;
+  }
+
+  protected function parseSlotForeignHtml(SimpleXMLElement $slot)
+  {
+    $html = $slot->value->__toString();
+    $segments = preg_split('/((?:<a href=".*?".*?>\s*)?(?:<br \/>|&nbsp;|\s)*<img.*?src=".*?".*?>(?:<br \/>|&nbsp;|\s)*(?:\s*<\/a>)?)/i', $html, null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+    foreach ($segments as $segment)
+    {
+      $mediaItem = null;
+      if (preg_match('/<img.*?src="(.*?)".*?>/i', $segment, $matches))
+      {
+        $src = $matches[1];
+        // &amp; won't work if we don't decode it to & before passing it to the server
+        $src = html_entity_decode($src);
+        $mediaId = $this->findOrAddMediaItem($src, 'id');
+        if (preg_match('/href="(.*?)"/', $segment, $matches))
+        {
+          $url = $matches[1];
+        }
+        // $mediaItem->save();
+        if (!is_null($mediaId))
+        {
+          $slotInfo = array('type' => 'aImage', 'mediaId' => $mediaId, 'value' => array());
+          if (isset($url))
+          {
+            $slotInfo = array('type' => 'aButton', 'value' => array('url' => $url, 'title' => ''), 'mediaId' => $mediaId);
+          }
+          $slotInfos[] = $slotInfo;
+        }
+      } else
+      {
+        $slotInfos[] = array('type' => 'aRichText', 'value' => aHtml::simplify($segment));
+      }
+    }
+    return $slotInfos;
+  }
+
+  protected function findOrAddMediaItem($src, $returnType = 'id', $tag = true)
+  {
+    $mediaId = null;
+    $slug = null;
+    $info = pathinfo($src);
+    $path = $info['dirname'] . '/' . $info['filename'];
+
+    $dirname = $info['dirname'];
+    // Remove any hostname before splitting for tags, also dump case differences
+    $dirname = strtolower(preg_replace('|^\w+://.*?/|', '', $dirname));
+    $tags = preg_split('#/#', $dirname);
+
+    $newTags = array();
+    foreach ($tags as $tag)
+    {
+      if (strlen($tag) > 1)
+      {
+        $newTags[] = $tag;
+      }
+    }
+    $tags = $newTags;
+
+    $extension = $info['extension'];
+    $slug = aTools::slugify($path) . "-$extension";
+
+    $filename = "web/uploads/media_items/$slug.original.$extension";
+    // We need to encode spaces but not slashes...
+    $src = str_replace(' ', '%20', $src);
+
+    if (substr($src, 0, 5) !== 'http:')
+    {
+      $src = $this->imagesDir . '/' . $src;
+    }
+
+    $result = $this->sql->query('SELECT id FROM a_media_item WHERE slug = :slug', array('slug' => $slug));
+    if (isset($result[0]['id']))
+    {
+      $mediaId = $result[0]['id'];
+    } else
+    {
+      $mediaItem = new aMediaItem();
+      $mediaItem->setTitle($slug);
+      $mediaItem->setSlug($slug);
+      if ($extension === 'pdf')
+      {
+        $mediaItem->setType('pdf');
+      } else
+      {
+        $mediaItem->setType('image');
+      }
+      if (file_exists($filename))
+      {
+        $mediaItem->preSaveFile($filename);
+      } else
+      {
+        $bad = isset($this->failedMedia[$src]);
+        if (!$bad)
+        {
+          $tmpFile = aFiles::getTemporaryFilename();
+          try
+          {
+            if (!copy($src, $tmpFile))
+            {
+              throw new sfException(sprintf('Could not copy file: %s', $src));
+            }
+            if (!$mediaItem->saveFile($tmpFile))
+            {
+              throw new sfException(sprintf('Could not save file: %s', $src));
+            }
+          } catch (Exception $e)
+          {
+            $this->failedMedia[$src] = true;
+          }
+          unlink($tmpFile);
+        }
+      }
+      if (!isset($this->failedMedia[$src]))
+      {
+        $this->sql->fastSaveMediaItem($mediaItem);
+        if ($tag)
+        {
+          $this->sql->fastSaveTags('aMediaItem', $mediaItem->id, $tags);
+        }
+        $mediaId = $mediaItem->id;
+        // getOriginalPath needs a context, ugh
+        $path = '/uploads/media_items/' . $mediaItem->slug . '.original.pdf';
+        $mediaItem->free(true);
+      }
+    }
+    if ($returnType === 'path')
+    {
+      return $path;
+    } else
+    {
+      return $mediaId;
+    }
+
+    return false;
+  }
+
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aFiles.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aFiles.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aFiles.class.php	(revision 9)
@@ -0,0 +1,223 @@
+<?php
+
+class aFiles
+{
+  /*
+   * Returns a data folder in which files can be read and written by
+   * the web server, but NOT seen as part of the server's document space. 
+   * Automatically checks for overriding path settings via app.yml so
+   * you can customize these directory settings.
+   * 
+   * getWritableDataFolder() returns sf_data_dir/a_writable unless 
+   * overridden by app_aToolkit_writable_dir. Note that this main directory
+   * is automatically chmodded appropriately by symfony project:permissions.
+   * (apostrophePlugin registers an event handler that extends this task.)
+   *
+   * getWritableDataFolder(array('indexes')) returns 
+   * sf_data_dir/a_writable/indexes unless overridden by 
+   * app_aToolkit_writable_indexes_dir (first preference) or 
+   * app_aToolkit_writable_dir (second preference). If app_aToolkit_writable_indexes_dir
+   * is not set, but app_aToolkit_writable_dir is found, then 
+   * /indexes will be appended to app_aToolkit_writable_dir.
+   *
+   * You may supply more than one component in the array. For instance,
+   * getWritableDataFolder(array('indexes', 'purple')) returns
+   * sf_data_dir/a_writable/indexes/purple unless overridden by
+   * app_aToolkit_writable_indexes_purple_dir (first choice), or
+   * app_aToolkit_writable_indexes_dir (second choice), or
+   * app_aToolkit_writable_dir (third choice). 
+   *
+   * You can also pass a single path argument rather than an
+   * array, in which case it is split into components at the slashes,
+   * with any leading and trailing slashes removed first.
+   * 
+   * Always attempts to create the folder if needed. This generally
+   * succeeds except for the top level sf_data_dir/a_writable folder,
+   * so you'll need to create that folder and make it readable, 
+   * writable and executable by the web server (chmod 777 in many cases).
+   * 
+   * Occurrences of SF_DATA_DIR in the final path will be automatically
+   * replaced with the value of sfConfig::get('sf_data_dir'). This is
+   * useful when specifying alternate paths in app.yml, e.g.
+   * (to be compatible with a very early release of our CMS):
+   *
+   * a_writable_zend_indexes: SF_DATA_DIR/zendIndexes
+   *
+   * SF_WEB_DIR is supported in the same way.
+   */
+  static public function getWritableDataFolder($components = array())
+  {
+    return self::getOrCreateFolder("app_aToolkit_writable_dir", 
+      sfConfig::get('sf_data_dir') . DIRECTORY_SEPARATOR . 'a_writable',
+      $components);
+  }
+
+  /*
+   * Returns a subfolder of the project's upload folder in which files
+   * can be read and written by the web server and also seen as part of the
+   * web server's document space. Automatically checks for overriding 
+   * path settings via app.yml so you can customize these directory settings.
+   * 
+   * getUploadFolder() returns sf_upload_dir unless 
+   * overridden by app_aToolkit_upload_dir.
+   *
+   * getUploadFolder(array('media')) returns sf_upload_dir/media 
+   * unless overridden by app_aToolkit_upload_media_dir (first preference) or 
+   * app_aToolkit_upload_dir (second preference). If app_aToolkit_upload_media_dir
+   * is not set, but app_aToolkit_upload_dir is found, then 
+   * /media will be appended to app_aToolkit_upload_dir.
+   *
+   * You may supply more than one component in the array. For instance,
+   * getUploadFolder(array('media', 'jpegs')) returns
+   * sf_upload_dir/media/jpegs unless overridden by
+   * app_aToolkit_upload_media_jpegs_dir (first choice), or
+   * app_aToolkit_upload_media_dir (second choice), or
+   * app_aToolkit_upload_dir (third choice).
+   * 
+   * You can also pass a single path argument rather than an
+   * array, in which case it is split into components at the slashes,
+   * with any leading and trailing slashes removed first.
+   *
+   * Always attempts to create the folder if needed. This generally
+   * succeeds because Symfony projects have a world-writable
+   * top-level web/upload folder by default.
+   *
+   * Occurrences of SF_DATA_DIR in the final path will be automatically
+   * replaced with the value of sfConfig::get('sf_data_dir'). This is
+   * useful when specifying alternate paths in app.yml, e.g.
+   * (to be compatible with a very early release of our CMS):
+   *
+   * a_writable_zend_indexes: SF_DATA_DIR/zendIndexes
+   *
+   * SF_WEB_DIR is supported in the same way.
+   */
+  static public function getUploadFolder($components = array())
+  {
+    return self::getOrCreateFolder("app_aToolkit_upload_dir",
+      sfConfig::get('sf_upload_dir'), $components);
+  }
+
+  /*
+   * Returns a subfolder of $basePath.
+   * Automatically checks for overriding path settings via app.yml 
+   * so you can customize these directory settings.
+   * 
+   * getOrCreateFolder('app_key_dir', '/path') returns /path unless
+   * overridden by the Symfony config setting app_key_dir. 
+   *
+   * getOrCreateFolder('app_key_dir', '/path', array('media')) returns 
+   * /path/media unless overridden by app_key_media_dir (first preference) or 
+   * app_key_dir (second preference). If app_key_media_dir
+   * is not set, but app_key_dir is set, then 
+   * /media will be appended to app_key_dir.
+   *
+   * You may supply more than one component in the array. For instance,
+   * getOrCreateFolder('app_key_dir', '/path', array('media', 'jpegs')) 
+   * returns /path/media/jpegs unless overridden by
+   * app_key_media_jpegs_dir (first choice), or
+   * app_key_media_dir (second choice), or
+   * app_key_dir (third choice).
+   *
+   * You can also pass a single path argument rather than an
+   * array, in which case it is split into components at the slashes,
+   * with any leading and trailing slashes removed first.
+   *
+   * Always attempts to create the folder if needed. This generally
+   * succeeds because Symfony projects have a world-writable
+   * top-level web/upload folder by default.
+   *
+   * Occurrences of SF_DATA_DIR in the final path will be automatically
+   * replaced with the value of sfConfig::get('sf_data_dir'). This is
+   * useful when specifying alternate paths in app.yml, e.g.
+   * (to be compatible with a very early release of our CMS):
+   *
+   * all:
+   *   aToolkit:
+   *     _writable_zend_indexes_dir: SF_DATA_DIR/zendIndexes
+   *
+   * SF_WEB_DIR is supported in the same way.
+   */
+  static public function getOrCreateFolder($baseKey, $basePath, $components = array())
+  {
+    if (!is_array($components))
+    {
+      $components = preg_split("/\//", $components, -1, PREG_SPLIT_NO_EMPTY);
+    }
+    $key = $baseKey;
+    $count = count($components);
+    $path = false;
+    $baseKeyStem = $baseKey;
+    $pos = strpos($baseKey, "_dir");
+    if ($pos !== false)
+    {
+      $baseKeyStem = substr($baseKey, 0, $pos) . "_";
+    }
+    for ($i = $count; ($i >= 0); $i--)
+    {
+      if ($i === 0)
+      {
+        $key = $baseKey;
+      }
+      else
+      {
+        $key = $baseKeyStem . 
+          implode("_", array_slice($components, 0, $i)) . "_dir";
+      }
+      $default = false;
+      if ($i === 0)
+      {
+        $default = $basePath;
+      }
+      $result = sfConfig::get($key, $default);
+      if ($result !== false)
+      {
+        $remainder = implode(DIRECTORY_SEPARATOR, array_slice($components, $i));
+        $ancestor = $result;
+        if (strlen($remainder))
+        {
+          $path = $result . DIRECTORY_SEPARATOR . $remainder;
+        }
+        else
+        {
+          $path = $result;
+        }
+        break;
+      }
+    }
+    
+    $path = str_replace(
+      array("SF_DATA_DIR", "SF_WEB_DIR"),
+      array(sfConfig::get('sf_data_dir'), sfConfig::get('sf_web_dir')),
+      $path);
+    if (!is_dir($path))
+    {
+      // There's a recursive mkdir flag in PHP 5.x, neato
+      if (!mkdir($path, 0777, true))
+      {
+        // It's better to report $ancestor rather than $path because
+        // creating that one parent should solve the problem
+        throw new Exception("Unable to create $ancestor the admin will probably need to do this manually the first time and set permissions so that the web server can write to that folder");
+      }
+    }
+    return $path;
+  }
+
+  /*
+   * Symfony has a getTempDir method in sfToolkit but it is only
+   * used by unit tests. It relies on the system temporary folder
+   * which might not always be accessible in a non-command-line
+   * PHP environment. Let's use something more local to our project.
+   */
+  static public function getTemporaryFileFolder()
+  {
+    return self::getWritableDataFolder(array("tmp"));
+  }
+  
+  static public function getTemporaryFilename()
+  {
+
+    $filename = aGuid::generate();
+    $tempDir = self::getTemporaryFileFolder();
+    return $tempDir . DIRECTORY_SEPARATOR . $filename;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrineRouteCollection.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrineRouteCollection.class.php	(revision 2008)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrineRouteCollection.class.php	(revision 2008)
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * aDoctrineRouteCollection represents a collection of routes bound to Doctrine objects via aDoctrineRoute
+ * for use in an Apostrophe engine module.
+ *
+ */
+class aDoctrineRouteCollection extends sfObjectRouteCollection
+{
+  protected $routeClass = 'aDoctrineRoute';
+  public function __construct(array $options)
+  {
+    // Prefix path is always empty since the engine page already brought us here
+    $options['prefix_path'] = '';
+    parent::__construct($options);
+  }
+  // Special case: the root route has to be /, even though we actually don't have a leading / on the index action of a home page
+  protected function getRouteForList()
+  {
+    return new $this->routeClass(
+      '/.:sf_format',
+      array_merge(array('module' => $this->options['module'], 'action' => $this->getActionMethod('list'), 'sf_format' => 'html'), $this->options['default_params']),
+      array_merge($this->options['requirements'], array('sf_method' => 'get')),
+      array('model' => $this->options['model'], 'type' => 'list', 'method' => $this->options['model_methods']['list'])
+    );
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aUrl.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aUrl.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aUrl.class.php	(revision 9)
@@ -0,0 +1,122 @@
+<?php
+
+class aUrl
+{
+
+  # http_build_query is nice but limited: it's not much use for
+  # modifying an existing URL that may or may not already contain
+  # a query string with one or more parameters. That's the gap this
+  # method fills:
+
+  # Add a hash of GET-method parameters to any URL. If there
+  # is no ? it is added. If there are existing parameters
+  # the leading & is supplied correctly. urlencode is called
+  # correctly, etc. 
+
+  # You can pass as many hashes of parameters
+  # as you wish, they get merged together. This is nice
+  # in templates because you can avoid playing with variables.
+
+  # Settings in later hashes override those found in earlier hashes
+  # or already packed in the $path. Passing a parameter with a false 
+  # (empty) value will remove that parameter from the URL if already present.
+
+  # ADG: added limited support for arrays as values (only one level deep).
+  # We should work on generalizing this to handle nested arrays
+  # just as http_build_query can.
+  
+  # TBB: addParamNoDelete() variant does NOT delete parameters with empty values.
+  
+  static public function addParams($path /*, $paramhash1, $paramhash2, */)
+  {
+    $args = func_get_args();
+    return self::addParamsBody($path, true, array_slice($args, 1));
+  }
+  
+  static public function addParamsNoDelete($path /*, $paramhash1, $paramhash2, */)
+  {
+    $args = func_get_args();
+    return self::addParamsBody($path, false, array_slice($args, 1));
+  }
+  
+  static public function addParamsBody($path, $deleteIfEmpty, $args)
+  {
+
+    $params = array();
+    $pos = strpos($path, '?');
+    if ($pos !== false) 
+    {
+      $ppairs = explode("&", substr($path, $pos + 1));  
+      $path = substr($path, 0, $pos);
+      foreach ($ppairs as $pair) 
+      {
+        // TBB 02/10/2009: careful, you will get one empty item if the
+        // string is empty after the ?
+        if (!strlen($pair))
+        {
+          continue;
+        }
+        list($key, $val) = explode("=", $pair);
+        $key = urldecode($key);
+        $val = urldecode($val);
+        $params[$key] = $val;
+      }
+    }
+    foreach ($args as $arg)
+    {
+      if (is_array($arg)) 
+      {
+        $params = array_merge($params, $arg);
+      }
+    }
+    # Filter out the blank parameters. That way
+    # Symfony doesn't generate things like /foo// that
+    # it will subsequently misinterpret. TBB
+
+		/**
+		 * Also, look for any potential params that are arrays, and break them out to be generated
+		 * in the foreach below.
+		 */
+    $nparams = array();
+    foreach ($params as $key => $val)
+		{
+			if (!is_array($val))
+			{
+	      if ($deleteIfEmpty && (!strlen($val)))
+				{
+	        continue;
+	      }
+
+	      $nparams[$key] = $val;
+			}
+			else
+			{
+				foreach ($val as $key2 => $val2)
+				{
+		      if (!strlen($val2))
+					{
+		        continue;
+		      }
+
+		      $nparams[$key.'['.$key2.']'] = $val2;
+				}
+			}
+    }
+    $params = $nparams;
+    if (!count($params)) {
+      return $path;
+    }
+    $path .= "?";
+    $first = true;
+    foreach ($params as $key => $val) {
+      if (!$first) {
+        $path .= "&";
+      }
+      $first = false;
+			
+      $path .= urlencode($key) . "=" . urlencode($val);
+    } 
+    return $path;
+  }
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aFeed.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aFeed.class.php	(revision 1078)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aFeed.class.php	(revision 1078)
@@ -0,0 +1,70 @@
+<?php
+
+class aFeed
+{
+	/**
+	 * Takes the url/routing rule of a feed and adds it to the request attributes to be read by
+	 * include_feeds() (see feedHelper.php), which is called in the layout. Allows for dynamic
+	 * inclusion of rel tags for RSS. 
+	 * http://spindrop.us/2006/07/04/dynamic-linking-to-syndication-feeds-with-symfony/
+	 *
+	 * @author Dave Dash (just this method)
+	 *
+	 * Unrelated to aFeed slots.
+	 */
+	public static function addFeed($request, $feed)
+	{
+		$feeds = $request->getAttribute('helper/asset/auto/feed', array());
+		
+		$feeds[$feed] = $feed;
+		
+		$request->setAttribute('helper/asset/auto/feed', $feeds);
+	}
+	
+	// Rock the Symfony cache to avoid fetching the same external URL over and over
+  
+  // These defaults are safe and boring and way faster than bashing on other servers.
+  // But here's a tip. If you don't have APC enabled your site is probably running very, 
+  // very slowly, so fix that. And then do this for even better speed:
+  //
+  // a:
+  //   feed:
+  //     cache_class: sfAPCCache
+  //     cache_options: { }
+
+  static public function fetchCachedFeed($url, $interval = 300)
+  {
+    $cacheClass = sfConfig::get('app_a_feed_cache_class', 'sfFileCache');
+    $cache = new $cacheClass(sfConfig::get('app_a_feed_cache_options', array('cache_dir' => aFiles::getWritableDataFolder(array('a_feed_cache')))));
+    $key = 'apostrophe:feed:' . $url;
+    $feed = $cache->get($key, false);
+    if ($feed === 'invalid')
+    {
+      return false;
+    }
+    else
+    {
+      if ($feed !== false)
+      {
+        // sfFeed is designed to serialize well
+        $feed = unserialize($feed);
+      }
+    }
+    if (!$feed)
+    {
+      try
+      {
+        $feed = sfFeedPeer::createFromWeb($url);    
+        $cache->set($key, serialize($feed), $interval);
+      }
+      catch (Exception $e)
+      {
+        // Cache the fact that the feed is invalid for 60 seconds so we don't
+        // beat the daylights out of a dead feed
+        $cache->set($key, 'invalid', 60);
+        return false;
+      }
+    }
+    return $feed;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaAPI.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaAPI.php	(revision 1917)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaAPI.php	(revision 1917)
@@ -0,0 +1,206 @@
+<?php
+
+// DEPRECATED: based on the old REST API, which we do not enable by default and found
+// to be a serious maintenance hassle as our client projects simply don't call for
+// shared media repositories.
+
+// Conveniences for Symfony code that uses the REST API.
+
+class aMediaAPI
+{
+  // These two are now conveniences built on top of the 
+  // new aMediaAPI object methods. The key argument has
+  // been removed in favor of the simplified client key
+  // discovery mechanism
+  
+  static public function getSelectedItem(sfRequest $request, $type = false)
+  {
+    $result = self::getSelectedItems($request, true, $type);
+    if (is_array($result))
+    {
+      if (count($result))
+      {
+        return $result[0];
+      }
+    }
+    return false;
+  }
+  
+  static public function getSelectedItems(sfRequest $request, $singular = false, $type = false)
+  {
+    if ($singular)
+    {
+      if (!$request->hasParameter('aMediaId'))
+      {
+        return false;
+      }
+      $id = $request->getParameter('aMediaId');
+      if (!preg_match("/^\d+$/", $id))
+      {
+        return false;
+      }
+      $ids = $id; 
+    }
+    else
+    {
+      if (!$request->hasParameter('aMediaIds'))
+      {
+        // User cancelled the operation in the media plugin
+        return false;
+      }
+      $ids = $request->getParameter('aMediaIds');
+      if (!preg_match("/^(\d+\,?)*$/", $ids))
+      {
+        // Bad input, possibly a hack attempt
+        return false;
+      }
+    }
+    $ids = explode(",", $ids);
+    if ($ids === false)
+    {
+      // Empty list, nothing to ask for
+      return array();
+    }
+    $api = new aMediaAPI();
+    $results = $api->getItems($ids);
+    if ($type !== false)
+    {
+      // This is intended to filter out user attempts to jam video into the list
+      // of ids before we ever got to the API stage
+      $nresults = array();
+      foreach ($results as $result)
+      {
+        if ($result->type === $type)
+        {
+          $nresults[] = $result;
+        }
+      }
+      $results = $nresults;
+    }
+    return $results;
+  }
+  
+  public function __construct($apikey = false, $site = false)
+  {
+    if ($apikey === false)
+    {
+      $apikey = sfConfig::get('app_aMedia_client_apikey');
+    }
+    $this->apikey = $apikey;
+    if ($site === false)
+    {
+      $site = sfConfig::get('app_aMedia_client_site', sfContext::getInstance()->getRequest()->getUriPrefix());
+    }
+    $this->site = $site;
+    if ($this->site === 'http://')
+    {
+      throw new sfException('You are probably running a task that utilizes aMediaAPI without calling aTaskTools::setCliHost(), or you are calling aTaskTools::setCliHost() but app_cli_host is not set in app.yml. It should be set to the fully qualified domain name of your site. Alternatively you can also set app_aMedia_client_site to specify a media plugin server running on a separate site.');
+    }
+  }
+  
+  // List all media tags (returns an array of strings)
+  public function getTags()
+  {
+    return $this->query('tags');
+  }
+        
+  // Returns a query matching media items satisfying the specified parameters, all of which
+  // are optional:
+  //
+  // tag
+  // search
+  // type (video, image or pdf)
+  // user (a username, to determine access rights)
+  // aspect-width and aspect-height (returns only images with the specified aspect ratio)
+  // minimum-width
+  // minimum-height
+  // width
+  // height 
+  // offset (zero-based offset into complete set of results)
+  // limit (max items to return, often used with offset to implement pagination)
+  //
+  // All parameters are optional. The server may impose a ceiling on the 
+  // number of results returned even if limit is not given, but will also indicate
+  // the true number of total matching items (see below).
+  //
+  // Matching items are returned in newest-first order unless a search parameter is present,
+  // in which case they are returned in descending order by match quality.
+  //
+  // The response will consist of an object with two members,
+  // total and items. total contains the total # of items matching the browse criteria
+  // (regardless of offset and limit). items contains an array of item info in exactly the same format
+  // returned by the getItems() method.
+  
+  public function browseItems($parameters)
+  {
+    return $this->query('info', $parameters);
+  }
+  
+  public function getItems($ids)
+  {
+    $result = $this->query('info', array('ids' => implode(',', $ids)));
+    if ($result !== false)
+    {
+      return $result->items;
+    }
+    return false;
+  }
+
+  protected $apikey;
+  protected $site;
+  
+  protected function getUrl($action)
+  {
+    return $this->site . "/media/$action";
+  }
+  
+  protected function completeParams(&$params)
+  {
+    $params['apikey'] = $this->apikey;
+    $user = sfContext::getInstance()->getUser();
+    // Send the user's username so the media plugin can decide if they are worthy of
+    // performing a particular action... unless this is disabled via app.yml or
+    // there is no sfGuardUser to get a username from.
+    if (sfConfig::get('app_aMedia_client_send_user', true))
+    {
+      if ($user->isAuthenticated() && method_exists($user, 'getGuardUser'))
+      {
+        $params['user'] = $user->getGuardUser()->getUsername();
+      }
+    }
+    // If the server site is explicitly specified, ask for
+    // absolute URLs for images, video, etc. Otherwise relative
+    // URLs are more convenient and compact
+    if (sfConfig::get('app_aMedia_client_site'))
+    {
+      $params['absolute'] = true;
+    }
+  }
+
+  protected function query($action, $params = array())
+  {
+    $this->completeParams($params);
+    $context = stream_context_create(array(
+       'http' => array(
+         'method'  => 'POST',
+         'header'  => "Content-type: application/x-www-form-urlencoded",
+         'content' => http_build_query($params),
+         'timeout' => 30,
+       ),
+     ));
+    $url = $this->site . "/media/$action";
+    $content = file_get_contents($url, false, $context);  
+    sfContext::getInstance()->getLogger()->info('ZZ action: ' . $url . ' params: ' . http_build_query($params) . ' tags: ' . $content);
+    
+    $response = json_decode($content);
+    if (!is_object($response))
+    {
+      return false;
+    }
+    if ($response->status !== 'ok')
+    {
+      return false;
+    }
+    return $response->result;    
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aString.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aString.class.php	(revision 2808)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aString.class.php	(revision 2808)
@@ -0,0 +1,279 @@
+<?php
+
+/**
+ * Tools, utilities and snippets collected and composed...
+ */
+
+class aString
+{
+	/**
+	* Limits the number of words in a string.
+	*
+	* @param string $string
+	*
+	* @param uint $word_limit
+	*   number of words to return
+	* 
+	* @param optional array
+	* 	if $options['append_ellipsis'] is set, append an ellipsis to the end 
+  *   of strings that have been truncated
+	*
+	* Whitespace will be collapsed to single spaces. UTF8-aware where supported
+	*
+	* @return string
+	*   new string containing only words up to the word limit.
+	*/
+	public static function limitWords($string, $word_limit, $options = array())
+	{
+	  $regexp = '/\s+/';
+	  if (function_exists('mb_strtolower'))
+    {
+      $regexp .= 'u';
+    }
+	  $words = preg_split($regexp, $string, $word_limit + 1);
+    $num_words = count($words);
+
+		# TBB: if there are $word_limit words or less, this check is necessary
+    # to prevent the last word from being lost.
+		if ($num_words > $word_limit)
+		{
+      array_pop($words);
+    }
+	  
+		$string = implode(' ', $words);
+		
+		$append_ellipsis = false;
+		if (isset($options['append_ellipsis']))
+		{
+			$append_ellipsis = $options['append_ellipsis'];
+		}
+		if ($append_ellipsis == true && $num_words > $word_limit)
+		{
+			$string .= '&hellip;';
+		}
+		
+		return $string;
+	}
+
+	/**
+	* Limits the number of characters in a string.
+	*
+	* @param string $string
+	*
+	* @param uint $character_limit
+	*   maximum number of characters to return, inclusive of any added ellipsis
+	*   NOTE: this is characters, not bytes (think UTF8). Be generous with columns
+	* 
+	* @param optional array
+	* 	if $options['append_ellipsis'] is set, append an ellipsis to the end 
+  *   of strings that have been truncated
+	*
+	* @return string
+	*   new string containing only characters up to the limit
+  * 
+  * Suitable when a word count limit is not enough (because words are
+  * sometimes unreasonably long).
+  *
+  * Tries to preserve word boundaries, but not too hard, as very long words can
+  * create problems of their own.
+	*/
+  public static function limitCharacters($s, $length, $options = array())
+  {
+    $ellipsis = "";
+    if (isset($options['append_ellipsis']) && $options['append_ellipsis'])
+    {
+      $ellipsis = "...";
+    }
+    if ($length < 12)
+    {
+      // Not designed to be elegant below this length
+      return aString::substr($s, 0, $length);
+    }
+    if (aString::strlen($s) > $length)
+    {
+      $s = aString::substr($s, 0, $length - aString::strlen($ellipsis));
+      $slength = aString::strlen($s);
+      for ($i = 1; ($i <= 10); $i++)
+      {
+        $c = aString::substr($s, $slength - $i, 1);
+        if (($c === ' ') || ($c === '\t') || ($c === '\r') || ($c === '\n'))
+        {
+          return aString::substr($s, 0, $slength) . $ellipsis;
+        }
+      }
+      return $s . $ellipsis;
+    }
+    return $s;
+  }
+	
+ 	/**
+  *
+	* Accepts an array of keywords and a text; returns the portion of the
+  * text beginning a few words prior to the first keyword encountered,
+  * and continuing to the end of the text. If none of the keywords are
+  * seen, returns the entire text.
+  *
+	* @param array $terms keywords
+  * @param string $text
+	*
+	* @return string
+  *
+	*/
+  public static function beginNear($keywords, $text)
+  {
+    foreach ($keywords as $keyword) {
+      # TODO: can we do this without so many calls? I don't want
+      # to capture an arbitrary number of words preceding - no more
+      # than three - and I don't want to reject cases with fewer
+      # than three preceding either. 
+      $keyword = preg_quote($keyword, '/');
+      for ($wordsPreceding = 3; ($wordsPreceding >= 0); $wordsPreceding--) {
+        $regexp = "(" . 
+          str_repeat("\w+\W+", $wordsPreceding) . ")(" . $keyword . ")" . "(.*)/is";
+        if (function_exists('mb_strtolower'))
+        {
+          $regexp .= 'u';
+        }
+        if (preg_match("/^" . $regexp, $text, $matches)) {
+          return $matches[1] . "<b>" . $matches[2] . "</b>" . $matches[3]; 
+        } 
+        if (preg_match("/" . $regexp, $text, $matches)) {
+          return "... " . $matches[1] . "<b>" . $matches[2] . "</b>" . $matches[3]; 
+        } 
+      }
+    }
+    return false;
+  }
+  
+ 	/**
+  *
+	* Accepts two text strings; returns a human-friendly representation of
+	* the difference between them. The strategy is to word-wrap the strings
+	* at a reasonably short boundary, split at line breaks, and then use
+	* array_diff (in both directions) to discover differences. This function
+	* returns an array like this:
+	*
+	* array(
+  *   "onlyin1" => 
+	*     array("first line unique to 1", "second line unique to 1..."), 
+	*   "onlyin2" => 
+	*     array("first line unique to 2", "second line unique to 2...")
+	* )
+	* It is suggested that, at a minimum, the first line of
+	* onlyin1 be displayed (with visual cues to indicate that it is gone in 2)
+	* and the first line of onlyin2 also be displayed (with visual cues to indicate
+	* that is new in 2). 
+	*
+	* TODO: detect situations in which content has been purely rearranged rather
+	* than edited, deleted or added, add preceding and trailing context, etc.
+	* These are all going to be a lot less efficient than this simple
+	* implementation though.
+  *
+	* @param string $text1
+  * @param string $text2
+	*
+	* @return array
+  *
+	*/
+  
+  public static function diff($text1, $text2)
+  {
+    $array1 = array_map('trim', explode("\n", wordwrap($text1, 70)));
+    $array2 = array_map('trim', explode("\n", wordwrap($text2, 70)));
+    $onlyin1 = array_values(array_diff($array1, $array2));
+    $onlyin2 = array_values(array_diff($array2, $array1));
+    if (count($onlyin1) && count($onlyin2))
+    {
+      // The first line is critical because history displays
+      // so little of a diff. So remove any shared prefix from the
+      // first deleted and first added lines unless that means we'd
+      // take it all
+      $s1 = $onlyin1[0];
+      $s2 = $onlyin2[0];
+      if (strlen($s1) !== strlen($s2))
+      {
+        $min = min(strlen($s1), strlen($s2));
+        for ($i = 0; ($i < $min); $i++)
+        {
+          $c1 = substr($s1, $i, 1);
+          $c2 = substr($s2, $i, 1);
+          if ($c1 !== $c2)
+          {
+            break;
+          }
+        }
+        $onlyin1[0] = substr($s1, $i);
+        $onlyin2[0] = substr($s2, $i);
+        if (!strlen($onlyin1[0]))
+        {
+          array_shift($onlyin1);
+        }
+        if (!strlen($onlyin2[0]))
+        {
+          array_shift($onlyin2);
+        }
+      }
+    }
+    return array("onlyin1" => array_values($onlyin1), "onlyin2" => array_values($onlyin2));
+  }
+  
+  static public function strtolower($s)
+  {
+    if (function_exists('mb_strtolower'))
+    {
+      return mb_strtolower($s, 'UTF-8');
+    }
+    else
+    {
+      return strtolower($s);
+    }
+  }
+
+  static public function strlen($s)
+  {
+    if (function_exists('mb_strlen'))
+    {
+      return mb_strlen($s, 'UTF-8');
+    }
+    else
+    {
+      return strlen($s);
+    }
+  }
+
+  static public function substr($s, $start, $length = null)
+  {
+    // Frustratingly you can't pass 'null' as a safe way of skipping the length
+    // parameter, even with mb_substr which takes a fourth 'encoding' argument, so you
+    // have to make a superfluous mb_strlen call
+    if (function_exists('mb_substr'))
+    {
+      return mb_substr($s, $start, is_null($length) ? mb_strlen($s) : $length, 'UTF-8');
+    }
+    else
+    {
+      return substr($s, $start, is_null($length) ? strlen($s) : $length);
+    }
+  }
+  
+  static public function firstLine($s)
+  {
+    $ln = strpos($s, "\n");
+    if ($ln === false)
+    {
+      return $s;
+    }
+    return substr($s, 0, $ln);
+  }
+  
+  static public function toVcal($s)
+  {
+    // vcal is fairly picky. Avoid a lot of problems by
+    // simplifying whitespace
+    $s = preg_replace('/\s+/', ' ', $s);
+    $s = trim($s);
+    $s = addslashes($s);
+    return $s;
+  }
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverterTest.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverterTest.php	(revision 1917)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverterTest.php	(revision 1917)
@@ -0,0 +1,18 @@
+<?php
+
+class sfConfig
+{
+  static public function get($key, $default)
+  {
+    return $default;
+  }
+}
+require 'aImageConverter.class.php';
+
+// aImageConverter::scaleToFit("testin.jpg", "testoutscaletofit.jpg", 400, 300);
+// aImageConverter::scaleByFactor("testin.jpg", "testoutscalebyfactor.jpg", 0.5);
+// aImageConverter::scaleToFit("testin.jpg", "testoutscaleoriginalbobbi.jpg", 340, 451);
+// aImageConverter::cropOriginal("testin.jpg", "testoutcroporiginaltall.jpg", 100, 300);
+// aImageConverter::scaleToNarrowerAxis("testin.jpg", "testoutscaletonarroweraxis.jpg", 300, 200);
+// aImageConverter::cropOriginal("testin.jpg", "testoutcroporiginaltall.jpg", 100, 300);
+aImageConverter::cropOriginal("testin.jpg", "testoutcroporiginalcorner.jpg", 100, 100, null, 200, 200, 200, 200);
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrine.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrine.class.php	(revision 2593)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrine.class.php	(revision 2593)
@@ -0,0 +1,51 @@
+<?php
+
+// Conveniences for cross-database-compatible Doctrine programming 
+
+class aDoctrine
+{
+  // Used to order the results of a query according to a specific list of IDs. 
+  // If we used FIELD we would be limited to MySQL. So we use CASE instead (SQL92 standard).
+  
+  // Note that you are still responsible for adding a whereIn clause, if you
+  // want to limit the results to this list of ids. If you don't, any extra objects
+  // will be returned at the end.
+  
+  // YOU NEED TO HAVE AN EXPLICIT SELECT CLAUSE, if you don't the select clause added by this
+  // method will override the default 'select everything' behavior and you will
+  // get back nothing! I get burned by this myself.
+  
+  // Example: 
+  //
+  // $q = Doctrine::getTable('aMediaItem')->createQuery('m')->select('m.*')->whereIn('m.id', $ids);
+  // $mediaItems = aDoctrine::orderByList($q, $ids)->execute();
+  
+  static public function orderByList($query, $ids, $modelName = null)
+  {
+    // If there are no IDs, then we don't alter the query at all. Otherwise we wind up
+    // with an ELSE clause alone, which is an error in SQL
+    if (!count($ids))
+    {
+      return $query;
+    }
+    if (is_null($modelName))
+    {
+      $modelName = $query->getRootAlias();
+    }
+    $col = $modelName . '.id';
+    $n = 1;
+    $select = "(CASE $col";
+    foreach ($ids as $id)
+    {
+      $id = (int) $id;
+      $select .= " WHEN $id THEN $n";
+      $n++;
+    }
+    $select .= " ELSE $n";
+    $select .= " END) AS id_order";
+    $query->addSelect($select);
+    $query->orderBy("id_order ASC");
+    // Now it's a little more chainable
+    return $query;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDimensions.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDimensions.class.php	(revision 1917)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDimensions.class.php	(revision 1917)
@@ -0,0 +1,74 @@
+<?php
+
+class aDimensions
+{
+  public static function constrain($originalWidth, $originalHeight, $originalFormat, $options)
+  {
+    if (!isset($options['width']))
+    {
+      throw new sfException("No width parameter in options");
+    }
+    $width = $options['width'];
+    if (!isset($options['height']))
+    {
+      throw new sfException("No height parameter in options (specify false for flexHeight)");
+    }
+    $height = $options['height'];
+    if ($height === false)
+    {
+      $height = ceil(($width * $originalHeight) / $originalWidth);
+    }
+    if (!isset($options['resizeType']))
+    {
+      throw new sfException("No resizeType parameter in options");
+    }
+    $resizeType = $options['resizeType'];
+    $cropLeft = isset($options['cropLeft']) ? $options['cropLeft'] : null;
+    $cropTop = isset($options['cropTop']) ? $options['cropTop'] : null;
+    $cropWidth = isset($options['cropWidth']) ? $options['cropWidth'] : null;
+    $cropHeight = isset($options['cropHeight']) ? $options['cropHeight'] : null;
+    
+    if (isset($options['scaleWidth']) && isset($options['scaleHeight']) && !is_null($cropLeft) && !is_null($cropTop) && !is_null($cropWidth) && !is_null($cropHeight))
+    {
+      $scalingFactor =  $originalWidth / $options['scaleWidth'];
+            
+      $cropLeft = floor($scalingFactor * $cropLeft);
+      $cropTop = floor($scalingFactor * $cropTop);
+      $cropWidth = floor($scalingFactor * $cropWidth);
+      $cropHeight = floor($scalingFactor * $cropHeight);
+    }
+    
+    if (!(isset($options['forceScale']) && $options['forceScale']))
+    {
+      // Never exceed original size, but don't exceed requested size on the other axis
+      // as a consequence either
+      if ($originalWidth < $width)
+      {
+        $height = ceil($height * ($originalWidth / $width));
+        $width = $originalWidth;
+      }
+      if ($originalHeight < $height)
+      {
+        $width = ceil($width * ($originalHeight / $height));
+        $height = $originalHeight;
+      }
+    }
+    if (isset($options['format']))
+    {
+      $format = $options['format'];
+    }
+    else
+    {
+      $format = $originalFormat;
+    }
+    if ($format === 'pdf')
+    {
+      // aImageConverter can't render PDF as output anyway, so we know we will always
+      // be converting pdf to something else
+      $format = 'jpg';
+    }
+    
+    return array("width" => $width, "height" => $height, "format" => $format, "resizeType" => $resizeType,
+      "cropLeft" => $cropLeft, "cropTop" => $cropTop, "cropWidth" => $cropWidth, "cropHeight" => $cropHeight);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouteTools.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouteTools.php	(revision 2982)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouteTools.php	(revision 2982)
@@ -0,0 +1,220 @@
+<?php
+
+// A helper class containing methods to be called from subclasses of sfRoute that are
+// intended for use with apostrophe engines. Keeping this code here minimizes duplication
+// and avoids the need for frequent changes to multiple classes when this code is modified.
+// This is poor man's multiple inheritance. See aRoute and aDoctrineRoute
+
+class aRouteTools
+{
+  /**
+   * Returns the portion of the URL after the engine page slug, or false if there
+   * is no engine page matching the URL. As a special case, if the URL exactly matches the slug,
+   * / is returned.
+   *
+   * @param  string  $url     The URL
+   *
+   * @return string The remainder of the URL
+   */
+  static public function removePageFromUrl(sfRoute $route, $url)
+  {
+    $remainder = false;
+    $page = aPageTable::getMatchingEnginePage($url, $remainder);
+    if (!$page)
+    {
+      return false;
+    }
+    // Engine pages can't have subpages, so if the longest matching path for any engine page
+    // has the wrong engine type for this route, this route definitely doesn't match
+    $defaults = $route->getDefaults();
+    if ($page->engine !== $defaults['module'])
+    {
+      return false;
+    }
+    // Allows aRoute URLs to be written like ordinary URLs rather than
+    // specifying an empty URL, which seems prone to lead to incompatibilities
+    
+    // Remainder comes back as false, not '', for an exact match
+    if (!strlen($remainder))
+    {
+      $remainder = '/';
+    }
+    return $remainder;
+  }
+  
+  protected static $targetEnginePages = array();
+
+  /**
+   *
+   * THIS METHOD WILL NOT WORK RELIABLY UNLESS THE ROUTING CACHE IS TURNED **OFF**.
+   *
+   * The routing cache defaults to off in new Symfony 1.3 and 1.4 projects because
+   * it has found to hurt performance in most cases, sometimes quite severely. We do 
+   * not currently enable it on any of our projects.
+   * 
+   * The routing cache does not take the desired engine page into account, so it will
+   * return URLs targeting the wrong page. If you must use the routing cache,
+   * design your projects to avoid the use of multiple engine pages for the
+   * same engine module.
+   *
+   * This method sets a specific target engine page for any url_for, link_to, etc. 
+   * calls invoking an engine route. If you have only one instance of a given engine 
+   * in your site, you don't need to call this method. A link generated within that 
+   * engine page will target the same engine page, and a link generated from anywhere 
+   * else will target the first engine page for that engine module name found in 
+   * the database. If you have more than one engine page for the same engine module 
+   * name, and you care which one the link points to, call this method to specify 
+   * that page. 
+   *
+   * A stack of target engine slugs is maintained for each engine module name.
+   * This allows you to push a new engine page at the top of a partial or component
+   * that potentially targets a different engine page than the template that
+   * invoked it, and then pop that engine page at the end to ensure that any links
+   * generated later in the calling template still target the original engine page.
+   *
+   * You can pass a page object or, for convenience, a page slug. The latter is useful
+   * when targeting an engine page that is guaranteed to exist, such as /admin/media
+   *
+   * @param  aPage $page|string $page The target engine page for engine routes, or a page slug
+   *
+   */
+  
+  static public function pushTargetEnginePage($page, $engine = null)
+  {
+    if (!(is_object($page) && ($page instanceof aPage)))
+    {
+      if(is_null($engine))
+      {
+        $page = aPageTable::retrieveBySlug($page);
+        $engine = $page->engine;
+      }
+      $slug = $page;
+    }
+    else
+    {
+      $slug = $page->slug;
+      $engine = $page->engine;
+    }
+    self::$targetEnginePages[$engine][] = $slug;
+  }
+
+  /**
+   *
+   * @param string $slug The target page slug for engine routes
+   * @param string $engine The type of engine the page is
+   */
+  static public function pushTargetEngineSlug($slug, $engine)
+  {
+    self::$targetEnginePages[$engine][] = $slug;
+  }
+
+  /**
+   * Pops the most recent target engine page for the specified engine name.
+   * See aRouteTools::pushTargetEnginePage for more information.
+   *
+   * @param  string $engine The engine name in question
+   *
+   */
+  static public function popTargetEnginePage($engine)
+  {
+    self::popTargetEngine($engine);
+  }
+
+  /**
+   * Pops the most recent target engine page for the specified engine name.
+   * See aRouteTools::pushTargetEnginePage for more information.
+   *
+   * @param  string $engine The engine name in question
+   *
+   */
+  static public function popTargetEngine($engine)
+  {
+    array_pop(self::$targetEnginePages[$engine]);
+  }
+  
+  /**
+   * If an engine page has already been pushed or we are on an engine page now,
+   * returns that engine page slug. Otherwise returns null. Useful to determine
+   * whether you should get clever or not in a getEngineSlug() method for an
+   * aDoctrineRoute.
+   *
+   * @param  sfRoute $route
+   *
+   * @return string The engine slug, or null
+   */
+  
+  static public function getContextEngineSlug(sfRoute $route)
+  {
+    $defaults = $route->getDefaults();
+    $currentPage = aTools::getCurrentPage();
+    $engine = $defaults['module'];
+    if (isset(self::$targetEnginePages[$engine]) && count(self::$targetEnginePages[$engine]))
+    {
+      return end(self::$targetEnginePages[$engine]);
+    }
+    elseif (($currentPage) && ($currentPage->engine === $defaults['module']))
+    {
+      return $currentPage->slug;
+    }
+    else
+    {
+      return null;
+    }
+  }
+  
+  /**
+   * Prepends the current CMS page to the URL.
+   *
+   * @param  string $url The URL so far obtained from parent::generate
+   * @param  Boolean $absolute  Whether to generate an absolute URL
+   *
+   * @return string The generated URL
+   */
+  
+  static public function addPageToUrl(sfRoute $route, $url, $absolute)
+  {
+    $slug = aRouteTools::getContextEngineSlug($route);
+    if (!$slug)
+    {
+      $defaults = $route->getDefaults();
+      $page = aPageTable::getFirstEnginePage($defaults['module']);
+      if (!$page)
+      {
+        $slug = null;
+      }
+      else
+      {
+        $slug = $page->slug;
+      }
+    }
+    if (!$slug)
+    {
+      throw new sfException('Attempt to generate aRoute URL for module ' . $defaults['module'] . ' with no matching engine page on the site');
+    }
+    // A route URL of / for an engine route maps to the page itself, without a trailing /
+    if ($url === '/')
+    {
+      $url = '';
+    }
+    // Ditto for / followed by a query string (missed this before)
+    if (substr($url, 0, 2) === '/?')
+    {
+      $url = substr($url, 1);
+    }
+    
+    $pageUrl = aTools::urlForPage($slug, $absolute);
+    
+    $rr = preg_quote(sfContext::getInstance()->getRequest()->getRelativeUrlRoot(), '/');
+    
+    // Strip controller off so it doesn't duplicate the controller in the 
+    // URL we just generated. Also strip off sf_relative_root if any. 
+    // We could use the slug directly, but that would
+    // break if the CMS were not mounted at the root on a particular site.
+    // Take care to function properly in the presence of an absolute URL
+    if (preg_match("/^(?:https?:\/\/[^\/]+)?$rr(?:\/[^\/]+\.php)?(.*)$/", $pageUrl, $matches))
+    {
+      $pageUrl = $matches[1];
+    }
+    return $pageUrl . $url;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaSelect.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaSelect.class.php	(revision 1917)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaSelect.class.php	(revision 1917)
@@ -0,0 +1,104 @@
+<?php
+
+// Conveniences for selecting content with the media repository.
+// This replaces aMediaAPI for new projects in which the media repository
+// is part of the same site.
+
+class aMediaSelect
+{
+  // Let's have a nice non-static API to be a bit more futureproof
+  public function __construct()
+  {
+  }
+  
+  public function getSelectedItem()
+  {
+    $result = aMediaTools::getSelection();
+    if (count($result))
+    {
+      return $result[0];
+    }
+    return false;
+  }
+  
+  public function getSelectedItems()
+  {
+    return aMediaTools::getSelection();
+  }
+  
+  // Returns a hash by image id of hashes containing cropping info:
+  // cropLeft, cropTop, cropWidth, cropHeight. There may not be
+  // cropping info for images that were never cropped. In such cases
+  // you should refer to the original dimensions of the image
+  
+  public function getCroppingInfo()
+  {
+    $croppingInfo = array();
+    $imageInfo = aMediaTools::getAttribute('imageInfo', array());
+    $selection = aMediaTools::getSelection();
+    foreach ($selection as $item)
+    {
+      if (isset($imageInfo['cropLeft']))
+      {
+        $info = array('cropLeft' => $imageInfo['cropLeft'],
+          'cropTop' => $imageInfo['cropTop'],
+          'cropWidth' => $imageInfo['cropWidth'],
+          'cropHeight' => $imageInfo['cropHeight']);
+        $croppingInfo[$item->id] = $info;
+      }
+    }
+    return $croppingInfo;
+  }
+  
+  // Select a media item or items, then redirect to the URL
+  // specified by the $after parameter, at which time the above
+  // information retrieving methods are valid for use.
+  
+  // The $actions parameter should be the current actions class
+  // ($this, if you are writing an executeFoo method).
+  
+  // $after is the URL to redirect to after the selection is completed or cancelled.
+  // For backwards compatibility this URL will receive several GET method parameters,
+  // however you should use the methods above rather than consulting them. The methods
+  // above are not limited by URL length considerations.
+
+  // $currentIds should contain a list of ids or a list of aMediaItems that are
+  // currently selected (allowing the user to modify the list rather than making
+  // an entirely new selection), or a single item or id, or false for no current selection.
+
+  // $options is a hash which may contain:
+  
+  // multiple => true: allow multiple media items to be selected
+  // 'type', 'aspect-width', 'aspect-height', 'minimum-width', 'minimum-height', 
+  // 'width', 'height': enforce these constraints on type or dimensions
+  // type can currently be image, video or pdf
+  // 'label': set the reminder message that appears at the top of the media browser
+  // to remind the user why they are there and what they are looking for
+  // 'cropping' => true: allow the user to crop each selected item. Cropping
+  // parameters can be retrieved later with getCroppingInfo()
+  // 'croppingInfo' => an array of existing cropping info as returned by
+  // getCroppingInfo after a previous successful selection. Allows the user to
+  // edit a selection with existing cropping choices
+
+  public function select($actions, $after, $currentIds = false, $options = array())
+  {
+    if ($currentIds === false)
+    {
+      $currentIds = array();
+    }
+    elseif ($currentIds instanceof aMediaItem)
+    {
+      $currentIds = array($currentIds);
+    }
+    elseif (!is_array($currentIds))
+    {
+      $currentIds = array($currentIds);
+    }
+    if ($currentIds[0] instanceof aMediaItem)
+    {
+      $currentIds = aArray::getIds($currentIds);
+    }
+    aMediaTools::setSelecting($after, $options['multiple'], $ids, $options);
+    return $actions->redirect("aMedia/index");
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTaskTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTaskTools.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTaskTools.class.php	(revision 9)
@@ -0,0 +1,89 @@
+<?php
+
+class aTaskTools
+{
+  /**
+   * Signs in as a superuser (ataskuser) and creates a suitable context and db connection.
+   *
+   * @param sfConfiguration $configuration  An sfConfiguration instance
+   * @param string          $connectionName The connection name (defaults to doctrine)
+   *
+   *
+   * In addition to signing in as ataskuser (a superadmin with all privileges), this method also
+   * creates a context and a database connection to prevent "default context not found" errors elsewhere.
+   *
+   * The signInAsTaskUser method is intended to be called at the beginning of the execute method 
+   * of your task. 
+   *
+   * This method sets up a context, opens a Doctrine database connection, and signs in as the 
+   * ataskuser superadmin user, ensuring that privileges are available on objects that check 
+   * privileges on a user by user basis. This method takes a task configuration object and 
+   * a database connection name, which defaults to doctrine. 
+   *
+   * Call the method like this:
+   * aTaskTools::signinAsTaskUser($this->createConfiguration($options['application'], $options['env']), $options['connection']);
+   */
+  
+  static public function signinAsTaskUser($configuration, $connectionName = 'doctrine')
+  {
+    // Create the context
+    sfContext::createInstance($configuration);
+    
+    // initialize the database connection
+    $databaseManager = new sfDatabaseManager($configuration);
+    $connection = $databaseManager->getDatabase($connectionName)->getConnection();
+    
+    // Fetch the task user, create if necessary
+    $user = self::getTaskUser();
+    
+    // Sign in as the task user
+    sfContext::getInstance()->getUser()->signin($user, false);
+  }
+  
+  static public function getTaskUser()
+  {    
+    $user = Doctrine::getTable('sfGuardUser')->findOneByUsername('ataskuser');
+    if (!$user)
+    {
+      $user = new sfGuardUser();
+      $user->setUsername('ataskuser');
+      // Set a good unique password just in case someone cluelessly sets the active flag.
+      // This further ensures that no one can ever log in with this account
+      $user->setPassword(aGuid::generate());
+      // Prevents normal login
+      $user->setIsActive(false);
+      $user->setIsSuperAdmin(true);
+      $user->save();
+    }
+    return $user;
+  }
+  
+  static public function setCliHost()
+  {
+    /**
+     * Ensures that links generated by link_to will use the hostname specified by
+     * app_cli_host rather than generating a bogus link starting with ./symfony
+     *
+     * By default we'll get the wrong hostname in links in emails
+     * (./symfony). We could override this by setting a context in
+     * factories.yml but that then requires us to maintain a completely
+     * separate environment just for the cli, duplicating all of the
+     * staging and production settings, which is bug-prone. The solution
+     * is to make the default behavior of link_to work for us by setting
+     * appropriate environment variables. Call this early, before
+     * a context has been created
+     *
+     * Call the method like this:
+     * aTaskTools::setCliHost();
+     */
+
+    $host = sfConfig::get('app_cli_host');
+    if (!$host)
+    {
+      throw new sfException('app_cli_host must be set to the hostname of this site so that valid links can be generated in emails');
+    }
+    $_SERVER['HTTP_HOST'] = $host;
+    // Otherwise we get ./symfony after the hostname
+    $_SERVER['SCRIPT_NAME'] = '';
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineTools.class.php	(revision 2883)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineTools.class.php	(revision 2883)
@@ -0,0 +1,73 @@
+<?php
+
+class aEngineTools
+{
+  // Poor man's multiple inheritance. This allows us to subclass an existing
+  // actions class in order to create an engine version of it. See aEngineActions
+  // for the call to add to your own preExecute method
+  
+  static public function preExecute($actions)
+  {
+    $request = $actions->getRequest();
+    // Figure out where we are all over again, because there seems to be no clean way
+    // to get the same controller-free URL that the routing engine gets. TODO:
+    // ask Fabien how we can do that.
+    $uri = urldecode($actions->getRequest()->getUri());
+    $rr = preg_quote(sfContext::getInstance()->getRequest()->getRelativeUrlRoot(), '/');
+    if (preg_match("/^(?:https?:\/\/[^\/]+)?$rr(?:\/[^\/]+\.php)?(.*)$/", $uri, $matches))
+    {
+      $uri = $matches[1];
+    }
+    else
+    {
+      throw new sfException("Unable to parse engine URL $uri");
+    }
+    // This will quickly fetch a result that was already cached when we 
+    // ran through the routing table (unless we hit the routing table cache,
+    // in which case we're looking it up for the first time, also OK)
+    $page = aPageTable::getMatchingEnginePage($uri, $remainder);
+    if (!$page)
+    {
+      throw new sfException('Attempt to access engine action without a page');
+    }
+    $page = aPageTable::retrieveByIdWithSlots($page->id);
+    // We want to do these things the same way executeShow would
+    aTools::validatePageAccess($actions, $page);
+    aTools::setPageEnvironment($actions, $page);
+    // Convenient access to the current page for the subclass
+    $actions->page = $page;
+    
+    // If your engine supports allowing the user to choose from several page types
+    // to distinguish different ways of using your engine, then you'll need to
+    // return the template name from your show and index actions (and perhaps
+    // others as appropriate). You can pull that information straight from
+    // $this->page->template, or you can take advantage of $this->pageTemplate which
+    // is ready to return as the result of an action (default has been changed
+    // to Success, other values have their first letter capitalized)
+    
+    $templates = aTools::getTemplates();
+    
+    // originalTemplate is what's in the template field of the page, except that
+    // nulls and empty strings from pre-1.5 Apostrophe have been converted to 'default'
+    // for consistency
+    $actions->originalTemplate = $page->template;
+    if (!strlen($actions->originalTemplate))
+    {
+      // Compatibility with 1.4 templates and reasonable Symfony expectations
+      $actions->originalTemplate = 'default';
+    }
+    
+    // pageTemplate is suitable to return from an action. 'default' becomes 'Success'
+    // (the Symfony standard for a "normal" template's suffix) and other values have
+    // their first letter capitalized
+    
+    if ($actions->originalTemplate === 'default')
+    {
+      $actions->pageTemplate = 'Success';
+    }
+    else
+    {
+      $actions->pageTemplate = ucfirst($actions->originalTemplate);
+    }
+  }  
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSubCrudActions.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSubCrudActions.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSubCrudActions.class.php	(revision 9)
@@ -0,0 +1,154 @@
+<?php
+
+// A typical Doctrine route collection CRUD action class framework, with the addition of support for subforms that
+// edit subsets of the object's fields via AJAX. Note that the name of your module determines the name of the
+// variable. TODO: a list of allowed subforms (although the existence of the class is
+// a good first pass at that).
+
+// TODO: think about whether $singular and $list are worth the trouble. It's nice to
+// refer to things as 'event' and 'events' rather than 'item' and 'items' in templates,
+// but this code would be more readable if we dumped the metavariables
+
+class aSubCrudActions extends sfActions
+{
+  
+  // These must be public to allow poor-man's mix-ins like aRosterTools to work. 
+  // You can set them explicitly in your subclass initialize() if the default guesses
+  // do not work for you (see initialize() below).
+  
+  // The module we're in (this is always set correctly by initialize() below)
+  public $module;
+  // The singular, lowercase name of the type we're editing; for model class Event this is typically 'event'.
+  // $this->$singular is often set by methods here and in aRosterTools, allowing $this->event to be referenced
+  // in subclass code for convenience. By default, the module name with the first character lowercased
+  public $singular;
+  // The plural name, by default event_list if singular is event
+  public $list;
+  // The model class name, by default ucfirst() of $singular
+  public $model;
+  
+  public function initialize($context, $moduleName, $actionName)
+  {
+    parent::initialize($context, $moduleName, $actionName);
+    $this->module = $moduleName;
+    if (!isset($this->singular))
+    {
+      // 5.2.x doesn't have lcfirst(), that arrives in 5.3.0
+      $this->singular = strtolower(substr($this->module, 0, 1)) . substr($this->module, 1);
+    }
+    $this->list = $this->singular . "_list";
+    if (!isset($this->model))
+    {
+      $this->model = ucfirst($this->singular);
+    }
+  }
+  
+  public function executeIndex(sfWebRequest $request)
+  {
+    $list = $this->list;
+    $this->$list = $this->getRoute()->getObjects();
+  }
+
+  public function executeShow(sfWebRequest $request)
+  {
+    $singular = $this->singular;
+    $this->$singular = $this->getRoute()->getObject();
+  }
+
+  public function executeEdit(sfWebRequest $request)
+  {
+    $this->getForm($request);
+    
+    return 'Ajax';
+  }
+  
+  public function executeUpdate(sfWebRequest $request)
+  {
+    $this->getForm($request);
+    
+    if ($this->processForm($request, $this->form))
+    {
+      return $this->renderPartial($this->module . '/' . $this->form->subtype);
+    }
+
+    $this->setTemplate('edit');
+
+    return 'Ajax';
+  }
+
+  public function executeDelete(sfWebRequest $request)
+  {
+    $request->checkCSRFProtection();
+
+    $this->getRoute()->getObject()->delete();
+
+    $this->redirect($this->module . '/index');
+  }
+  
+  public function executeNew(sfWebRequest $request)
+  {
+    $className = $this->model . 'CreateForm';
+    $this->form = new $className();
+  }
+
+  public function executeCreate(sfWebRequest $request)
+  {
+    $className = $this->model . 'CreateForm';
+    $this->form = new $className();
+
+    if ($this->processForm($request, $this->form))
+    {
+      $singular = $this->singular;
+      return $this->redirect($this->generateUrl($this->module . '_show', $this->$singular));
+    }
+
+    $this->setTemplate('new');
+  }
+
+  protected function processForm(sfWebRequest $request, sfForm $form)
+  {
+    $form->bind($request->getParameter($form->getName()));
+    if ($form->isValid())
+    {
+      $singular = $this->singular;
+      $this->$singular = $form->save();
+      // Without this, one-to-many relationships don't show the
+      // effects of the changes we just made when we render the partial
+      // for the static view
+      $this->$singular->refreshRelated();
+      return true;
+    }
+    
+    return false;
+  }
+  
+  protected function getForm($request)
+  {
+    if ($request->hasParameter('form'))
+    {
+      $class = aSubCrudTools::getFormClass($this->model, $request->getParameter('form'));
+      
+      // Custom form getters in the subform classes allow for dependency objection in a way 
+      // that permits a chunk to operate on a relation class (like EventUser) or an unrelated class (like sfGuardUserProfile)
+      // rather than directly on the object itself (like Event)
+      $object = $this->getRoute()->getObject();
+      
+      if (method_exists($class, 'getForm'))
+      { 
+        $this->form = call_user_func(array($class, 'getForm'), $object, $request);
+      }
+      else
+      {
+        $this->form = new $class($object);
+      }
+
+      if (method_exists($this->form, 'userCanEdit') && (!$this->form->userCanEdit()))
+      {
+        aSignin::signin();
+      }
+      
+      return;
+    }
+    throw new sfException('No form parameter.');
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aGlobalButton.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aGlobalButton.php	(revision 2920)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aGlobalButton.php	(revision 2920)
@@ -0,0 +1,64 @@
+<?php
+
+class aGlobalButton
+{
+  protected $label;
+  protected $link;
+  protected $cssClass;
+  protected $targetEnginePage;
+
+  // Use the name when reordering them in app.yml etc. The label will 
+  // be automatically i18n'd for you
+  
+  // 1.5: the $targetEngine parameter never made sense and was not used by globalTools. Your link
+  // implies a route which implies an engine. Only $targetEnginePage is in question
+  
+  public function __construct($name, $label, $link, $cssClass = '', $targetEnginePage = null)
+  {
+    $this->name = $name;
+    $this->label = $label;
+    $this->link = $link;
+    $this->cssClass = $cssClass;
+    $this->targetEnginePage = $targetEnginePage;
+    if ($this->targetEnginePage)
+    {
+      // 1.5: we've had this sane alternative to pushing and popping engines for a while so use it.
+      // It's also possible that you already did this for us and didn't bother passing $targetEnginePage,
+      // which is fine
+      
+      // Oops, I forgot to add $this->
+      
+      $this->link = aUrl::addParams($this->link, array('engine-slug' => $this->targetEnginePage));
+    }
+  }
+
+  public function getName()
+  {
+    return $this->name;
+  }
+  
+  public function getLabel()
+  {
+    return $this->label;
+  }
+  
+  public function getLink()
+  {
+    return $this->link;
+  }
+  
+  public function getCssClass()
+  {
+    return $this->cssClass;
+  }
+  
+  public function getTargetEnginePage()
+  {
+    return $this->targetEnginePage;
+  }
+
+  public function getTargetEngine()
+  {
+    return $this->targetEngine;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRules.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRules.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRules.class.php	(revision 9)
@@ -0,0 +1,88 @@
+<?php
+
+class aRules
+{
+  // Simple path-matching rules. They work like this:
+  // * matches anything
+  // % matches anything except / (great for directory components)
+  
+  // Call select with an array of rules and a string to be matched. Each 
+  // rule is an associative array with a key named 'rule' that contains
+  // the actual pattern. select returns the entire associative array
+  // for the first matching rule, or false if no rules match.
+
+  // This is very handy in CMS configuration files. Example
+  // (for a CMS with rooted paths):
+//      ## Home page gets homePage
+//      #
+//      # - rule: "/"
+//      #   template: homePage
+//      #
+//      ## All second level pages get secondPage
+//      # - rule: "/%/%"
+//      #   template: secondPage
+//      #
+//      ## All descendants (not just direct children) of /faq/ get faqPage.
+//      ## For direct children only, use % instead of *
+//      # - rule: "/home/faq/*"
+//      #   template: faqPage
+//      #
+//      # Everything else gets simplePage
+//      # - rule: "*"
+//      #   template: simplePage
+//      #
+
+  static public function select($rules, $s)
+  {
+    foreach ($rules as $rule) 
+    {
+      $ruleGlob = $rule['rule'];
+      $ruleReg = "/^" . str_replace(
+        array('%', '\*'),
+        array('[^\/]*', '.*'),
+        preg_quote($ruleGlob, '/')) . "$/";
+      if (preg_match($ruleReg, $s))
+      {
+#        sfContext::getInstance()->getLogger()->info("Matched $ruleReg to $s");
+        return $rule;
+      }
+    }
+    return false;
+  }
+
+  static public function test()
+  {
+    $tests = 
+      array("/this/is/a/path",
+        "/second/level",
+        "/somethingelse",
+        "/foo");
+
+    $rules = array(
+      array(
+        "rule" => "/foo",
+        "template" => "fooonly"),
+      array(
+        "rule" => "/%/%",
+        "template" => "secondlevel"),
+      array(
+        "rule" => "*",
+        "template" => "simple")
+      );
+
+    foreach ($tests as $test)
+    {
+      $rule = aRules::select($rules, $test);
+      if ($rule)
+      {
+        echo($test . ": " . $rule['template'] . " " . $rule['rule'] . "\n");
+      }
+      else
+      {
+        echo("No match\n");
+      }
+    }
+  }
+}
+
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRoute.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRoute.php	(revision 2982)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRoute.php	(revision 2982)
@@ -0,0 +1,55 @@
+<?php
+
+// Used by engine pages.
+
+class aRoute extends sfRoute 
+{
+  public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array())
+  {
+    parent::__construct($pattern, $defaults, $requirements, $options);  
+  }
+
+  /**
+   * Returns true if the URL matches this route, false otherwise.
+   *
+   * @param  string  $url     The URL
+   * @param  array   $context The context
+   *
+   * @return array   An array of parameters
+   */
+  public function matchesUrl($url, $context = array())
+  {
+    $url = aRouteTools::removePageFromUrl($this, $url);
+    return parent::matchesUrl($url, $context);
+  }
+
+  /**
+   * Generates a URL from the given parameters.
+   *
+   * @param  mixed   $params    The parameter values
+   * @param  array   $context   The context
+   * @param  Boolean $absolute  Whether to generate an absolute URL
+   *
+   * @return string The generated URL
+   */
+  public function generate($params, $context = array(), $absolute = false)
+  {
+    $slug = null;
+    $defaults = $this->getDefaults();
+    if (isset($params['engine-slug']))
+    {
+      $slug = $params['engine-slug'];
+      aRouteTools::pushTargetEngineSlug($slug, $defaults['module']);
+      unset($params['engine-slug']);
+    }
+    
+    // Note that you must pass false to parent::generate for the $absolute parameter
+    $result = aRouteTools::addPageToUrl($this, parent::generate($params, $context, false), $absolute);
+    if ($slug)
+    {
+      $engine = $defaults['module'];
+      aRouteTools::popTargetEnginePage($engine);
+    }
+    return $result;
+  } 
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTagAdminGenerator.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTagAdminGenerator.class.php	(revision 2317)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTagAdminGenerator.class.php	(revision 2317)
@@ -0,0 +1,17 @@
+<?php
+
+class aTagAdminGenerator extends sfDoctrineGenerator
+{
+  public function renderField($field)
+  {
+    if(preg_match('/^tag_/', $field->getName()))
+    {
+      $parts = preg_split('/^tag_/', $field->getName());
+      return sprintf('$%s->%sCount', $this->getSingularName(), $parts[1]);
+    }
+    else
+    {
+      return parent::renderField($field);
+    }
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/BaseaTools.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/BaseaTools.class.php	(revision 3145)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/BaseaTools.class.php	(revision 3145)
@@ -0,0 +1,927 @@
+<?php
+
+class BaseaTools
+{
+  // ALL static variables must go here
+  
+  // We need a separate flag so that even a non-CMS page can
+  // restore its state (i.e. set the page back to null)
+  static protected $global = false;
+  // We now allow fetching of slots from multiple pages, which can be
+  // normal pages or outside-of-navigation pages like 'global' that are used
+  // solely for this purpose. This allows efficient fetching of only slots that are
+  // relevant to your needs, rather than fetching all 'global' slots at once
+  static protected $globalCache = array();
+  static protected $currentPage = null;
+  static protected $pageStack = array();
+  static protected $globalButtons = false;
+  static protected $allowSlotEditing = true;
+  static protected $realUrl = null;
+  static public $jsCalls = array();
+  
+  // Must reset ALL static variables to their initial state
+  static public function listenToSimulateNewRequestEvent(sfEvent $event)
+  {
+    aTools::$global = false;
+    aTools::$globalCache = false;
+    aTools::$currentPage = null;
+    aTools::$pageStack = array();
+    aTools::$globalButtons = false;
+    aTools::$allowSlotEditing = true;
+    aTools::$realUrl = null;
+    aTools::$jsCalls = array();
+    aNavigation::simulateNewRequest();
+  }
+  
+  static public function cultureOrDefault($culture = false)
+  {
+    if ($culture)
+    {
+      return $culture;
+    }
+    return aTools::getUserCulture();
+  }
+  static public function getUserCulture($user = false)
+  {
+    if ($user == false)
+    {
+      $culture = false;
+      try
+      {
+        $context = sfContext::getInstance();
+      } catch (Exception $e)
+      {
+        // Not present in tasks
+        $context = false;
+      }
+      if ($context)
+      {
+        $user = sfContext::getInstance()->getUser();
+      }
+    }
+    if ($user)
+    {
+      $culture = $user->getCulture();
+    }
+    if (!$culture)
+    {
+      $culture = sfConfig::get('sf_default_culture', 'en');
+    }
+    return $culture;
+  }
+  static public function urlForPage($slug, $absolute = true)
+  {
+    // sfSimpleCMS found a nice workaround for this
+    // By using @a_page we can skip to a shorter URL form
+    // and not get tripped up by the default routing rule which could
+    // match first if we wrote a/show 
+    $routed_url = sfContext::getInstance()->getController()->genUrl('@a_page?slug=-PLACEHOLDER-', $absolute);
+    $routed_url = str_replace('-PLACEHOLDER-', $slug, $routed_url);
+    // We tend to get double slashes because slugs begin with slashes
+    // and the routing engine wants to helpfully add one too. Fix that,
+    // but don't break http://
+    $matches = array();
+    // This is good both for dev controllers and for absolute URLs
+    $routed_url = preg_replace('/([^:])\/\//', '$1/', $routed_url);
+    // For non-absolute URLs without a controller
+    if (!$absolute) 
+    {
+      $routed_url = preg_replace('/^\/\//', '/', $routed_url);
+    }
+    return $routed_url;
+  }
+  
+  static public function setCurrentPage($page)
+  {
+    aTools::$currentPage = $page;
+  }
+  
+  static public function getCurrentPage()
+  {
+    return aTools::$currentPage;
+  }
+
+  // Similar to getCurrentPage, but returns null if the current page is an admin page,
+  // and therefore not suitable for normal navigation like the breadcrumb and subnav
+  static public function getCurrentNonAdminPage()
+  {
+    $page = aTools::getCurrentPage();
+    return $page ? ($page->admin ? null : $page) : null;
+  }
+
+  /**
+   * We've fetched a page on our own using aPageTable::queryWithSlots and we want
+   * to make Apostrophe aware of it so that areas on the current page that live on
+   * that virtual page don't generate a superfluous second query
+   *
+   * @param array, Doctrine_Collection, aPage $pages
+   */
+  static public function cacheVirtualPages($pages)
+  {
+    if(get_class($pages) == 'Doctrine_Collection' || is_array($pages))
+    {
+      foreach($pages as $page)
+      {
+        aTools::$globalCache[$page['slug']] = $page;
+      }
+    }
+    else
+    {
+      aTools::$globalCache[$pages['slug']] = $pages;
+    }
+  }
+
+  static public function globalSetup($options)
+  {
+    if (isset($options['global']) && $options['global'])
+    {
+      if (!isset($options['slug']))
+      {
+        $options['slug'] = 'global';
+      }
+    }
+    if (isset($options['slug']))
+    {
+      // Note that we push onto the stack even if the page specified is the same page
+      // we're looking at. This doesn't hurt because of caching, and it allows us
+      // to keep the stack count properly
+      $slug = $options['slug'];
+      aTools::$pageStack[] = aTools::getCurrentPage();
+      // Caching the global page speeds up pages with two or more global slots
+      if (isset(aTools::$globalCache[$slug]))
+      {
+        $global = aTools::$globalCache[$slug];
+      }
+      else
+      {        
+        $global = aPageTable::retrieveBySlugWithSlots($slug);
+        if (!$global)
+        {
+          $global = new aPage();
+          $global->slug = $slug;
+          $global->save();
+        }
+        aTools::$globalCache[$slug] = $global;
+      }
+      aTools::setCurrentPage($global);
+      aTools::$global = true;
+    }
+  }
+
+  static public function globalShutdown()
+  {
+    if (aTools::$global)
+    {
+      aTools::setCurrentPage(array_pop(aTools::$pageStack));
+      aTools::$global = (count(aTools::$pageStack));
+    }
+  }
+
+  static public function getSlotOptionsGroup($groupName)
+  {
+    $optionGroups = sfConfig::get('app_a_slot_option_groups', 
+      array());
+    if (isset($optionGroups[$groupName]))
+    {
+      return $optionGroups[$groupName];
+    }
+    throw new sfException("Option group $groupName is not defined in app.yml");
+  }
+
+  // Oops: we can't cache this list because it's different for various areas on the same page.
+  
+  static public function getSlotTypesInfo($options)
+  {
+    $instance = sfContext::getInstance();
+    $slotTypes = array_merge(
+      array(
+         'aText' => 'Plain Text',
+         'aRichText' => 'Rich Text',
+         'aFeed' => 'RSS Feed',
+         'aSlideshow' => 'Photo Slideshow',
+         'aSmartSlideshow' => 'Smart Slideshow',
+         'aButton' => 'Button',
+         'aAudio' => 'Audio',
+         'aVideo' => 'Video',
+         'aFile' => 'File',
+         'aRawHTML' => 'Raw HTML'),
+      sfConfig::get('app_a_slot_types', array()));
+    if (isset($options['allowed_types']))
+    {
+      $newSlotTypes = array();
+      foreach($options['allowed_types'] as $type)
+      {
+        if (isset($slotTypes[$type]))
+        {
+          $newSlotTypes[$type] = $slotTypes[$type];
+        }
+      }
+      $slotTypes = $newSlotTypes;
+    }
+    $info = array();
+    
+    foreach ($slotTypes as $type => $label)
+    {
+      $info[$type]['label'] = $label;
+      // We COULD cache this. Would it pay to do so?
+      $info[$type]['class'] = strtolower(preg_replace('/^a(\w)/', 'a-$1', $type));
+    }
+    return $info;
+  }
+  
+  // Includes classes for buttons for adding each slot type
+  static public function getSlotTypeOptionsAndClasses($options)
+  {
+    
+  }
+  
+  static public function getOption($array, $name, $default)
+  {
+    if (isset($array[$name]))
+    {
+      return $array[$name];
+    }
+    return $default;
+  }
+  static public function getRealPage()
+  {
+    if (count(aTools::$pageStack))
+    {
+      $page = aTools::$pageStack[0];
+      if ($page)
+      {
+        return $page;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    elseif (aTools::$currentPage)
+    {
+      return aTools::$currentPage;
+    }
+    else
+    {
+      return false;
+    }
+  }
+  // Fetch options array saved in session
+  static public function getAreaOptions($pageid, $name)
+  {
+    $lookingFor = "area-options-$pageid-$name";
+    $options = array();
+    $user = sfContext::getInstance()->getUser();
+    if ($user->hasAttribute($lookingFor, 'apostrophe'))
+    {
+      $options = $user->getAttribute(
+        $lookingFor, false, 'apostrophe');
+    }
+    return $options;
+  }
+  
+  // Get template choices in the new format, then provide bc with the old format
+  // (one level with no engines specified), and also add entries for any engines
+  // listed in the old way that don't have templates specified in the new way
+  
+  static public function getTemplates()
+  {
+    if (sfConfig::get('app_a_get_templates_method'))
+    {
+      $method = sfConfig::get('app_a_get_templates_method');
+
+      return call_user_func($method);
+    }
+    $templates = sfConfig::get('app_a_templates', array(
+      'a' => array(
+        'default' => 'Default Page',
+        'home' => 'Home Page')));
+    // Provide bc 
+    $newTemplates = $templates;
+    foreach ($templates as $key => $value)
+    {
+      if (!is_array($value))
+      {
+        $newTemplates['a'][$key] = $value;
+        unset($newTemplates[$key]);
+      }
+    }
+    $templates = $newTemplates;
+    $engines = aTools::getEngines();
+    foreach ($engines as $name => $label)
+    {
+      if (!strlen($name))
+      {
+        // Ignore the "template-based" engine option
+        continue;
+      }
+      if (!isset($templates[$name]))
+      {
+        $templates[$name] = array('default' => $label);
+      }
+    }
+    return $templates;
+  }
+  
+  // Flat name => label array for use in select elements
+  
+  static public function getTemplateChoices()
+  {
+    $templates = aTools::getTemplates();
+    $choices = array();
+    foreach ($templates as $engine => $etemplates)
+    {
+      foreach ($etemplates as $name => $label)
+      {
+        $choices["$engine:$name"] = $label;
+      }
+    }
+    return $choices;
+  }
+  
+  // Used to provide bc with the old app_a_engines way of listing engine choices
+  
+  static public function getEngines()
+  {
+    if (sfConfig::get('app_a_get_engines_method'))
+    {
+      $method = sfConfig::get('app_a_get_engines_method');
+
+      return call_user_func($method);
+    }
+    return sfConfig::get('app_a_engines', array(
+      '' => 'Template-Based'));
+  }
+  
+  // Fetch an internationalized option from app.yml. Example:
+  // all:
+  //   a:
+  
+  static public function getOptionI18n($option, $default = false, $culture = false)
+  {
+    $culture = aTools::cultureOrDefault($culture);
+    $values = sfConfig::get("app_a_$option", array());
+    if (!is_array($values))
+    {
+      // Convenience for single-language sites
+      return $values;
+    }
+    if (isset($values[$culture]))
+    {
+      return $values[$culture];  
+    } 
+    return $default; 
+  }
+  
+  static public function getGlobalButtonsInternal(sfEvent $event)
+  {
+    // If we needed a context object we could get it from $event->getSubject(),
+    // but this is a simple static thing
+    
+    // Add the users button only if the user has the admin credential.
+    // This is typically only given to admins and superadmins.
+    $user = sfContext::getInstance()->getUser();
+    if ($user->hasCredential('admin'))
+    {
+      $extraAdminButtons = sfConfig::get('app_a_extra_admin_buttons', 
+        array('users' => array('label' => 'Users', 'action' => 'aUserAdmin/index', 'class' => 'a-users'),
+          'categories' => array('label' => 'Categories', 'action' => 'aCategoryAdmin/index', 'class' => 'a-categories'),
+          'tags' => array('label' => 'Tags', 'action' => 'aTagAdmin/index', 'class' => 'a-tags'),
+          'reorganize' => array('label' => 'Reorganize', 'action' => 'a/reorganize', 'class' => 'a-reorganize')        
+        ));
+
+      if (is_array($extraAdminButtons))
+      {
+        foreach ($extraAdminButtons as $name => $data)
+        {
+          aTools::addGlobalButtons(array(new aGlobalButton(
+            $name, $data['label'], $data['action'], isset($data['class']) ? $data['class'] : '')));
+        }
+      }
+    }
+  }
+  
+  // To be called only in response to a a.getGlobalButtons event 
+  static public function addGlobalButtons($array)
+  {
+    foreach ($array as $button)
+    {
+      aTools::$globalButtons[$button->getName()] = $button;
+    }
+  }
+  
+  // Returns global buttons as a flat array, either in alpha order or, if app_a_global_button_order is
+  // specified, in that order. This is used to implement the default behavior. However see also
+  // aTools::getGlobalButtonsByName() which is much nicer if you want to aggressively customize
+  // the admin bar
+  
+  static public function getGlobalButtons()
+  {
+    $buttonsByName = aTools::getGlobalButtonsByName();
+    $buttonsOrder = sfConfig::get('app_a_global_button_order', false);
+    if ($buttonsOrder === false)
+    {
+      ksort($buttonsByName);
+      $orderedButtons = array_values($buttonsByName);
+    }
+    else
+    {
+      $orderedButtons = array();
+      foreach ($buttonsOrder as $name)
+      {
+        if (isset($buttonsByName[$name]))
+        {
+          $orderedButtons[] = $buttonsByName[$name];
+        }
+      }
+    }
+    
+    return $orderedButtons;
+  }
+  
+  // Returns global buttons as an associative array by button name.
+  // Ignores app_a_global_button_order. For use by those who prefer to
+  // override the _globalTools partial. Note that you will NOT get the
+  // same buttons for every user! An admin has more buttons than a
+  // mere editor and so on. Use isset()
+
+  static public function getGlobalButtonsByName()
+  {
+    if (aTools::$globalButtons === false)
+    {
+      aTools::$globalButtons = array();
+      // We could pass parameters here but it's a simple static thing in this case 
+      // so the recipients just call back to addGlobalButtons
+      sfContext::getInstance()->getEventDispatcher()->notify(new sfEvent(null, 'a.getGlobalButtons', array()));
+    }
+    return aTools::$globalButtons;
+  }
+  
+  static public function globalToolsPrivilege()
+  {
+    // if you can edit the page, there are tools for you in the apostrophe
+    if (aTools::getCurrentPage() && aTools::getCurrentPage()->userHasPrivilege('edit'))
+    {
+      return true;
+    }
+    // if you are the site admin, there are ALWAYS tools for you in the apostrophe
+    $user = sfContext::getInstance()->getUser();
+    return $user->hasCredential('cms_admin');
+  }
+  
+  // These methods allow slot editing to be turned off even for people with
+  // full and appropriate privileges.
+  
+  // Most of the time being able to edit a global slot on a non-CMS page is a
+  // good thing, especially if that's the only place the global slot appears.
+  // But sometimes, as in the case where you're editing other types of data,
+  // it's just a source of confusion to have those buttons displayed. 
+  
+  // (Suppressing editing of slots on normal CMS pages is of course a bad idea,
+  // because how else would you ever edit them?)
+  
+  static public function setAllowSlotEditing($value)
+  {
+    aTools::$allowSlotEditing = $value;
+  }
+  static public function getAllowSlotEditing()
+  {
+    return aTools::$allowSlotEditing;
+  }
+  
+  // Kick the user out to appropriate places if they don't have the proper 
+  // privileges to be here. a::executeShow and aEngineActions::preExecute
+  // both use this 
+  
+  static public function validatePageAccess(sfAction $action, $page)
+  {
+    $action->forward404Unless($page);
+    if (!$page->userHasPrivilege('view'))
+    {
+      // forward rather than login because referrers don't always
+      // work. Hopefully the login action will capture the original
+      // URI to bring the user back here afterwards.
+
+      if ($action->getUser()->isAuthenticated())
+      {
+        return $action->forward(sfConfig::get('sf_secure_module'), sfConfig::get('sf_secure_action'));
+      }
+      else
+      {
+        return $action->forward(sfConfig::get('sf_login_module'), sfConfig::get('sf_login_action'));
+
+      }
+    }
+    if ($page->archived && (!$page->userHasPrivilege('edit')))
+    {
+      $action->forward404();
+    }    
+  }
+
+  // Establish the page title, set the layout, and add the javascripts that are
+  // necessary to manage pages. a::executeShow and aEngineActions::preExecute
+  // both use this. TODO: is this redundant now that aHelper does it?
+  
+  static public function setPageEnvironment(sfAction $action, aPage $page)
+  {
+    // Title is pre-escaped as valid HTML
+    $prefix = aTools::getOptionI18n('title_prefix');
+    $suffix = aTools::getOptionI18n('title_suffix');
+    $action->getResponse()->setTitle($prefix . $page->getTitle() . $suffix, false);
+    // Necessary to allow the use of
+    // aTools::getCurrentPage() in the layout.
+    // In Symfony 1.1+, you can't see $action->page from
+    // the layout.
+    aTools::setCurrentPage($page);
+    // Borrowed from sfSimpleCMS
+    if(sfConfig::get('app_a_use_bundled_layout', true))
+    {
+      $action->setLayout(sfContext::getInstance()->getConfiguration()->getTemplateDir('a', 'layout.php').'/layout');
+    }
+
+    // Loading the a helper at this point guarantees not only
+    // helper functions but also necessary JavaScript and CSS
+    sfContext::getInstance()->getConfiguration()->loadHelpers('a');     
+  }
+  
+  static public function pageIsDescendantOfInfo($page, $info)
+  {
+    return ($page->lft > $info['lft']) && ($page->rgt < $info['rgt']);
+  }
+  
+  // Same rules found in aPage::userHasPrivilege(), but without checking for
+  // a particular page, so we return true even for users who are just *potential* editors
+  // when granted privileges at an appropriate point in the page tree. This is useful for
+  // deciding whether the tabs control should show archived pages or not. (Showing those to
+  // a few editors who can't edit them is not a major problem, and checking the privs on
+  // each and every one is an unacceptable performance hit) 
+  
+  static public function isPotentialEditor($user = false)
+  {
+    if ($user === false)
+    {
+      $user = sfContext::getInstance()->getUser();
+    }
+    // Rule 1: admin can do anything
+    // Work around a bug in some releases of sfDoctrineGuard: users sometimes
+    // still have credentials even though they are not logged in
+    if ($user->isAuthenticated() && $user->hasCredential('cms_admin'))
+    {
+      return true;
+    }
+
+    // The editor permission, which (like the editor group) makes you a candidate to edit
+    // if actually granted that privilege somewhere in the tree (via membership in a group
+    // that has the editor permission), is generally received from a group. In older installs the 
+    // editor group itself won't have it, so we still check by other means (see below). 
+    if ($user->isAuthenticated() && $user->hasCredential(sfConfig::get('app_a_group_editor_permission', 'editor')))
+    {
+      return true;
+    }
+    
+    $sufficientCredentials = sfConfig::get("app_a_edit_sufficient_credentials", false);
+    $sufficientGroup = sfConfig::get("app_a_edit_sufficient_group", false);
+    $candidateGroup = sfConfig::get("app_a_edit_candidate_group", false);
+    // By default users must log in to do anything except view
+    $loginRequired = sfConfig::get("app_a_edit_login_required", true);
+    
+    if ($loginRequired)
+    {
+      if (!$user->isAuthenticated())
+      {
+        return false;
+      }
+      // Rule 3: if there are no sufficient credentials and there is no
+      // required or sufficient group, then login alone is sufficient. Common 
+      // on sites with one admin
+      if (($sufficientCredentials === false) && ($candidateGroup === false) && ($sufficientGroup === false))
+      {
+        // Logging in is the only requirement
+        return true; 
+      }
+      // Rule 4: if the user has sufficient credentials... that's sufficient!
+      // Many sites will want to simply say 'editors can edit everything' etc
+      if ($sufficientCredentials && 
+        ($user->hasCredential($sufficientCredentials)))
+      {
+        
+        return true;
+      }
+      if ($sufficientGroup && 
+        ($user->hasGroup($sufficientGroup)))
+      {
+        return true;
+      }
+
+      // Rule 5: if there is a candidate group, make sure the user is a member
+      if ($candidateGroup && 
+        (!$user->hasGroup($candidateGroup)))
+      {
+        return false;
+      }
+      return true;
+    }
+    else
+    {
+      // No login required
+      return true;
+    }      
+  }
+  
+  static public function getVariantsForSlotType($type, $options = array())
+  {
+    // 1. By default, all variants of the slot are allowed.
+    // 2. If app_a_allowed_variants is set and a specific list of allowed variants
+    // is provided for this slot type, those variants are allowed.
+    // 3. If app_a_allowed_variants is set and a specific list is not present for this slot type,
+    // no variants are allowed for this slot type.
+    // 4. An allowed_variants option in an a_slot or a_area call overrides all of the above.
+    
+    // This makes it easy to define lots of variants, then disable them by default for 
+    // templates that don't explicitly enable them. This is useful because variants are often
+    // specific to the dimensions or other particulars of a particular template
+
+    if (sfConfig::has('app_a_allowed_slot_variants'))
+    {
+      $allowedVariantsAll = sfConfig::get('app_a_allowed_slot_variants', array());
+      $allowedVariants = array();
+      if (isset($allowedVariantsAll[$type]))
+      {
+        $allowedVariants = $allowedVariantsAll[$type];
+      }
+    }
+    if (isset($options['allowed_variants']))
+    {
+      $allowedVariants = $options['allowed_variants'];
+    }
+    
+    $variants = sfConfig::get('app_a_slot_variants');
+    if (!is_array($variants))
+    {
+      return array();
+    }
+    if (!isset($variants[$type]))
+    {
+      return array();
+    }
+    $variants = $variants[$type];
+    if (isset($allowedVariants))
+    {
+			// Don't call array_flip since we seem to have decorated values coming in ):
+			// (TODO: find that and make it stop)
+			$allowed = array();
+			foreach ($allowedVariants as $name)
+			{
+				$allowed[$name] = true;
+			}
+      $keep = array();
+      foreach ($variants as $name => $value)
+      {
+        if (isset($allowed[$name]))
+        {
+          $keep[$name] = $value;
+        }
+      }
+      $variants = $keep;
+    }
+    return $variants;
+  }
+  
+  static protected function i18nDummy()
+  {
+    __('Reorganize', null, 'apostrophe');
+    __('Users', null, 'apostrophe');
+    __('Plain Text', null, 'apostrophe');
+    __('Rich Text', null, 'apostrophe');
+    __('RSS Feed', null, 'apostrophe');
+    __('Image', null, 'apostrophe');
+    __('Slideshow', null, 'apostrophe');
+    __('Button', null, 'apostrophe');
+    __('Video', null, 'apostrophe');
+    __('PDF', null, 'apostrophe');
+    __('Raw HTML', null, 'apostrophe');    
+    __('Template-Based', null, 'apostrophe');
+    __('Users', null, 'apostrophe');
+    __('Reorganize', null, 'apostrophe');
+  }
+  
+  static public function getRealUrl()
+  {
+    if (isset(aTools::$realUrl))
+    {
+      return aTools::$realUrl;
+    }
+    return sfContext::getInstance()->getRequest()->getUri();
+  }
+  
+  static public function setRealUrl($url)
+  {
+    aTools::$realUrl = $url;
+  }
+  
+  // Returns a regexp fragment that matches a valid slug in a UTF8-aware way.
+  // Does not reject slugs with consecutive dashes or slashes. DOES accept the %
+  // sign because URLs generated by url_for arrive with the UTF8 characters
+  // %-encoded. You should anchor it with ^ and $ if your goal is to match one slug as the whole string
+  static public function getSlugRegexpFragment($allowSlashes = false)
+  {
+    // Looks like the 'u' modifier is purely for allowing UTF8 in the pattern *itself*. So we
+    // shouldn't need it to achieve 
+    if (function_exists('mb_strtolower'))
+    {
+      // UTF-8 capable replacement for \W. Works fine for English and also for Greek, etc.
+      // ALlow % as well to work with preescaped UTF8, which is common in URLs
+      $alnum = '\p{L}\p{N}_%';
+      $modifier = '';
+    }
+    else
+    {
+      $alnum = '\w';
+      $modifier = '';
+    }
+    if ($allowSlashes)
+    {
+      $alnum .= '\/';
+    }
+    $regexp = "[$alnum\-]+";
+    return $regexp;
+  }
+  
+  // UTF-8 where available. If your UTF-8 gets munged make sure your PHP has the
+  // mbstring extension. allowSlashes will allow / but will reduce duplicate / and
+  // remove any / at the end. Everything that isn't a letter or a number 
+  // (or a slash, when allowed) is converted to a -. Consecutive -'s are reduced and leading and
+  // trailing -'s are removed
+  
+  // $betweenWords must not contain characters that have special meaning in a regexp.
+  // Usually it is - (the default) or ' '
+  
+  static public function slugify($path, $allowSlashes = false, $allowUnderscores = true, $betweenWords = '-')
+  {
+    // This is the inverse of the method above
+    if (function_exists('mb_strtolower'))
+    {
+      // UTF-8 capable replacement for \W. Works fine for English and also for Greek, etc.
+      $alnum = '\p{L}\p{N}' . ($allowUnderscores ? '_' : '');
+      $modifier = 'u';
+    }
+    else
+    {
+      $alnum = $allowUnderscores ? '\w' : '[A-Za-z0-9]';
+      $modifier = '';
+    }
+    if ($allowSlashes)
+    {
+      $alnum .= '\/';
+    }
+    // Removing - here expands flexibility and shouldn't hurt because it's the replacement anyway
+    $regexp = "/[^$alnum]+/$modifier";
+    $path = aString::strtolower(preg_replace($regexp, $betweenWords, $path));  
+    if ($allowSlashes)
+    {
+      // No multiple consecutive /
+      $path = preg_replace("/\/+/$modifier", "/", $path);
+      // No trailing / unless it's the homepage
+      if ($path !== '/')
+      {
+        $path = preg_replace("/\/$/$modifier", '', $path);
+      }
+    }
+    // No consecutive dashes
+    $path = preg_replace("/$betweenWords+/$modifier", $betweenWords, $path);
+    // Leading and trailing dashes are silly. This has the effect of trim()
+    // among other sensible things
+    $path = preg_replace("/^-*(.*?)-*$/$modifier", '$1', $path);     
+    return $path;
+  }
+
+  // MUST BE KEPT UP TO DATE
+  static protected $cssByName = array(
+    'reset' => '/apostrophePlugin/css/a-reset.css',
+    'forms' => '/apostrophePlugin/css/a-forms.css',
+    'buttons' => '/apostrophePlugin/css/a-buttons.css',
+    'components' => '/apostrophePlugin/css/a-components.css',
+    'area-slots' => '/apostrophePlugin/css/a-area-slots.css',
+    'engines' => '/apostrophePlugin/css/a-engines.css',
+    'admin' => '/apostrophePlugin/css/a-admin.css',
+    'colors' => '/apostrophePlugin/css/a-colors.css',
+    'utility' => '/apostrophePlugin/css/a-utility.css',
+    'jquery-ui' => '/apostrophePlugin/css/ui-apostrophe/jquery-ui.css'
+  );
+
+  static public function addStylesheetsIfDesired()
+  {
+    if (!sfConfig::get('app_a_use_bundled_stylesheets', true))
+    {
+      return;
+    }
+    $response = sfContext::getInstance()->getResponse();
+    $preferences = sfConfig::get('app_a_use_bundled_stylesheets', array());
+    foreach (aTools::$cssByName as $stylesheet => $default)
+    {
+      $good = true;
+      if (isset($preferences[$stylesheet]))
+      {
+        $good = $preferences[$stylesheet];
+      }
+      if ($good)
+      {
+        if ($good === true)
+        {
+          $response->addStylesheet($default);
+        }
+        else
+        {
+          $response->addStylesheet($good);
+        }
+      }
+    }
+  }
+  
+  // MUST BE KEPT UP TO DATE
+  static protected $jsByName = array(
+    'jquery' => '/apostrophePlugin/js/jquery-1.4.3.min.js',
+    'main' => '/apostrophePlugin/js/a.js',
+    'controls' => '/apostrophePlugin/js/aControls.js',
+    'json2' => '/apostrophePlugin/js/json2.js',
+    'jquery-autogrow' => '/apostrophePlugin/js/plugins/jquery.simpleautogrow.js',
+    'jquery-hover-intent' => '/apostrophePlugin/js/plugins/jquery.hoverIntent.js',
+    'jquery-ui' => '/apostrophePlugin/js/plugins/jquery-ui-1.8.7.custom.min.js',
+    'tagahead' => '/sfDoctrineActAsTaggablePlugin/js/pkTagahead.js'
+  );
+
+  static public function addJavascriptsIfDesired()
+  {
+    if (!sfConfig::get('app_a_use_bundled_javascripts', true))
+    {
+      return;
+    }
+    $response = sfContext::getInstance()->getResponse();
+    $preferences = sfConfig::get('app_a_use_bundled_javascripts', array());
+    foreach (aTools::$jsByName as $javascript => $default)
+    {
+      $good = true;
+      if (isset($preferences[$javascript]))
+      {
+        $good = $preferences[$javascript];
+      }
+      if ($good)
+      {
+        if ($good === true)
+        {
+          $response->addJavascript($default);
+        }
+        else
+        {
+          // Override with a new path
+          $response->addJavascript($good);
+        }
+      }
+      else
+      {
+        // They don't want it at all
+      }
+    }
+  }
+  
+  static protected $locks = array();
+
+  // Lock names must be \w+ 
+  static public function lock($name)
+  {
+    $dir = aFiles::getWritableDataFolder(array('a', 'locks'));
+    if (!preg_match('/^\w+$/', $name))
+    {
+      throw new sfException("Lock name is empty or contains non-word characters");
+    }
+    $file = "$dir/$name.lck";
+    while (true)
+    {
+      $fp = fopen($file, 'a');
+      if ($fp)
+      {
+        if (flock($fp, LOCK_EX))
+        {
+          break;
+        }
+      }
+      sleep(1);
+    } 
+    flock($fp, LOCK_EX);
+    aTools::$locks[] = $fp;
+  }
+  
+  static public function unlock()
+  {
+    if (count(aTools::$locks))
+    {
+      $fp = array_pop(aTools::$locks);
+      fclose($fp);
+    }
+    else
+    {
+      // It's OK to call with no lock, this greatly simplifies methods like flunkUnless()
+      // If you are using multiple names you are responsible for making sure you unlock consistently. 
+    }
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaTools.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaTools.php	(revision 2281)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaTools.php	(revision 2281)
@@ -0,0 +1,7 @@
+<?php
+
+// I can be overridden at project level
+
+class aMediaTools extends BaseaMediaTools
+{
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aArrayPager.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aArrayPager.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aArrayPager.class.php	(revision 9)
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @author Scott Meves
+ * http://snippets.symfony-project.org/snippet/177
+ */
+class aArrayPager extends sfPager
+{
+  protected $resultsArray = null;
+ 
+  public function __construct($class = null, $maxPerPage = 10)
+  {
+    parent::__construct($class, $maxPerPage);
+  }
+ 
+  public function init()
+  {
+    $this->setNbResults(count($this->resultsArray));
+ 
+    if (($this->getPage() == 0 || $this->getMaxPerPage() == 0))
+    {
+     $this->setLastPage(0);
+    }
+    else
+    {
+     $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage()));
+    }
+  }
+ 
+  public function setResultArray($array)
+  {
+    $this->resultsArray = $array;
+  }
+ 
+  public function getResultArray()
+  {
+    return $this->resultsArray;
+  }
+ 
+  public function retrieveObject($offset) {
+    return $this->resultsArray[$offset];
+  }
+ 
+  public function getResults()
+  {
+    return array_slice($this->resultsArray, ($this->getPage() - 1) * $this->getMaxPerPage(), $this->maxPerPage);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTrace.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTrace.class.php	(revision 1343)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aTrace.class.php	(revision 1343)
@@ -0,0 +1,97 @@
+<?php
+
+// A simple inline backtrace that won't crash with an out of
+// memory error if the parameters are big objects (as
+// debug_print_backtrace sometimes does), clutter the
+// browser with huge var_dumps, etc. The trace can be
+// collapsed and expanded with a mouse click. Just call
+// aTrace::printTrace() from anywhere. Or call
+// aTrace::trace() to get the trace HTML code as
+// a string, which may be useful in contexts where you're building
+// something up in a helper.
+// 
+// Plaintext versions are also available and quite useful, call
+// aTrace::traceText() for a plaintext trace as a string and
+// aTrace::printTraceText() to echo it as text. To log it
+// to the Symfony log with 'info' priority, call
+// aTrace::traceLog(). You can then grep for method names. Super useful.
+//
+// I don't use a javascript toolkit here because this ought to work in sites
+// built with any of them. 
+//
+// 2009-08-10: migrated from apostrophePlugin to reduce plugin count bloat.
+// Shortened class name for convenience. Added traceLog().
+//
+// 2009-12-03: loads helpers the Symfony 1.3+ way.
+//
+// tom@punkave.com
+
+class aTrace
+{
+  static $traceId = 0;
+  static public function trace($ignoreCount = 1)
+  {
+    $result = "";
+    self::$traceId++;
+    $traceId = "aTrace" . self::$traceId;
+    $traceIdShow = $traceId . "Show";
+    $traceIdHide = $traceId . "Hide";
+    sfContext::getInstance()->getConfiguration()->loadHelpers(array('Tag', 'JavascriptBase'));
+    $result .= "<div class='aTrace'>Trace " . 
+      link_to_function("&gt;&gt;&gt;", 
+        "document.getElementById('$traceId').style.display = 'block'; " .
+        "document.getElementById('$traceIdShow').style.display = 'none'; " .
+        "document.getElementById('$traceIdHide').style.display = 'inline'",
+        array("id" => $traceIdShow)) .
+      link_to_function("&lt;&lt;&lt;", 
+        "document.getElementById('$traceId').style.display = 'none'; " .
+        "document.getElementById('$traceIdHide').style.display = 'none'; " .
+        "document.getElementById('$traceIdShow').style.display = 'inline'",
+        array("id" => $traceIdHide, "style" => 'display: none'));
+    $result .= "</div>";
+    $result .= "<pre id='$traceId' style='display: none'>\n";
+    $result .= self::traceText($ignoreCount + 1);
+    $result .= "</pre>\n";
+    return $result;
+  }
+  static public function printTrace()
+  {
+    echo(self::trace(2));
+  }
+  // Now you can pass in a trace from a getTrace() call on an exception object
+  static public function traceText($ignoreCount = 1, $trace = null)
+  {
+    if ($trace === null)
+    {
+      $trace = debug_backtrace();    
+    }
+    $count = 0;
+    $result = "";
+    $lastLine = 'NONE';
+    foreach ($trace as $element)    
+    {
+      $count++;
+      if ($count > $ignoreCount)
+      {
+        $result .= "Class: " . (isset($element['class']) ? $element['class'] : 'NONE') . " function: " . $element['function'] . " line: " . $lastLine . " File: " . (isset($element['file']) ? $element['file'] : 'NONE') . "\n";
+      }
+      if (isset($element['line']))
+      {
+        $lastLine = $element['line'];
+      }
+      else
+      {
+        $lastLine = 'NONE';
+      }
+    }
+    return $result;
+  }
+  static public function printTraceText()
+  {
+    echo(self::traceText(2));
+  }  
+  static public function traceLog()
+  {
+    sfContext::getInstance()->getLogger()->info(self::traceText());
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSql.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSql.class.php	(revision 3067)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aSql.class.php	(revision 3067)
@@ -0,0 +1,204 @@
+<?php
+
+class aSql
+{
+  protected $pdo;
+
+  public function  __construct($pdo)
+  {
+    $this->pdo = $pdo;
+  }
+
+  protected function getPDO()
+  {
+    return $this->pdo;
+  }
+
+  public function deleteNonAdminPages()
+  {
+    $sql = 'DELETE FROM a_page where admin IS FALSE AND slug <> :g';
+    $this->query($sql, array('g' => 'global'));
+  }
+
+  public function query($s, $params = array())
+  {
+    $pdo = $this->getPDO();
+    $nparams = array();
+    // I like to use this with toArray() while not always setting everything,
+    // so I tolerate extra stuff. Also I don't like having to put a : in front
+    // of everything
+    foreach ($params as $key => $value)
+    {
+      if (strpos($s, ":$key") !== false)
+      {
+        $nparams[":$key"] = $value;
+      }
+    }
+    $statement = $pdo->prepare($s);
+    try
+    {
+      $statement->execute($nparams);
+    }
+    catch (Exception $e)
+    {
+      echo($e);
+      echo("Statement: $s\n");
+      echo("Parameters:\n");
+      var_dump($params);
+      exit(1);
+    }
+    $result = true;
+    try
+    {
+      $result = $statement->fetchAll();
+    } catch (Exception $e)
+    {
+      // Oh no, we tried to fetchAll on a DELETE statement, everybody panic!
+      // Seriously PDO, you need to relax
+    }
+    return $result;
+  }
+
+
+  /**
+   * Inserts a page from info array and updates the array with new fields
+   * @param Array $info
+   * @param string $title
+   * @param int $parentId
+   * @return array
+   */
+  public function insertPage(&$info, $title, $parentId)
+  {
+    if (isset($info['id']))
+    {
+      throw new sfException("fastSavePage doesn't know how to handle an existing page");
+    }
+    // This page needs to be the last child of its parent
+    if(is_null($parentId))
+    {
+      list($lft, $rgt, $level) = array(0,1,-1);
+    }
+    else
+    {
+      $result = $this->query('SELECT lft, rgt, level FROM a_page WHERE id = :id', array('id' => $parentId));
+      list($lft, $rgt, $level) = array($result[0]['lft'], $result[0]['rgt'], $result[0]['level']);
+    }
+    $this->query('UPDATE a_page SET rgt = rgt + 2 WHERE lft <= :lft AND rgt >= :rgt', array('lft' => $lft, 'rgt' => $rgt));
+    $info['lft'] = $rgt;
+    $info['rgt'] = $rgt + 1;
+    $info['level'] = $level + 1;
+    if (!isset($info['view_is_secure']))
+    {
+      $info['view_is_secure'] = false;
+    }
+    if (!isset($info['archived']))
+    {
+      $info['archived'] = false;
+      $info['published_at'] = 'NOW()';
+    }
+    if (!isset($info['engine']))
+    {
+      $info['engine'] = null;
+    }
+    if(!isset($info['admin']))
+    {
+      $info['admin'] = false;
+    }
+    if(!isset($info['template']))
+    {
+      $info['template'] = 'default';
+    }
+    $this->query('INSERT INTO a_page (created_at, updated_at, slug, template, view_is_secure, archived, published_at, lft, rgt, level, engine, admin) VALUES (NOW(), NOW(), :slug, :template, :view_is_secure, :archived, :published_at, :lft, :rgt, :level, :engine, :admin)', $info);
+    $info['id'] = $this->lastInsertId();
+
+    $this->insertArea($info['id'], 'title', array(array('type' => 'aText', 'value' => htmlentities($title))));
+
+    return $info;    
+  }
+
+
+  /**
+   * Inserts an area with its slots
+   * @param int $aPageId
+   * @param string $name
+   * @param Array $slotInfos
+   * @return <type>
+   */
+  public function insertArea($aPageId, $name, $slotInfos)
+  {
+    $this->fastClearArea($aPageId, $name);
+    $slotIds = array();
+    if (!count($slotInfos))
+    {
+      // Nothing to do
+      return;
+    }
+    $this->query('INSERT INTO a_area (page_id, name, culture, latest_version) VALUES (:page_id, :name, :culture, 1)', array('page_id' => $aPageId, 'name' => $name, 'culture' => 'en'));
+    $areaId = $this->lastInsertId();
+    foreach ($slotInfos as $slotInfo)
+    {
+      $this->query('INSERT INTO a_slot (type, value) VALUES (:type, :value)', array('type' => $slotInfo['type'], 'value' => (is_array($slotInfo['value']) ? serialize($slotInfo['value']) : $slotInfo['value'])));
+      $slotId = $this->lastInsertId();
+      $slotIds[] = $slotId;
+      if ($slotInfo['type'] === 'aSlideshow')
+      {
+        foreach ($slotInfo['value']['order'] as $mediaId)
+        {
+          $this->query('INSERT INTO a_slot_media_item (media_item_id, slot_id) VALUES (:media_item_id, :slot_id)', array('media_item_id' => $mediaId, 'slot_id' => $slotId));
+        }
+      }
+      if (($slotInfo['type'] === 'aImage') || ($slotInfo['type'] === 'aButton') && isset($slotInfo['mediaId']))
+      {
+        $this->query('INSERT INTO a_slot_media_item (media_item_id, slot_id) VALUES (:media_item_id, :slot_id)', array('media_item_id' => $slotInfo['mediaId'], 'slot_id' => $slotId));
+      }
+    }
+
+    $this->query('INSERT INTO a_area_version (area_id, version) VALUES(:area_id, 1)', array('area_id' => $areaId));
+    $areaVersionId = $this->lastInsertId();
+    $this->query('UPDATE a_area SET latest_version = :latest_version WHERE id = :id', array('id' => $areaId, 'latest_version' => 1));
+
+    $n = 1;
+    foreach ($slotIds as $slotId)
+    {
+      $this->query('INSERT INTO a_area_version_slot (slot_id, area_version_id, permid, rank) VALUES (:slot_id, :area_version_id, :permid, :rank)', array('slot_id' => $slotId, 'area_version_id' => $areaVersionId, 'permid' => $n, 'rank' => $n));
+      $n++;
+    }
+  }
+
+  public function fastSaveMediaItem($a)
+  {
+    $data = $a->toArray();
+    $this->query('INSERT INTO a_media_item (created_at, updated_at, slug, type, format, width, height, embed, title, description, credit, view_is_secure) VALUES (NOW(), NOW(), :slug, :type, :format, :width, :height, :embed, :title, :description, :credit, :view_is_secure)', $data);
+    $a->id = $this->lastInsertId();
+  }
+
+  public function fastSaveTags($taggable_model, $taggable_id , $tags)
+  {
+    // It would be faster to do fewer queries by caching what we know so far about tags
+    foreach ($tags as $tag)
+    {
+      $existing = Doctrine::getTable('Tag')->createQuery('t')->where('t.name = ?', $tag)->execute(array(), Doctrine::HYDRATE_ARRAY);
+      if (!count($existing))
+      {
+        $this->query('INSERT INTO tag (name) VALUES (:name)', array('name' => $tag));
+        $existing['id'] = $this->lastInsertId();
+      }
+      else
+      {
+        $existing = $existing[0];
+      }
+      $this->query('INSERT INTO tagging (tag_id, taggable_model, taggable_id) VALUES (:tag_id, :taggable_model, :taggable_id)', array('tag_id' => $existing['id'], 'taggable_model' => $taggable_model, 'taggable_id' => $taggable_id));
+    }
+  }
+
+  public function fastClearArea($aPageId, $name)
+  {
+    $this->query('DELETE FROM a_area WHERE name = :name AND page_id = :id', array('name' => $name, 'id' => $aPageId));
+  }
+
+  public function lastInsertId()
+  {
+    return $this->getPDO()->lastInsertId();
+  }
+
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aHtml.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aHtml.class.php	(revision 3146)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aHtml.class.php	(revision 3146)
@@ -0,0 +1,699 @@
+<?php
+
+/**
+ * HTML related utilities. HTML markup to RSS markup conversion,
+ * simplification of HTML to a short list of legal tags and no 
+ * dangerous attributes, mailto: obfuscation, word count limit
+ * that preserves valid HTML markup, and basic text-to-HTML
+ * conversion that preserves line breaks and creates links.
+ *
+ * doc-to-HTML conversion has been removed as it's out of scope for
+ * apostrophePlugin which should contain lightweight stuff only.
+ * We should consider putting that out as a separate plugin.
+ *
+ * @author Tom Boutell <tom@punkave.com>
+ */
+
+class aHtmlNotHtmlException extends Exception
+{
+  
+}
+
+class aHtml
+{
+  static private $badPunctuation = array('â', 'â', 'Â®', 'â', 'â');
+  static private $badPunctuationReplacements = array('&lquot;', '&rquot;', '&reg;', '&lsquo;', '&rsquo;');
+
+  static private $rssEntityMap = 
+    array('&lquot;' => '\"',
+      '&rquot;' => '\"',
+      '&reg;' => '(Reg TM)', 
+      '&lsquo;' => '\'',
+      '&rsquo;' => '\'',
+      '&bull' => '*',
+      '&amp;' => '&amp;',
+      '&lt;' => '&lt;',
+      '&gt;' => '&gt;'
+    );
+
+  // Right now this just converts obscure HTML entities to 
+  // simpler stuff that all feed readers will digest.
+  public static function htmlToRss($doc)
+  {
+    // Eval stuff like this is not the quickest. There 
+    // must be a better way. We should be saving a
+    // pre-RSSified version of posts, for one thing.
+    return preg_replace(
+      '/(&\w+;)/e', 
+      "aHtml::entityToRss('$1')",
+      $doc);
+  }
+  
+  public static function entityToRss($entity)
+  {
+    if (isset(self::$rssEntityMap[$entity]))
+    {
+      return self::$rssEntityMap[$entity];
+    } 
+    else
+    {
+      return '';
+    }
+  }
+
+  // The default list of allowed tags for aHtml::simplify().
+  // These work well for user-generated content made with FCK.
+  // You can now alter this list by passing a similar list as the second
+  // argument to aHtml::simplify(). An array of tag names without braces is also allowed.
+  
+  // Reserving h1 and h2 for the site layout's use is generally a good idea
+  
+  static private $defaultAllowedTags =
+    '<h3><h4><h5><h6><blockquote><p><a><ul><ol><nl><li><b><i><strong><em><strike><code><hr><br><div><table><thead><caption><tbody><tr><th><td><pre>';
+
+  // The default list of allowed attributes for aHtml::simplify().
+  // You can now alter this list by passing a similar array as the fourth
+  // argument to aHtml::simplify().
+
+  static private $defaultAllowedAttributes = array(
+    "a" => array("href", "name", "target"),
+    "img" => array("src")
+  );
+  
+  // Subtle control of the style attribute is possible, but we don't allow
+  // any styles by default. See the allowedStyles argument to simplify()
+  
+  static private $defaultAllowedStyles = array();
+
+  // allowedTags can be an array of tag names, without < and > delimiters, 
+  // or a continuous string of tag names bracketed by < and > (as strip_tags 
+  // expects). 
+  
+  // By default, if the 'a' tag is in allowedTags, then we allow the href attribute on 
+  // that (but not JavaScript links). If the 'img' tag is in allowedTags, 
+  // then we allow the src attribute on that (but no JavaScript there either).
+  // You can alter this by passing a different array of allowed attributes.
+
+  // If $complete is true, the returned string will be a complete
+  // HTML 4.x document with a doctype and html and body elements.
+  // otherwise, it will be a fragment without those things
+  // (which is what you almost certainly want).
+  
+  // If $allowedAttributes is not false, it should contain an array in which the
+  // keys are tag names and the values are arrays of attribute names to be permitted.
+  // Note that javascript: is forbidden at the start of any attribute, so attributes
+  // that act as URLs should be safe to permit (we now check for leading space and
+  // mixed case variations of javascript: as well).
+  
+  // If $allowedStyles is not false, it should contain an array in which the keys
+  // are tag names and the values are arrays of CSS style property names to be permitted.
+  // This is a much better idea than just allowing the style attribute, which is one
+  // of the best ways to kill the layout of an entire page.
+  //
+  // An example:
+  //
+  // array("table" => array("width", "height"),
+  //   "td" => array("width", "height"),
+  //   "th" => array("width", "height"))
+  //
+  // Note that rich text editors vary in how they handle table width and height; 
+  // Safari sets the width and height attributes of the tags rather than going
+  // the CSS route. The simplest workaround is to allow that too.
+
+  // loadHtml, in its infinite wisdom, insists on giving us br tags
+  // without a proper /> at the end. Force a fix by default (thanks to
+  // Geoff Hammond). This is done in a simple way that would have problems 
+  // if you were allowing script elements, but why would you do such a foolish thing?
+  static private $defaultHtmlStrictBr = true;
+
+  static public function simplify($value, $allowedTags = false, $complete = false, $allowedAttributes = false, $allowedStyles = false, $htmlStrictBr = false)
+  {
+    if ($allowedTags === false)
+    {
+      // Not using Symfony? Replace the entire sfConfig::get call with self::$defaultAllowedTags
+      $allowedTags = sfConfig::get('app_aToolkit_allowed_tags', self::$defaultAllowedTags);
+    }
+    if ($allowedAttributes === false)
+    {
+      // See above
+      $allowedAttributes = sfConfig::get('app_aToolkit_allowed_attributes', self::$defaultAllowedAttributes);
+    }
+    if ($allowedStyles === false)
+    {
+      // See above
+      $allowedStyles = sfConfig::get('app_aToolkit_allowed_styles', self::$defaultAllowedStyles);
+    }
+    if ($htmlStrictBr === false)
+    {
+      // See above
+      $htmlStrictBr = sfConfig::get('app_aToolkit_html_strict_br', self::$defaultHtmlStrictBr);
+    }
+    $value = trim($value);
+    if (!strlen($value))
+    {
+      // An empty string is NOT something to panic
+      // and generate warnings about
+      return '';
+    }
+    if (is_array($allowedTags))
+    {
+      $tags = "";
+      foreach ($allowedTags as $tag)
+      {
+        $tags .= "<$tag>";
+      }
+      $allowedTags = $tags;
+    }
+    $value = strip_tags($value, $allowedTags);
+
+    // Now we use DOMDocument to strip attributes. In principle of course
+    // we could do the whole job with DOMDocument. But in practice it is quite
+    // awkward to hoist subtags correctly when a parent tag is not on the
+    // allowed list with DOMDocument, and strip_tags takes care of that
+    // task just fine.
+
+    // At first I used matt@lvi.org's function from the strip_tags 
+    // documentation wiki. Unfortunately preg_replace tends to return null
+    // on some of his regexps for nontrivial documents which is pretty
+    // disastrous. He seems to have some greedy regexps where he should
+    // have ungreedy regexps. Let's do it right rather than trying to
+    // make regular expressions do what they shouldn't.
+
+    // We also get rid of javascript: links here, a good idea from 
+    // Matt's script.
+    
+    $oldHandler = set_error_handler("aHtml::warningsHandler", E_WARNING);
+    
+    // If we do not have a properly formed <html><head></head><body></body></html> document then
+    // UTF-8 encoded content will be trashed. This is important because we support fragments
+    // of HTML containing UTF-8 as part of a
+    if (!preg_match("/<head>/i", $value))
+    {
+      $value = '
+      <html>
+      <head>
+      <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+      </head>
+      <body>
+      ' . $value . '
+      </body>
+      </html>
+      ';
+    }
+    try 
+    {
+      // Specify UTF-8 or UTF-8 encoded stuff passed in will turn into sushi.
+      $doc = new DOMDocument('1.0', 'UTF-8');
+      $doc->strictErrorChecking = true;
+      $doc->loadHTML($value);
+      self::stripAttributesNode($doc, $allowedAttributes, $allowedStyles);
+      // Per user contributed notes at 
+      // http://us2.php.net/manual/en/domdocument.savehtml.php
+      // saveHTML forces a doctype and container tags on us; get
+      // rid of those as we only want a fragment here
+      $result = $doc->saveHTML();
+    } catch (aHtmlNotHtmlException $e)
+    {
+      // The user thought they were entering text and used & accordingly (as they so often do)
+      $result = htmlspecialchars($value);
+    }
+
+    // Browser RTEs love to insert <p>&nbsp;</p> where <br /> is all they really need.
+    // There are more elaborate cases we don't mess with because 
+    // introducing a <br /> as a replacement for <h4>&nbsp;</h4> would not
+    // have the same impact (an h4-sized gap between two h4s). Tested across
+    // browsers. Fixes #500
+    $result = str_replace('<p>&nbsp;</p>', '<br />', $result);
+    
+    if($htmlStrictBr)
+    {
+      $result = str_replace('<br>', '<br />', $result);
+    }
+
+    if ($oldHandler)
+    {
+      set_error_handler($oldHandler);
+    }
+      
+    if ($complete)
+    {
+      // Don't allow whitespace to balloon
+      return trim($result);
+    }
+
+    $result = self::documentToFragment($result);
+		return $result;
+  }
+
+  static public function documentToFragment($s)
+  {
+    // Added trim call because otherwise size begins to balloon indefinitely
+    return trim(preg_replace(array('/^<!DOCTYPE.+?>/', '/<head>.*?<\/head>/i'), '', 
+      str_replace( array('<html>', '</html>', '<body>', '</body>'), array('', '', '', ''), $s)));
+  }
+  
+  static public function warningsHandler($errno, $errstr, $errfile, $errline) 
+  {
+    // Most warnings should be ignored as DOMDocument cleans up the HTML in exactly
+    // the way we want. However "no name in entity" usually means the user thought they
+    // were entering plaintext, so we should throw an exception signaling that
+    
+    if (strstr("no name in Entity", $errstr))
+    {
+      throw new aHtmlNotHtmlException();
+    }
+    return;
+  }
+  
+  static private function stripAttributesNode($node, $allowedAttributes, $allowedStyles)
+  {
+    if ($node->hasChildNodes())
+    {
+      foreach ($node->childNodes as $child)
+      {
+        self::stripAttributesNode($child, $allowedAttributes, $allowedStyles);
+      }
+    }
+    if ($node->hasAttributes())
+    {
+      $removeList = array();
+      foreach ($node->attributes as $index => $attr)
+      {
+        $good = false;
+        if ($attr->name === 'style')
+        {
+          if (isset($allowedStyles[$node->nodeName]))
+          {
+            // There is no handy function in core PHP to parse CSS rules, so we'll do it ourselves
+            
+            // First chop it into raw tokens as follows: /* ... */, \', \", ;, :, ', " and anything else
+            $styles = array();
+            $rawTokens = preg_split('/(\/\*.*?\*\/|\\\'|\\\"|;|:|\'|")/', $attr->value, null, PREG_SPLIT_DELIM_CAPTURE);
+            // Now assemble quoted strings into single tokens, inclusive of escaped quotes, ;, :, etc. so that
+            // we don't get tripped up by them later
+            $realTokens = array();
+            $single = false;
+            $double = false;
+            $s = '';
+            foreach ($rawTokens as $rawToken)
+            {
+              if ($rawToken === "'")
+              {
+                if ($single)
+                {
+                  $single = false;
+                  $realTokens[] = "'" . $s . "'";
+                }
+                else
+                {
+                  $single = true;
+                  $s = '';
+                }
+              }
+              elseif ($rawToken === '"')
+              {
+                if ($double)
+                {
+                  $double = false;
+                  $realTokens[] = '"' . $s . '"';
+                }
+                else
+                {
+                  $double = true;
+                  $s = '';
+                }
+              }
+              else
+              {
+                if ($single || $double)
+                {
+                  $s .= $rawToken;
+                }
+                else
+                {
+                  $realTokens[] = $rawToken;
+                }
+              }
+            }
+            // Now we can just scan for semicolons and colons and make pretty rules
+            $styles = array();
+            $state = 'property';
+            $p = '';
+            $v = '';
+						if (end($realTokens) !== ';')
+						{
+							$realTokens[] = ';';
+						}
+            foreach ($realTokens as $token)
+            {
+              if ($state === 'property')
+              {
+                if ($token === ':')
+                {
+                  $state = 'value';
+                }
+                else
+                {
+                  // We dump comments. Seems like a good idea in a tool used to clean up
+                  // rich text editor output. If we didn't do this, we'd need a way to
+                  // preserve them while still comparing names correctly
+                  if (substr($token, 0, 2) !== '/*')
+                  {
+                    $p .= $token;
+                  }
+                }
+              }
+              elseif ($state === 'value')
+              {
+                if ($token === ';')
+                {
+                  // TODO: unescape quotes and unicode escapes in property names so
+                  // we can compare them to the allowed properties, then reescape them
+                  // when assembling the final rules. 
+                  // 
+                  // Not that hard given the tokenizing we've already done,
+                  // but rich text editors don't generally introduce that nonsense
+                  // into style attributes
+                  $p = trim($p);
+                  $styles[$p] = $v;
+                  $p = '';
+                  $v = '';
+                  $state = 'property';
+                }
+                else
+                {
+                  // We dump comments. Seems like a good idea in a tool used to clean up
+                  // rich text editor output
+                  if (substr($token, 0, 2) !== '/*')
+                  {
+                    $v .= $token;
+                  }
+                }
+              }
+              else
+              {
+                throw new sfException('Unknown state in CSS parser in stripAttributesNode: ' . $state);
+              }
+            }
+            $allowed = array_flip($allowedStyles[$node->nodeName]);
+            $newStyles = array();
+            foreach ($styles as $p => $v)
+            {
+              if (isset($allowed[$p]))
+              {
+                $newStyles[$p] = $v;
+              }
+            }
+            $good = true;
+            $rules = array();
+            foreach ($newStyles as $p => $v)
+            {
+              $rules[] = "$p: $v;";
+            }
+            $attr->value = implode(' ', $rules);
+          }
+        }
+        if (!$good)
+        {
+          if (isset($allowedAttributes[$node->nodeName]))
+          {
+            foreach ($allowedAttributes[$node->nodeName] as $attrName)
+            {
+              // Be more careful about this: leading space is tolerated by the browser,
+              // so is mixed case in the protocol name (at least in Firefox and Safari, 
+              // which is plenty bad enough)
+              if (($attr->name === $attrName) && (!preg_match('/^\s*javascript:/i', $attr->value)))
+              {
+                // We keep this one
+                $good = true;
+              }
+            }
+          }
+        }
+        if (!$good)
+        {
+          // Off with its head
+          $removeList[] = $attr->name; 
+        }
+      }
+      foreach ($removeList as $name)
+      {
+        $node->removeAttribute($name);
+      }
+    }
+  }
+
+  // TODO: limitWords currently might not do a great job on typical
+  // "gross" HTML without closing </p> tags and the like.
+
+  static private $nonContainerTags = array(
+    "br" => true,
+    "img" => true,
+    "input" => true
+  );
+
+	public static function limitWords($string, $word_limit, $options = array())
+	{
+	  # TBB: tag-aware, doesn't split in the middle of tags 
+	  # (we will probably use fancier tags with attributes later,
+	  # so this is important). Tags must be valid XHTML unless
+	  # all allowed tags 
+	  $words = preg_split("/(\<.*?\>|\s+)/", $string, -1, 
+	    PREG_SPLIT_DELIM_CAPTURE);
+	  $wordCount = 0;
+	  # Balance tags that need balancing. We don't have strict XHTML
+	  # coming from OpenOffice (oh, if only) so we'll have to keep a
+	  # list of the tags that are containers.
+	  $open = array();
+	  $result = "";
+	  $count = 0;
+		$num_words = count($words);
+		
+		$shortEnough = true;
+		
+	  foreach ($words as $word) {
+	    if ($count > $word_limit) {
+				$shortEnough = false;
+	      break;
+	    } elseif (preg_match("/\<.*?\/\>/", $word)) {
+	      # XHTML non-container tag, we don't have to guess
+	      $result .= $word;
+	      continue;
+	    } elseif (preg_match("/\<(\w+)/s", $word, $matches)) {
+	      $tag = $matches[1];
+	      $result .= $word;
+	      if (isset(aHtml::$nonContainerTags[$tag])) {
+	        continue;
+	      }
+	      $open[] = $tag;
+	    } elseif (preg_match("/\<\/(\w+)/s", $word, $matches)) {
+	      $tag = $matches[1];
+	      if (!count($open)) {
+	        # Groan, extra close tag, ignore
+	        continue;
+	      }
+	      $last = array_pop($open);    
+	      if ($last !== $tag) {
+	        # They closed the wrong tag. Again, ignore for now, but 
+	        # we might want to work on a better solution
+	        continue;
+	      }
+	      $result .= $word;
+	    } elseif (preg_match("/^\s+$/s", $word)) {
+	      $result .= $word;
+	    } else {
+	      if (strlen($word)) {
+	        $count++;
+	        $result .= $word;
+	      }
+	    }
+	  }
+	
+		if ($shortEnough)
+		{
+			// Leave it totally untouched if it is short enough.
+			// Now you can use !== to see if it changed anything.
+			return $string;
+		}
+
+		$append_ellipsis = false;
+		if (isset($options['append_ellipsis']))
+		{
+			$append_ellipsis = $options['append_ellipsis'];
+		}
+		if ($append_ellipsis == true && $num_words > $word_limit)
+		{
+			$result .= '&hellip;';
+		}
+
+	  for ($i = count($open) - 1; ($i >= 0); $i--) {
+	    $result .= "</" . $open[$i] . ">";
+	  }
+	  return $result;
+	}
+
+  // This is a quick and dirty implementation based on calling limitWords
+  // with an optimistic guess and then backing off a few times if necessary
+  // until we get under the byte limit. Note that limitBytes is designed
+  // to fit things in buffers, not save screen space, so it does have to
+  // make sure the result is not too big
+  
+	public static function limitBytes($string, $byte_limit, $options = array())
+	{
+	  $word_limit = (int) ($byte_limit / 8);
+	  while (true)
+	  {
+	    $s = aHtml::limitWords($string, $word_limit, $options);
+	    if (strlen($s) <= $byte_limit)
+	    {
+	      break;
+	    }
+	    $word_limit = (int) ($word_limit * 0.75);
+	  }
+	  return $s;
+	}
+
+  public static function toText($html)
+  {
+    # Nothing fancy, we use the text for indexing only anyway.
+    # It would be nice to do a prettier job here for future applications
+    # that need pretty plaintext representations. That would be useful 
+    # as an alt-body in emails. This does not entity-decode. See
+    // toPlaintext for that
+    $txt = strip_tags($html);
+    return $txt;
+  }
+
+  public static function obfuscateMailto($html)
+  {
+    # Obfuscates any mailto: links found in $html. Good if you already
+    # have nice HTML from FCK or what have you. 
+   
+    # Note that this updated version is AJAX-friendly
+    # (it does not use document.write). Also, it preserves
+    # the innerHTML of the original link rather than forcing it
+    # to be the address found in the href.
+
+    # ACHTUNG: mailto links will become simply
+    # <a href="mailto:foo@bar.com">whatever-was-inside</a> (in the final
+    # presentation to the user, after obfuscation via javascript). 
+    # If there are other attributes on the <a> tag they will get tossed out.
+    # This is usually not a problem for code that
+    # comes from FCK etc. If it is a problem for you, make
+    # this method smarter. Also consider just wrapping the link in
+    # a span or div, which will not lose its class, id, etc. TBB
+
+    return preg_replace_callback("/\<a[^\>]*?href=\"mailto\:(.*?)\@(.*?)\".*?\>(.*?)\<\/a\>/is", 
+      array('aHtml', 'obfuscateMailtoInstance'),
+      $html);
+  }
+  
+  public static function obfuscateMailtoInstance($args)
+  {
+    list($user, $domain, $label) = array_slice($args, 1);
+    // We get some weird escaping problems without the trims
+    $user = trim($user);
+    $domain = trim($domain);
+    $id = 'a-email-' . sprintf("%u", crc32($user . $domain));
+    $href = rawurlencode("mailto:$user@$domain");
+    $label = rawurlencode(trim($label));
+    // This is an acceptable way to stub in a js call for now, since it's the
+    // way the helper has to do it too
+    aTools::$jsCalls[] = array('callable' => 'apostrophe.unobfuscateEmail(?, ?, ?)', 'args' => array($id, $href, $label));
+    return "<a href='#' id='$id'></a>";
+  }
+
+  // This is intentionally obscure for use in mailto: obfuscators.
+  // For an efficient way to pass data to javascript, use json_encode
+  static public function jsEscape($str)
+  {
+
+    $new_str = '';
+
+    for($i = 0; ($i < strlen($str)); $i++) {
+      $new_str .= '\\x' . dechex(ord(substr($str, $i, 1)));
+    }
+
+    return $new_str;
+  }
+
+  /**
+   * Just the basics: escape entities, turn URLs into links, and turn newlines into line breaks.
+   * Also turn email addresses into links (we don't obfuscate them here as that makes them
+   * harder to manipulate some more, but check out aHtml::obfuscateMailto). 
+   *
+   * This function is now a wrapper around TextHelper, except for the entity escape which is
+   * not included in simple_format_text for some reason 
+   *
+   * @param string $text The text you want converted to basic HTML.
+   * @return string Text with br tags and anchor tags.
+   */
+  static public function textToHtml($text)
+  {
+    sfContext::getInstance()->getConfiguration()->loadHelpers(array('Tag', 'Text'));
+   	return auto_link_text(simple_format_text(htmlentities($text, ENT_COMPAT, 'UTF-8')));
+  }
+
+  // For any given HTML, returns only the img tags. If 
+  // format is set to array, the result is returned as an array
+  // in which each element is an associative array with, at a
+  // minimum, a src attribute and also width, height, alt and title
+  // attributes if they were present in the tag. If format
+  // is set to html, an array of the original <img> tags
+  // is returned without further processing.
+  static public function getImages($html, $format = 'array')
+  {
+    $allowed = array_flip(array("src", "width", "height", "title", "alt"));
+    if (!preg_match_all("/\<img\s.*?\/?\>/i", $html, $matches, PREG_PATTERN_ORDER))
+    {
+      return array();
+    }
+    $images = $matches[0];
+    if (empty($images))
+    {
+      return array();
+    }
+    
+    if ($format == 'array')
+    {
+      $images_info = array();
+      foreach ($images as $image)
+      {
+        // Use a backreference to make sure we match the same
+        // type of quote beginning and ending
+        preg_match_all('/(\w+)\s*=\s*(["\'])(.*?)\2/', 
+          $image, 
+          $matches, 
+          PREG_SET_ORDER);
+        $attributes = array();
+        foreach ($matches as $attributeRaw)
+        {
+          $name = strtolower($attributeRaw[1]);
+          $value = $attributeRaw[3];
+          if (!isset($allowed[$name]))
+          {
+            continue;
+          }
+          $attributes[$name] = $value;
+        }
+        if (!isset($attributes['src']))
+        {
+          continue;
+        }
+        $images_info[] = $attributes;
+      }
+      
+      return $images_info;
+    }
+
+    return $images;
+  }
+  
+  static public function toPlaintext($html)
+  {
+    // Nonbreaking spaces don't work properly
+    // in a lot of contexts where plaintext is
+    // needed
+    return html_entity_decode(str_replace('&nbsp;', ' ', strip_tags($html)), ENT_COMPAT, 'UTF-8');
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMysql.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMysql.class.php	(revision 2951)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMysql.class.php	(revision 2951)
@@ -0,0 +1,331 @@
+<?php
+
+// A simple, safe, awesome wrapper for MySQL, used where Doctrine isn't
+// fast enough or doesn't allow you to write the SQL you really need (an issue
+// even with Doctrine_RawSql). Offers useful tools to check for existing columns 
+// and tables as well as a simple and clean way to make queries, insert rows,
+// delete rows, etc. without trying to mash objects into MySQL.
+
+// Borrowed from Tom's Plog project, which in turn borrowed the beginnings of
+// it from Apostrophe's aMigrate class.
+
+// -Tom
+
+class aMysql
+{
+  protected $conn;
+  protected $commandsRun;
+  
+  public function __construct()
+  {
+    // Raw PDO for performance
+    $connection = Doctrine_Manager::connection();
+    $this->conn = $connection->getDbh();
+  }
+
+  // Used to run a series of queries where you don't need parameters or results
+  // but would like to keep a count of those executed (usually migration stuff)
+  public function sql($commands)
+  {
+    foreach ($commands as $command)
+    {
+      $this->conn->query($command);
+      $this->commandsRun++;
+    }
+  }
+  
+  // Runs a single query, with parameters. If :foo appears in the query it gets
+  // substituted correctly (via PDO) with $params['foo']. Extra stuff in
+  // $params is allowed. The return value, as is standard with PDO, is an associative array 
+  // by column name as well as being a numerically indexed array in column order.
+  
+  // Note that not requiring a : in front of everything in the params array allows us to use a
+  // previous result as an argument.
+  
+  // If $params['foo'] is an array, then :foo is replaced by a correctly parenthesized and quoted
+  // array for use in a WHERE foo IN (a, b, c) clause. 
+  
+  public function query($s, $params = array())
+  {
+    $pdo = $this->conn;
+    $nparams = array();
+    foreach ($params as $key => $value)
+    {
+      // Tolerate numeric keys, which allows us to use the results of a 
+      // previous PDO query
+      if (is_numeric($key))
+      {
+        continue;
+      }
+      $regexp = '/:' . preg_quote($key, '/') . '\b/';
+      if (preg_match($regexp, $s) > 0)
+      {
+        // Arrays are turned into IN clauses (comma separated lists enclosed in parens)
+        if (is_array($value))
+        {
+          $s = preg_replace($regexp, '(' . implode(',', array_map(array($this, 'quote'), $value)) . ')', $s); 
+        }
+        else
+        {
+          $nparams[":$key"] = $value;
+        }
+      }
+    }
+    
+    $statement = $pdo->prepare($s);
+
+    // PDO has brain damage and can't figure out when to bind things as literals with
+    // PDO::PARAM_INT. This breaks offset and limit queries if you just bind naively with
+    // an array argument to execute(). Don't get greedy, only do this to definite integers
+
+    foreach ($nparams as $key => $value)
+    {
+      if (is_int($value) || preg_match('/^-?\d+$/', $value))
+      {
+        $statement->bindValue($key, $value, PDO::PARAM_INT);
+      }
+      else
+      {
+        $statement->bindValue($key, $value, PDO::PARAM_STR);
+      }
+    }
+    
+    try
+    {
+      $statement->execute();
+    }
+    catch (Exception $e)
+    {
+      throw new Exception("PDO exception on query: " . $s . " arguments: " . json_encode($params) . " bound arguments: " . json_encode($nparams) . "\n\n" . $e);
+    }
+    $result = true;
+    try
+    {
+      $result = $statement->fetchAll(PDO::FETCH_ASSOC);
+    } catch (Exception $e)
+    {
+      // Oh no, we tried to fetchAll on a DELETE statement, everybody panic!
+      // Seriously PDO, you need to relax
+    }
+    return $result;
+  }
+
+  // Why are you using this? Go read about the params argument to query() again
+  public function quote($item)
+  {
+    return $this->conn->quote($item);
+  }
+
+  // Returns just the first row of results. Add your own LIMIT clause to help MySQL
+  // deliver that efficiently. But also see find()
+  public function queryOne($query, $params = array())
+  {
+    $results = $this->query($query, $params);
+    if (count($results))
+    {
+      return $results[0];
+    }
+    return null;
+  }
+
+  // Handy for getting just the ids, just the names, etc. Returns an array 
+  // containing only the first column of each row 
+  public function queryScalar($query, $params = array())
+  {
+    $results = $this->query($query, $params);
+    $nresults = array();
+    foreach ($results as $result)
+    {
+      $nresults[] = reset($result);
+    }
+    return $nresults;
+  }
+
+  // Returns *one* scalar, useful when fetching just one thing.
+  // Note: returns null if there are no results, or no columns in the results (is that possible?)
+  public function queryOneScalar($query, $params = array())
+  {
+    $results = $this->query($query, $params);
+    if (!count($results))
+    {
+      return null;
+    }
+    $result = $results[0];
+    if (!count($result))
+    {
+      return null;
+    }
+    return reset($result);
+  }
+
+  // After an insert you'll need to know what the id of the new thing is.
+  // But also see insert()
+  public function lastInsertId()
+  {
+    return $this->conn->lastInsertId();
+  }
+  
+  // Trivial, but handy in array_map calls
+  public function colonPrefix($s)
+  {
+    return ':' . $s;
+  }
+  
+  // Useful for simple inserts. The id of the last added row is returned
+  // (just ignore the return value if the table does not have an autoincrementing id column).
+  public function insert($table, $params = array())
+  {
+    $columns = array_keys($params);
+    $this->query('INSERT INTO ' . $table . ' (' . implode(',', $columns) . ') VALUES (' . implode(',', array_map(array($this, 'colonPrefix'), $columns)) . ')', $params);
+    return $this->lastInsertId();
+  }
+  
+  // Useful for simple inserts where you'd like the resulting row returned to you.
+  // Not for use with tables that don't have an autoincrementing integer id
+  // named 'id', so just use query or plain insert() as you see fit. Makes an extra query to get what
+  // was really inserted since otherwise you won't get back values for the defaulted fields. 
+  // This is just a timesaver, use it where apropos
+  public function insertAndSelect($table, $params = array())
+  {
+    $columns = array_keys($params);
+    $this->query('INSERT INTO ' . $table . ' (' . implode(',', $columns) . ') VALUES (' . implode(',', array_map(array($this, 'colonPrefix'), $columns)) . ')', $params);
+    $id = $this->lastInsertId();
+    return $this->query('select * from ' . $table . ' where id = ?', array('id' => $id));
+  }
+  
+  // Handy for simple deletes where there is an 'id' column
+  public function delete($table, $id)
+  {
+    $this->query('DELETE FROM ' . $table . ' WHERE id = :id', array('id' => $id));
+  }
+
+  // Good for fetching a row when there is an 'id' column. 
+  public function find($table, $id)
+  {
+    return $this->queryOne('SELECT * from ' . $table . ' WHERE id = :id', array('id' => $id));
+  }
+  
+  public function exists($table, $id)
+  {
+    return !!$this->find($table, $id);
+  }
+  
+  // Writing SET clauses can be a pain. This method saves you the trouble for records
+  // with id columns
+  public function update($table, $id, $params = array())
+  {
+    $q = 'UPDATE ' . $table . ' ';
+    $first = true;
+    $params['id'] = $id;
+    foreach ($params as $k => $v)
+    {
+      if ($first)
+      {
+        $q .= 'SET ';
+        $first = false;
+      }
+      else
+      {
+        $q .= ', ';
+      }
+      $q .= $k . ' = :' . $k . ' ';
+    }
+    $q .= 'WHERE id = :id';
+    return $this->query($q, $params);
+  }
+
+  // Useful when you need the current MySQL date. $relative can be -30 minutes, +30 days, etc.
+  public function now($relative = '+0 seconds')
+  {
+    return date('Y-m-d H:i:s', strtotime($relative, time()));
+  }
+  
+  // Return the count of sql() calls 
+  public function getCommandsRun()
+  {
+    return $this->commandsRun;
+  }
+  
+  // Does this table already exist?
+  public function tableExists($tableName)
+  {
+    if (!preg_match('/^\w+$/', $tableName))
+    {
+      throw new Exception("Bad table name in tableExists: $tableName\n");
+    }
+    $data = array();
+    try
+    {
+      $data = $this->conn->query("SHOW CREATE TABLE $tableName")->fetchAll();
+    } catch (Exception $e)
+    {
+    }
+    return (isset($data[0]['Create Table']));    
+  }
+  
+  // Does this column already exist?
+  public function columnExists($tableName, $columnName)
+  {
+    if (!preg_match('/^\w+$/', $tableName))
+    {
+      die("Bad table name in columnExists: $tableName\n");
+    }
+    if (!preg_match('/^\w+$/', $columnName))
+    {
+      die("Bad table name in columnExists: $columnName\n");
+    }
+    $data = array();
+    try
+    {
+      $data = $this->conn->query("SHOW COLUMNS FROM $tableName LIKE '$columnName'")->fetchAll();
+    } catch (Exception $e)
+    {
+    }
+    return (isset($data[0]['Field']));
+  }
+  
+  // Return a value that will be unique for the column (assuming no race condition of course;
+  // you should still use UNIQUE INDEX) by modifying $value until it doesn't already exist.
+  // Trusts table and column (you would never let users enter metadata like that, right?)
+  
+  public function uniqueify($table, $column, $value, $exceptId = null)
+  {
+    $cvalue = $value;
+    $n = 1;
+    while (!$this->unique($table, $column, $cvalue, $exceptId))
+    {
+      $n++;
+      // Compatible with slugify
+      $cvalue = $value . '-' . $n;
+    }
+    return $cvalue;
+  }
+
+  // Just check for uniqueness. When you are updating an existing row it is
+  // convenient to pass the id of the existing row so keeping the value the same
+  // is not considered a conflict
+  public function unique($table, $column, $value, $exceptId = null)
+  {
+    $q = 'select * from ' . $table . ' where ' . $column . ' = :value ';
+    if (!is_null($exceptId))
+    {
+      $q .= 'AND id <> :except_id';
+    }
+    if (count($this->query($q, array('value' => $value, 'except_id' => $exceptId))))
+    {
+      return false;
+    }
+    return true;
+  }
+  
+  // Grab just the ids from an array of results
+  public function getIds($results)
+  {
+    $ids = array();
+    foreach ($results as $result)
+    {
+      $ids[] = $result['id'];
+    }
+    return $ids;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDump.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDump.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDump.class.php	(revision 9)
@@ -0,0 +1,100 @@
+<?php
+
+class aDump
+{
+  // Attempts to output the variable as valid PHP code. 
+  // Handy for metaprogramming. var_dump doesn't quite do this, 
+  // and serialize has its own format, etc.
+
+  // Does not work with objects. Does work with
+  // arrays (associative and flat), strings, numbers,
+  // null and false.
+
+  static public function dump($var, $depth = 0, $preIndented = false)
+  {
+    $result = "";
+    if (!$preIndented)
+    {
+      $result .= str_repeat("  ", $depth);
+    }
+    if (is_array($var))
+    {
+      $result .= "array(\n";
+      $keys = array_keys($var);
+      $associative = false;
+      foreach ($keys as $key)
+      {
+        if (!preg_match("/\d+/", $key))
+        {
+          $associative = true;
+        }
+      }
+      $first = true;
+      if ($associative)
+      {
+        foreach ($var as $key => $value)
+        {
+          if ($first)
+          {
+            $first = false;
+          }
+          else
+          {
+            $result .= ",\n";
+          }
+          $result .= str_repeat("  ", $depth + 1) . self::dump($key, $depth + 1, true) . " => " . self::dump($value, $depth + 1, true);
+        }  
+      }
+      else
+      {
+        foreach ($var as $value)
+        {
+          if ($first)
+          {
+            $first = false;
+          }
+          else
+          {
+            $result .= ",\n";
+          }
+          $result .= self::dump($value, $depth + 1, false);
+        }  
+      }
+      $result .= "\n" . str_repeat("  ", $depth) . ")";
+    }
+    elseif (is_null($var))
+    {
+      $result .= "null";
+    }
+    elseif ($var === false)
+    {
+      $result .= "false";
+    }
+    elseif (is_numeric($var))
+    {
+      $result .= $var;
+    }
+    else
+    {
+      $result .= "'" . str_replace("'", "\\'", $var) . "'";
+    }
+    return $result;
+  }
+}
+
+// $dump = aDump::dump(
+//   array("one" => 1,
+//     "two" => 2,
+//     "three" => 3,
+//     "four'teen" => null,
+//     75 => false,
+//     "five" => "five",
+//     "six" => array(
+//       1, 2, 3.78, 4, 5),
+//     "seven" => 7));
+// 
+// echo("The dump is:\n\n");
+// echo($dump);
+// 
+// echo("\n\nvar_dump of an eval call on that dump is:\n\n");
+// var_dump(eval("return " . $dump . ";"));
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aProcesses.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aProcesses.class.php	(revision 2494)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aProcesses.class.php	(revision 2494)
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * 4/28/08 
+ *
+ * Start processes in various ways.
+ *
+ * @author Tom Boutell <tom@punkave.com>
+ */
+
+class aProcesses
+{
+ /*
+  *
+  * Like system(), but the command runs in the background
+  * and is detached correctly from standard input and output.
+  *
+  */
+
+  static public function background($cmd)
+  {
+    $result = system("($cmd &) < /dev/null > /dev/null");
+    return $result;
+  }
+
+ /*
+  *
+  * Like system(), but stdout is discarded, and we return
+  * the result code (like C or Perl system()), not the first line of output.
+  *
+  */
+
+  static public function quiet($cmd)
+  {
+    $result = false;
+    system("($cmd) > /dev/null", $result);
+    return $result;
+  }
+
+ /*
+  *
+  * Like system(), but stdout AND stderr are discarded, and we return
+  * the result code (like C or Perl system()), not the first line of output.
+  *
+  */
+
+  static public function veryQuiet($cmd)
+  {
+    $result = false;
+    system("($cmd) >> /dev/null 2>&1", $result);
+    return $result;
+  }
+  
+  /*
+   *
+   * Handy if you're reinvoking based on $argv[]. 
+   * First argument is executable
+   *
+   */
+   
+  static public function systemArray($args, &$result = null)
+  {
+    /* for some reason argv[0] does not contain the PHP interpreter itself.
+      This is especially problematic on Windows */
+    if ($args[0] === 'symfony' || $args[0] === './symfony') 
+    {
+      $args[0] = 'php ./symfony';
+    }
+    $eargs = array();
+    foreach ($args as $arg)
+    {
+      $eargs[] = escapeshellarg($arg);
+    }
+    $cmd = implode(' ', $args);
+    return system($cmd, $result);
+  }
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverter.class.php.edited
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverter.class.php.edited	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverter.class.php.edited	(revision 1957)
@@ -0,0 +1,658 @@
+<?php
+
+/*
+ *
+ * Efficient image conversions using netpbm or (if netpbm is not available) gd.
+ * For more information see the README file.
+ *
+ */ 
+
+class aImageConverter 
+{
+  // Produces images suitable for intentional cropping by CSS.
+  // Either the width or the height will match the request; the other
+  // will EXCEED the request. Looks nicer than letterboxing in cases
+  // where keeping the entire picture is not essential.
+
+  static public function scaleToNarrowerAxis($fileIn, $fileOut, $width, $height, $quality = 75)
+  {
+    $width = ceil($width);
+    $height = ceil($height);
+    $quality = ceil($quality);
+    list($iwidth, $iheight) = getimagesize($fileIn); 
+    if (!$iwidth) {
+      return false;
+    }
+    $iratio = $iwidth / $iheight;
+    $ratio = $width / $height;
+    if ($iratio > $ratio) {
+      $width = false;
+    } else {
+      $height = false;
+    }
+    return self::scaleToFit($fileIn, $fileOut, $width, $height, $quality);
+  }
+
+  static public function scaleToFit($fileIn, $fileOut, $width, $height, $quality = 75)
+  {
+    if ($width === false) {
+      $scaleParameters = array('ysize' => $height + 0);
+    } elseif ($height === false) {
+      $scaleParameters = array('xsize' => $width + 0);
+    } else {
+      $scaleParameters = array('xysize' => array($width + 0, $height + 0));
+    }
+    $result = self::scaleBody($fileIn, $fileOut, $scaleParameters, array(), $quality);
+    return $result;
+  }
+
+  static public function scaleByFactor($fileIn, $fileOut, $factor, 
+    $quality = 75)
+  {
+    $quality = ceil($quality);
+    $scaleParameters = array('scale' => $factor + 0);  
+    return self::scaleBody($fileIn, $fileOut, $scaleParameters, array(), $quality);
+  }
+
+  // $width and $height are the dimensions of the final rendered image. $quality is the JPEG quality setting (where needed).
+  // The $crop parameters, when not null (all four must be null or not null), are used to crop the original before scaling/distorting 
+  // to the specified width and height and are always in the original image's coordinates.
+  
+  // If cropping coordinates are not specified, the largest possible portion of the center of the original image is scaled to fit into the 
+  // destination image without distortion
+  
+  static public function cropOriginal($fileIn, $fileOut, $width, $height, $quality = 75, $cropLeft = null, $cropTop = null, $cropWidth = null,  $cropHeight = null)
+  {
+    // Allow skipping of parameters
+    if (is_null($quality))
+    {
+      $quality = 75;
+    }
+    $width = ceil($width);
+    $height = ceil($height);
+    $quality = ceil($quality);
+    list($iwidth, $iheight) = getimagesize($fileIn); 
+    if (!$iwidth) 
+    {
+      return false;
+    }
+    $iratio = $iwidth / $iheight;
+    $ratio = $width / $height;
+
+     // Spike's contribution: arbitrary cropping
+     if (!is_null($cropWidth) && !is_null($cropHeight) && !is_null($cropLeft) && !is_null($cropTop))
+     {
+       $cropTop = ceil($cropTop + 0);
+       $cropLeft = ceil($cropLeft + 0);
+       $cropWidth = ceil($cropWidth + 0);
+       $cropHeight = ceil($cropHeight + 0);
+       
+       $scale = array('xysize' => array($width + 0, $height + 0));
+       $crop = array('left' => $cropLeft, 'top' => $cropTop, 'width' => $cropWidth, 'height' => $cropHeight);
+       return self::scaleBody($fileIn, $fileOut, $scale, $crop, $quality);
+     }
+
+    $scale = array('xysize' => array($width + 0, $height + 0));
+    if ($iratio < $ratio)
+    {
+      $cropHeight = floor($iwidth * ($height / $width));
+      $cropTop = floor(($iheight - $cropHeight) / 2);
+      $cropLeft = 0;
+      $cropWidth = $iwidth;
+    }
+    else
+    {
+      $cropWidth = floor($iheight * $ratio);
+      $cropLeft = floor(($iwidth - $cropWidth) / 2);
+      $cropTop = 0;
+      $cropHeight = $iheight;
+    }
+    $scale = array('xysize' => array($width + 0, $height + 0));
+    $crop = array('left' => $cropLeft, 'top' => $cropTop, 'width' => $cropWidth, 'height' => $cropHeight);
+    return self::scaleBody($fileIn, $fileOut, $scale, $crop, $quality);
+  }
+
+  // Change the format without cropping or scaling
+  static public function convertFormat($fileIn, $fileOut, $quality = 75)
+  {
+    $quality = ceil($quality);
+    return self::scaleBody($fileIn, $fileOut, false, false, $quality);
+  }
+
+  static private function scaleBody($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75) 
+  {    
+    if (sfConfig::get('app_aimageconverter_netpbm', true))
+    {
+      // Auto fallback to gd, but only if it's not a small image gd can handle better (1.4). This means we get
+      // full alpha channel for manageably-sized PNGs and good performance for huge PNGs
+      $info = getimagesize($fileIn);
+      $mapTypes = array(IMAGETYPE_GIF => IMG_GIF, IMAGETYPE_PNG => IMG_PNG, IMAGETYPE_JPEG => IMG_JPG);
+      // If we got valid image info, the image size is less than 1024x768, gd is enabled, and gd supports
+      // the image type... *then* we skip to gd.
+      if (($info !== false) && (($info[0] <= 1024) && ($info[1] <= 768)) && function_exists('imagetypes') && isset($mapTypes[$info[2]]) && (imagetypes() & $mapTypes[$info[2]]))
+      {
+        return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+      }
+      $result = self::scaleNetpbm($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+      if (!$result)
+      {
+        return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+      }
+    }
+    else
+    {
+      return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+    }
+  }
+  
+  static private function scaleNetpbm($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75)
+  {
+    $outputFilters = array(
+      "jpg" => "pnmtojpeg --quality %d",
+      "jpeg" => "pnmtojpeg --quality %d",
+      "ppm" => "cat",
+      "pbm" => "cat",
+      "pgm" => "cat",
+      "tiff" => "pnmtotiff",
+      "png" => "pnmtopng",
+      "gif" => "ppmquant 256 | ppmtogif",
+      "bmp" => "ppmtobmp"
+    );
+    if (preg_match("/\.(\w+)$/", $fileOut, $matches)) {
+      $extension = $matches[1];
+      $extension = strtolower($extension);
+      if (!isset($outputFilters[$extension])) {
+        return false;
+      }
+      $filter = sprintf($outputFilters[$extension], $quality);
+    } else {
+      return false;
+    }
+    $path = sfConfig::get("app_aimageconverter_path", "");
+    if (strlen($path)) {
+      if (!preg_match("/\/$/", $path)) {
+        $path .= "/";
+      }
+    }
+        
+    // AUGH: some versions of anytopnm don't have
+    // the brains to look at the file signature. We need
+    // to be compatible with this brain damage, so pick
+    // the right filter based on the results of getimagesize()
+    // and punt to anytopnm only if we can't figure it out.
+    
+    // While we're at it: detect PDF by magic number too,
+    // not by extension, that's tacky
+
+    $input = 'anytopnm';
+    
+    $in = fopen($fileIn, 'r');
+    $bytes = fread($in, 4);
+    if ($bytes === '%PDF')
+    {
+      $input = 'gs -sDEVICE=ppm -sOutputFile=- ' .
+        ' -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q -';
+    }
+    fclose($in);
+    
+    $info = getimagesize($fileIn);
+    if ($info !== false)
+    {
+      $type = $info[2];
+      if ($type === IMAGETYPE_GIF)
+      {
+        $input = 'giftopnm';
+      } 
+      elseif ($type === IMAGETYPE_PNG)
+      {
+        $input = 'pngtopnm';
+      }
+      elseif ($type === IMAGETYPE_JPEG)
+      {
+        $input = 'jpegtopnm';
+      }
+    }
+    
+  
+    $scaleString = '';
+    $extraInputFilters = '';
+    foreach ($scaleParameters as $key => $values)
+    {
+      $scaleString .= " -$key ";
+      if (is_array($values))
+      {
+        foreach ($values as $value)
+        {
+          $value = ceil($value);
+          $scaleString .= " $value";
+        }
+      }
+      else
+      {
+        $values = ceil($values);
+        $scaleString .= " $values";
+      }
+    }
+    if (count($cropParameters))
+    {
+      $extraInputFilters = 'pnmcut ';
+      foreach ($cropParameters as $ckey => $cvalue)
+      {
+        $cvalue = ceil($cvalue);
+        $extraInputFilters .= " -$ckey $cvalue";
+      }
+    }
+    
+    $cmd = "(PATH=$path:\$PATH; export PATH; $input < " . escapeshellarg($fileIn) . " " . ($extraInputFilters ? "| $extraInputFilters" : "") . " " . ($scaleParameters ? "| pnmscale $scaleString " : "") . "| $filter " .
+      "> " . escapeshellarg($fileOut) . " " .
+      ") 2> /dev/null";
+    // sfContext::getInstance()->getLogger()->info("$cmd");
+    system($cmd, $result);
+    if ($result != 0) 
+    {
+      return false;
+    }
+    return true;
+  }
+  
+  static private function scaleGd($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75)
+  {
+    // gd version for those who can't install netpbm, poor buggers
+    // "handles" PDF by rendering a blank white image. We already superimpose a PDF icon,
+    // so this should work well 
+    
+    // (if you can install ghostview, you can install netpbm too, so there's no middle case)
+    
+    // Special case to emit the original. This preserves transparency in GIFs and is faster for everything. (PNGs can always preserve 
+    // alpha channel in anything under 1024x768 or when gd is the only backend enabled.) WARNING: keep this up to date if new
+    // capabilities are added - we need to make sure they are not active etc. before using this trick. TODO: check for this in
+    // netpbm land too, right now in a typical configuration it's not checked over 1024x768    
+    
+    $imageInfo = getimagesize($fileIn);
+    // Don't panic on a PDF, fall through to the fake handler for that.
+    if ($imageInfo)
+    {
+      $width = $imageInfo[0];
+      $height = $imageInfo[1];
+      
+      $infoIn = pathinfo($fileIn);    
+      $infoOut = pathinfo($fileOut);    
+      
+      // Must use == because for some reason imagesize returns floats rather than integers
+      if (((!count($scaleParameters)) || (isset($scaleParameters['xysize']) && $scaleParameters['xysize'][0] == $width && $scaleParameters['xysize'][1] == $height)) && (strtolower($infoIn['extension']) === strtolower($infoOut['extension'])) && (!count($cropParameters)))
+      {
+        copy($fileIn, $fileOut);
+        return true;
+      }
+    }
+    
+    if (preg_match('/\.pdf$/i', $fileIn))
+    {
+      $in = self::createTrueColorAlpha(100, 100);
+      imagefilledrectangle($in, 0, 0, 100, 100, imagecolorallocate($in, 255, 255, 255));
+    } 
+    else
+    {
+      $in = self::imagecreatefromany($fileIn);
+    }
+    
+    if (preg_match("/\.(\w+)$/i", $fileOut, $matches))
+    {
+      $extension = $matches[1];
+      $extension = strtolower($extension);
+    }
+    else
+    {
+      imagedestroy($in);
+      return false;
+    }
+    
+    $top = 0;
+    $left = 0;
+    $width = imagesx($in);
+    $height = imagesy($in);
+    if (count($cropParameters))
+    {
+      if (isset($cropParameters['top']))
+      {
+        $top = $cropParameters['top'];
+      }
+      if (isset($cropParameters['left']))
+      {
+        $left = $cropParameters['left'];
+      }
+      if (isset($cropParameters['width']))
+      {
+        $width = $cropParameters['width'];
+      }
+      if (isset($cropParameters['height']))
+      {
+        $height = $cropParameters['height'];
+      }
+      $cropped = self::createTrueColorAlpha($width, $height);
+      imagealphablending($cropped, false);
+      imagesavealpha($cropped, true);
+      imagecopy($cropped, $in, 0, 0, $left, $top, $width, $height);
+      imagedestroy($in);
+      $in = null;
+    }
+    else
+    {
+      // No cropping, so don't waste time and memory
+      $cropped = $in;
+      $in = null;
+    }
+  
+    if (count($scaleParameters))
+    {
+      $width = imagesx($cropped);
+      $height = imagesy($cropped);
+      $swidth = $width;
+      $sheight = $height;
+      if (isset($scaleParameters['xsize']))
+      {
+        $height = $scaleParameters['xsize'] * imagesy($cropped) / imagesx($cropped);
+        $width = $scaleParameters['xsize'];
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+      elseif (isset($scaleParameters['ysize']))
+      {
+        $width = $scaleParameters['ysize'] * imagesx($cropped) / imagesy($cropped);
+        $height = $scaleParameters['ysize'];
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+      elseif (isset($scaleParameters['scale']))
+      {
+        $width = imagesx($cropped) * $scaleParameters['scale'];
+        $height = imagesy($cropped)* $scaleParameters['scale'];
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+      elseif (isset($scaleParameters['xysize']))
+      {
+        $width = $scaleParameters['xysize'][0];
+        $height = $scaleParameters['xysize'][1];
+<<<<<<< .working
+        
+=======
+        // This was backwards until 05/31/2010, making things bigger rather than smaller if their
+        // aspect ratios differed from the original. Be consistent with netpbm which makes things
+        // smaller not bigger
+>>>>>>> .merge-right.r1916
+        if (($width / $height) > ($swidth / $sheight))
+        {
+          // Wider than the original. So it will be narrower than requested
+          $width = ceil($height * ($swidth / $sheight));
+        }
+        else
+        {
+          // Taller than the original. So it will be shorter than requested
+          $height = ceil($width * ($sheight / $swidth));
+        }
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, $swidth, $sheight);
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+    }
+    else
+    {
+      // No scaling, don't waste time and memory
+      $out = $cropped;
+      $cropped = null;
+    }
+    
+    $extension = strtolower($infoOut['extension']);
+    if ($extension === 'gif')
+    {
+      imagegif($out, $fileOut);
+    }
+    elseif (($extension === 'jpg') || ($extension === 'jpeg'))
+    {
+      imagejpeg($out, $fileOut, $quality);
+    }
+    elseif ($extension === 'png')
+    {
+      imagepng($out, $fileOut);
+    }
+    else
+    {
+      return false;
+    }
+      
+    imagedestroy($out);
+    $out = null;
+    return true;
+  }
+  
+  // Make sure the new image is capable of being saved with intact alpha channel;
+  // don't composite alpha channel in gd. If a designer uploads an alpha channel image
+  // they must have a reason for doing so
+  static public function createTrueColorAlpha($width, $height)
+  {
+    $im = imagecreatetruecolor($width, $height);
+    imagealphablending($im, false);
+    imagesavealpha($im, true);
+    return $im;
+  }
+  
+  // Retrieves what you really want to know about an image file, PDFs included,
+  // before making calls such as the above based on good information.
+  
+  // Returns as follows:
+  
+  // array('format' => 'file extension: gif, jpg, png or pdf', 'width' => width in pixels, 'height' => height in pixels);
+
+  // $format is the recommended file extension based on the actual file type, not the user's (possibly totally false or absent)
+  // claimed file extension.
+  
+  // If the file does not have a valid header identifying it as one of these types, false is returned.
+  
+  static public function getInfo($file)
+  {
+    $result = array();
+    $in = fopen($file, "rb");
+    $data = fread($in, 4);
+    fclose($in);
+    if ($data === '%PDF')
+    {
+      if (!aImageConverter::supportsInput('pdf'))
+      {
+        // All we can do is confirm the format and allow
+        // download of the original (which, for PDF, is
+        // usually fine)
+        return array('format' => 'pdf');
+      }
+      $result['format'] = 'pdf';
+      $path = sfConfig::get("app_aimageconverter_path", "");
+      if (strlen($path)) {
+        if (!preg_match("/\/$/", $path)) {
+          $path .= "/";
+        }
+      }
+      // Bounding box goes to stderr, not stdout! Charming
+      // 5 second timeout for reading dimensions. Keeps us from getting stuck on
+      // PDFs that just barely work in Adobe but are noncompliant and hang ghostscript.
+      // Read the output one line at a time so we can catch the happy
+      // bounding box message without hanging
+      
+      // Problem: this doesn't work. We regain control but the process won't die for some reason. It helps
+      // with import but for now go with the simpler standard invocation and hope they fix gs
+
+      // $cmd = "(PATH=$path:\$PATH; export PATH; gs -sDEVICE=bbox -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q " . escapeshellarg($file) . " -c quit ) 2>&1";
+      
+      $cmd = "( PATH=$path:\$PATH; export PATH; gs -sDEVICE=bbox -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q " . escapeshellarg($file) . " -c quit & GS=$!; ( sleep 5; kill \$GS ) & TIMEOUT=\$!; wait \$GS; kill \$TIMEOUT ) 2>&1";
+
+      // For some reason system() does not get the same result when killing subshells as I get when executing
+      // $cmd directly. I don't know why this is this the case but it's easily reproduced
+      
+      $script = aFiles::getTemporaryFilename() . '.sh';
+      file_put_contents($script, $cmd);
+      $cmd = "/bin/sh " . escapeshellarg($script);
+      $in = popen($cmd, "r");
+      $data = stream_get_contents($in);
+      pclose($in);
+      // Actual nonfatal errors in the bbox output mean it's not safe to just
+      // read this naively with fscanf, look for the good part
+      if (preg_match("/%%BoundingBox: \d+ \d+ (\d+) (\d+)/", $data, $matches))
+      {
+        $result['width'] = $matches[1];
+        $result['height'] = $matches[2];
+      }
+      if (!isset($result['width']))
+      {
+        // Bad PDF
+        return false;
+      }
+      return $result;
+    }
+    else
+    {
+      $formats = array(
+        IMAGETYPE_JPEG => "jpg",
+        IMAGETYPE_PNG => "png",
+        IMAGETYPE_GIF => "gif"
+      );
+      $data = getimagesize($file);
+      if (count($data) < 3)
+      {
+        return false;
+      }
+      if (!isset($formats[$data[2]]))
+      {
+        return false;
+      }
+      $format = $formats[$data[2]];
+      $result['width'] = $data[0];
+      $result['height'] = $data[1];
+      $result['format'] = $format;
+      return $result;
+    }
+  }
+
+  // Odds and ends missing from gd
+  
+  // As commonly found on the Internets
+
+  static private function imagecreatefromany($filename) 
+  {
+    foreach (array('png', 'jpeg', 'gif', 'bmp', 'ico') as $type) 
+    {
+      $func = 'imagecreatefrom' . $type;
+      if (is_callable($func)) 
+      {
+        $image = @call_user_func($func, $filename);
+        if ($image) return $image;
+      }
+    }
+    return false;
+  }
+  
+  // Can this box handle pdf, png, jpeg (also acdepts jpg), gif, bmp, ico...
+
+  // Mainly used to check for PDF support.
+  
+  // NOTE: this call is a performance hit, especially with netpbm and ghostscript available.
+  // So we cache the result for 5 minutes. Keep that in mind if you make configuration changes, install
+  // ghostscript, etc. and don't see an immediate difference.
+
+  static public function supportsInput($extension)
+  {
+    $hint = aImageConverter::getHint("input:$extension");
+    if (!is_null($hint))
+    {
+      return $hint;
+    }
+    
+    $result = false;
+    if (sfConfig::get('app_aimageconverter_netpbm', true))
+    {
+      if (aImageConverter::supportsInputNetpbm($extension))
+      {
+        $result = true;
+      }
+    }
+    if (!$result)
+    {
+      $result = aImageConverter::supportsInputGd($extension);
+    }
+    aImageConverter::setHint("input:$extension", $result);
+    return $result;
+  }
+
+  static public function supportsInputNetpbm($extension)
+  {
+    $types = array('gif' => 'gif', 'png' => 'png', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'bmp' => 'bmp', 'ico' => 'ico');
+    $path = sfConfig::get("app_aimageconverter_path", "");
+    if (strlen($path)) {
+      if (!preg_match("/\/$/", $path)) {
+        $path .= "/";
+      }
+    }
+    if ($extension === 'pdf')
+    {
+      $cmd = 'gs';
+    }
+    elseif (!isset($types[$extension]))
+    {
+      if (!preg_match('/^\w+$/', $extension))
+      {
+        return false;
+      }
+      $cmd = $extension . 'topnm';
+    }
+    else
+    {
+      $cmd = $types[$extension] . 'topnm';
+    }
+    $in = popen("(PATH=$path:\$PATH; export PATH; which $cmd)", "r");
+    $result = stream_get_contents($in);
+    pclose($in);
+    if (strlen($result))
+    {
+      return true;
+    }
+    return false;
+  }
+  
+  static public function supportsInputGd($extension)
+  {
+    $types = array('gif' => 'gif', 'png' => 'png', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'bmp' => 'bmp', 'ico' => 'ico');
+    if (!isset($types[$extension]))
+    {
+      return false;
+    }
+    $f = 'imagecreatefrom' . $types[$extension];
+    return is_callable($f);
+  }
+  
+  static public function getHint($hint)
+  {
+    $cache = aImageConverter::getHintCache();
+    $key = 'apostrophe:imageconverter:' . $hint;
+    return $cache->get($key, null);
+  }
+  
+  static public function setHint($hint, $value)
+  {
+    $cache = aImageConverter::getHintCache();
+    // The lifetime should be short to avoid annoying developers who are
+    // trying to fix their configuration and test with new possibilities
+    $key = 'apostrophe:imageconverter:' . $hint;
+    $cache->set($key, $value, 300);
+  }
+  static public function getHintCache()
+  {
+    $cacheClass = sfConfig::get('app_a_hint_cache_class', 'sfFileCache');
+    $cache = new $cacheClass(sfConfig::get('app_a_hint_cache_options', array('cache_dir' => aFiles::getWritableDataFolder(array('a_hint_cache')))));
+    return $cache;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverter.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverter.class.php	(revision 3191)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aImageConverter.class.php	(revision 3191)
@@ -0,0 +1,876 @@
+<?php
+
+/*
+ *
+ * Efficient image conversions using netpbm or (if netpbm is not available) gd.
+ * For more information see the README file.
+ *
+ */ 
+
+class aImageConverter 
+{
+  // Produces images suitable for intentional cropping by CSS.
+  // Either the width or the height will match the request; the other
+  // will EXCEED the request. Looks nicer than letterboxing in cases
+  // where keeping the entire picture is not essential.
+
+  static public function scaleToNarrowerAxis($fileIn, $fileOut, $width, $height, $quality = 75)
+  {
+    $width = ceil($width);
+    $height = ceil($height);
+    $quality = ceil($quality);
+    list($iwidth, $iheight) = getimagesize($fileIn); 
+    if (!$iwidth) {
+      return false;
+    }
+    $iratio = $iwidth / $iheight;
+    $ratio = $width / $height;
+    if ($iratio > $ratio) {
+      $width = false;
+    } else {
+      $height = false;
+    }
+    return self::scaleToFit($fileIn, $fileOut, $width, $height, $quality);
+  }
+
+  static public function scaleToFit($fileIn, $fileOut, $width, $height, $quality = 75)
+  {
+    if ($width === false) {
+      $scaleParameters = array('ysize' => $height + 0);
+    } elseif ($height === false) {
+      $scaleParameters = array('xsize' => $width + 0);
+    } else {
+      $scaleParameters = array('xysize' => array($width + 0, $height + 0));
+    }
+    $result = self::scaleBody($fileIn, $fileOut, $scaleParameters, array(), $quality);
+    return $result;
+  }
+
+  static public function scaleByFactor($fileIn, $fileOut, $factor, 
+    $quality = 75)
+  {
+    $quality = ceil($quality);
+    $scaleParameters = array('scale' => $factor + 0);  
+    return self::scaleBody($fileIn, $fileOut, $scaleParameters, array(), $quality);
+  }
+
+  // $width and $height are the dimensions of the final rendered image. $quality is the JPEG quality setting (where needed).
+  // The $crop parameters, when not null (all four must be null or not null), are used to crop the original before scaling/distorting 
+  // to the specified width and height and are always in the original image's coordinates.
+  
+  // If cropping coordinates are not specified, the largest possible portion of the center of the original image is scaled to fit into the 
+  // destination image without distortion
+  
+  static public function cropOriginal($fileIn, $fileOut, $width, $height, $quality = 75, $cropLeft = null, $cropTop = null, $cropWidth = null,  $cropHeight = null)
+  {
+    // Allow skipping of parameters
+    if (is_null($quality))
+    {
+      $quality = 75;
+    }
+    $width = ceil($width);
+    $height = ceil($height);
+    $quality = ceil($quality);
+    list($iwidth, $iheight) = getimagesize($fileIn); 
+    if (!$iwidth) 
+    {
+      return false;
+    }
+    $iratio = $iwidth / $iheight;
+    $ratio = $width / $height;
+
+     // Spike's contribution: arbitrary cropping
+     if (!is_null($cropWidth) && !is_null($cropHeight) && !is_null($cropLeft) && !is_null($cropTop))
+     {
+       $cropTop = ceil($cropTop + 0);
+       $cropLeft = ceil($cropLeft + 0);
+       $cropWidth = ceil($cropWidth + 0);
+       $cropHeight = ceil($cropHeight + 0);
+       
+       $scale = array('xysize' => array($width + 0, $height + 0));
+       $crop = array('left' => $cropLeft, 'top' => $cropTop, 'width' => $cropWidth, 'height' => $cropHeight);
+       return self::scaleBody($fileIn, $fileOut, $scale, $crop, $quality);
+     }
+
+    $scale = array('xysize' => array($width + 0, $height + 0));
+    if ($iratio < $ratio)
+    {
+      $cropHeight = floor($iwidth * ($height / $width));
+      $cropTop = floor(($iheight - $cropHeight) / 2);
+      $cropLeft = 0;
+      $cropWidth = $iwidth;
+    }
+    else
+    {
+      $cropWidth = floor($iheight * $ratio);
+      $cropLeft = floor(($iwidth - $cropWidth) / 2);
+      $cropTop = 0;
+      $cropHeight = $iheight;
+    }
+    $scale = array('xysize' => array($width + 0, $height + 0));
+    $crop = array('left' => $cropLeft, 'top' => $cropTop, 'width' => $cropWidth, 'height' => $cropHeight);
+    return self::scaleBody($fileIn, $fileOut, $scale, $crop, $quality);
+  }
+
+  // Change the format without cropping or scaling
+  static public function convertFormat($fileIn, $fileOut, $quality = 75)
+  {
+    $quality = ceil($quality);
+    return self::scaleBody($fileIn, $fileOut, false, false, $quality);
+  }
+
+  static private function scaleBody($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75) 
+  {    
+    if (sfConfig::get('app_aimageconverter_netpbm', true))
+    {
+      // Auto fallback to gd, but only if it's not a small image gd can handle better (1.4). This means we get
+      // full alpha channel for manageably-sized PNGs and good performance for huge PNGs
+      $info = getimagesize($fileIn);
+      $mapTypes = array(IMAGETYPE_GIF => IMG_GIF, IMAGETYPE_PNG => IMG_PNG, IMAGETYPE_JPEG => IMG_JPG);
+      // Usually the 1024x768 rule is better, but this is useful for testing
+      if (sfConfig::get('app_aimageconverter_netpbm', true) === 'always')
+      {
+        return self::scaleNetpbm($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+      }
+      // If we got valid image info, the image size is less than 1024x768, gd is enabled, and gd supports
+      // the image type... *then* we skip to gd.
+      if (($info !== false) && (($info[0] <= 1024) && ($info[1] <= 768)) && function_exists('imagetypes') && isset($mapTypes[$info[2]]) && (imagetypes() & $mapTypes[$info[2]]))
+      {
+        return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+      }
+      $result = self::scaleNetpbm($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+      if (!$result)
+      {
+        return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+      }
+    }
+    else
+    {
+      return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
+    }
+  }
+  
+  // Get the JPEG EXIF rotation. Always returns 1 (no rotation) for other formats.
+  // Other values:
+  // case 2: // horizontal flip
+  // case 3: // 180 rotate left
+  // case 4: // vertical flip
+  // case 5: // vertical flip + 90 rotate right
+  // case 6: // 90 rotate right
+  // case 7: // horizontal flip + 90 rotate right
+  // case 8:    // 90 rotate left
+
+  static public function getRotation($file, $getimagesize = null)
+  {
+    if (is_null($getimagesize))
+    {
+      $getimagesize = getimagesize($file);
+    }
+    if ($getimagesize[2] !== IMAGETYPE_JPEG)
+    {
+      return 1;
+    }
+    if (!extension_loaded("exif"))
+    {
+      // We can't tell
+      return 1;
+    }
+    // exif_read_data is noisy if it encounters Adobe XMP instead of EXIF in the app0 marker
+    $exif = @exif_read_data($file);
+    if (!$exif)
+    {
+      return 1;
+    }
+    if (isset($exif['IFD0']['Orientation']))
+    {
+      // Code I'm seeing does this
+      $ort = $exif['IFD0']['Orientation'];
+    } elseif (isset($exif['Orientation']))
+    {
+      // Files I'm seeing do this
+      $ort = $exif['Orientation'];
+    }
+    else
+    {
+      $ort = 1;
+    }
+    return $ort;
+  }
+  
+  static private function scaleNetpbm($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75)
+  {
+    $outputFilters = array(
+      "jpg" => "pnmtojpeg --quality %d",
+      "jpeg" => "pnmtojpeg --quality %d",
+      "ppm" => "cat",
+      "pbm" => "cat",
+      "pgm" => "cat",
+      "tiff" => "pnmtotiff",
+      "png" => "pnmtopng",
+      "gif" => "ppmquant 256 | ppmtogif",
+      "bmp" => "ppmtobmp"
+    );
+    if (preg_match("/\.(\w+)$/", $fileOut, $matches)) {
+      $extension = $matches[1];
+      $extension = strtolower($extension);
+      if (!isset($outputFilters[$extension])) {
+        return false;
+      }
+      $filter = sprintf($outputFilters[$extension], $quality);
+    } else {
+      return false;
+    }
+    $path = sfConfig::get("app_aimageconverter_path", "");
+    if (strlen($path)) {
+      if (!preg_match("/\/$/", $path)) {
+        $path .= "/";
+      }
+    }
+        
+    // AUGH: some versions of anytopnm don't have
+    // the brains to look at the file signature. We need
+    // to be compatible with this brain damage, so pick
+    // the right filter based on the results of getimagesize()
+    // and punt to anytopnm only if we can't figure it out.
+    
+    // While we're at it: detect PDF by magic number too,
+    // not by extension, that's tacky
+
+    $input = 'anytopnm';
+    
+    $in = fopen($fileIn, 'r');
+    $bytes = fread($in, 4);
+    if ($bytes === '%PDF')
+    {
+      $input = 'gs -sDEVICE=ppm -sOutputFile=- ' .
+        ' -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q -';
+    }
+    fclose($in);
+    
+    $info = getimagesize($fileIn);
+    if ($info !== false)
+    {
+      $type = $info[2];
+      if ($type === IMAGETYPE_GIF)
+      {
+        $input = 'giftopnm';
+      } 
+      elseif ($type === IMAGETYPE_PNG)
+      {
+        $input = 'pngtopnm';
+      }
+      elseif ($type === IMAGETYPE_JPEG)
+      {
+        $input = 'jpegtopnm';
+      }
+    }
+    
+    $rotate = '';
+    
+    $rotation = aImageConverter::getRotation($fileIn, $info);
+    switch ($rotation)
+    {
+        case 1: // nothing
+        $rotate = '';
+        break;
+
+        case 2: // horizontal flip
+        $rotate = '| pamflip -leftright ';
+        break;
+
+        case 3: // 180 rotate left
+        $rotate = '| pamflip -rotate180 ';
+        break;
+
+        case 4: // vertical flip
+        $rotate = '| pamflip -topbottom ';
+        break;
+
+        case 5: // vertical flip + 90 rotate right
+        $rotate = '| pamflip -topbottom | pamflip -cw ';
+        break;
+
+        case 6: // 90 rotate right
+        $rotate = '| pamflip -cw ';
+        break;
+
+        case 7: // horizontal flip + 90 rotate right
+        $rotate = '| pamflip -leftright | pamflip -cw ';
+        break;
+
+        case 8:    // 90 rotate left
+        $rotate = '| pamflip -ccw ';
+        break;
+    }
+    
+  
+    $scaleString = '';
+    $extraInputFilters = '';
+    foreach ($scaleParameters as $key => $values)
+    {
+      $scaleString .= " -$key ";
+      if (is_array($values))
+      {
+        foreach ($values as $value)
+        {
+          $value = ceil($value);
+          $scaleString .= " $value";
+        }
+      }
+      else
+      {
+        $values = ceil($values);
+        $scaleString .= " $values";
+      }
+    }
+    if (count($cropParameters))
+    {
+      $extraInputFilters = 'pnmcut ';
+      foreach ($cropParameters as $ckey => $cvalue)
+      {
+        $cvalue = ceil($cvalue);
+        $extraInputFilters .= " -$ckey $cvalue";
+      }
+    }
+    
+    $cmd = "(PATH=$path:\$PATH; export PATH; $input < " . escapeshellarg($fileIn) . ' ' . $rotate . ' ' . ($extraInputFilters ? "| $extraInputFilters" : "") . " " . ($scaleParameters ? "| pnmscale $scaleString " : "") . "| $filter " .
+      "> " . escapeshellarg($fileOut) . " " .
+      ") 2> /dev/null";
+    // sfContext::getInstance()->getLogger()->info("$cmd");
+    system($cmd, $result);
+    if ($result != 0) 
+    {
+      return false;
+    }
+    return true;
+  }
+  
+  static private function scaleGd($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75)
+  {
+    
+    // gd version for those who can't install netpbm, poor buggers
+    // "handles" PDF by rendering a blank white image. We already superimpose a PDF icon,
+    // so this should work well 
+    
+    // (if you can install ghostview, you can install netpbm too, so there's no middle case)
+    
+    // Special case to emit the original. This preserves transparency in GIFs and is faster for everything. (PNGs can always preserve 
+    // alpha channel in anything under 1024x768 or when gd is the only backend enabled.) WARNING: keep this up to date if new
+    // capabilities are added - we need to make sure they are not active etc. before using this trick. TODO: check for this in
+    // netpbm land too, right now in a typical configuration it's not checked over 1024x768    
+    
+    $imageInfo = getimagesize($fileIn);
+    // Don't panic on a PDF, fall through to the fake handler for that.
+    if ($imageInfo)
+    {
+      $width = $imageInfo[0];
+      $height = $imageInfo[1];
+      $orientation = aImageConverter::getRotation($fileIn, $imageInfo);
+      if ($imageInfo[2] === IMAGETYPE_JPEG)
+      {
+        // Some EXIF orientations swap width and height
+        switch ($orientation)
+        {
+          case 5: // vertical flip + 90 rotate right
+          case 6: // 90 rotate right
+          case 7: // horizontal flip + 90 rotate right
+          case 8:    // 90 rotate left
+          $tmp = $width;
+          $width = $height;
+          $height = $tmp;
+          break;
+        }
+      }
+      
+      $infoIn = pathinfo($fileIn);    
+      $infoOut = pathinfo($fileOut);    
+      
+      // Try not to do any work if we are not changing anything
+      if ($orientation == 1)
+      {
+        if (((!count($scaleParameters)) || (isset($scaleParameters['xysize']) && $scaleParameters['xysize'][0] == $width && $scaleParameters['xysize'][1] == $height)) && (strtolower($infoIn['extension']) === strtolower($infoOut['extension'])) && (!count($cropParameters)))
+        {
+          copy($fileIn, $fileOut);
+          return true;
+        }
+      }
+    }
+    
+    if (preg_match('/\.pdf$/i', $fileIn))
+    {
+      $in = self::createTrueColorAlpha(100, 100);
+      imagefilledrectangle($in, 0, 0, 100, 100, imagecolorallocate($in, 255, 255, 255));
+    } 
+    else
+    {
+      $in = self::imagecreatefromany($fileIn);
+      if ($orientation != 1)
+      {
+        // Note that gd rotation is CCL
+        
+        switch ($orientation)
+        {
+          case 2: // horizontal flip
+          aImageConverter::horizontalFlip($in);
+          break;
+
+          case 3: // 180 rotate left
+          $in2 = imagerotate($in, 180, imagecolorallocate($in, 255, 255, 255));
+          imagedestroy($in);
+          $in = $in2;
+          break;
+
+          case 4: // vertical flip
+          aImageConverter::verticalFlip($in);
+          break;
+
+          case 5: // vertical flip + 90 rotate right
+          aImageConverter::verticalFlip($in);
+          $in2 = imagerotate($in, 270, imagecolorallocate($in, 255, 255, 255));
+          imagedestroy($in);
+          $in = $in2;
+          break;
+
+          case 6: // 90 rotate right
+          $in2 = imagerotate($in, 270, imagecolorallocate($in, 255, 255, 255));
+          imagedestroy($in);
+          $in = $in2;
+          break;
+
+          case 7: // horizontal flip + 90 rotate right
+          aImageConverter::horizontalFlip($in);
+          $in2 = imagerotate($in, 270, imagecolorallocate($in, 255, 255, 255));
+          imagedestroy($in);
+          $in = $in2;
+          break;
+
+          case 8:    // 90 rotate left
+          $in2 = imagerotate($in, 90, imagecolorallocate($in, 255, 255, 255));
+          imagedestroy($in);
+          $in = $in2;
+          break;
+        }
+      }
+    }
+    
+    if (!$in)
+    {
+      return false;
+    }
+    
+    if (preg_match("/\.(\w+)$/i", $fileOut, $matches))
+    {
+      $extension = $matches[1];
+      $extension = strtolower($extension);
+    }
+    else
+    {
+      imagedestroy($in);
+      return false;
+    }
+    
+    $top = 0;
+    $left = 0;
+    $width = imagesx($in);
+    $height = imagesy($in);
+    if (count($cropParameters))
+    {
+      if (isset($cropParameters['top']))
+      {
+        $top = $cropParameters['top'];
+      }
+      if (isset($cropParameters['left']))
+      {
+        $left = $cropParameters['left'];
+      }
+      if (isset($cropParameters['width']))
+      {
+        $width = $cropParameters['width'];
+      }
+      if (isset($cropParameters['height']))
+      {
+        $height = $cropParameters['height'];
+      }
+      $cropped = self::createTrueColorAlpha($width, $height);
+      imagealphablending($cropped, false);
+      imagesavealpha($cropped, true);
+      imagecopy($cropped, $in, 0, 0, $left, $top, $width, $height);
+      imagedestroy($in);
+      $in = null;
+    }
+    else
+    {
+      // No cropping, so don't waste time and memory
+      $cropped = $in;
+      $in = null;
+    }
+  
+    if (count($scaleParameters))
+    {
+      $width = imagesx($cropped);
+      $height = imagesy($cropped);
+      $swidth = $width;
+      $sheight = $height;
+      if (isset($scaleParameters['xsize']))
+      {
+        $height = $scaleParameters['xsize'] * imagesy($cropped) / imagesx($cropped);
+        $width = $scaleParameters['xsize'];
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+      elseif (isset($scaleParameters['ysize']))
+      {
+        $width = $scaleParameters['ysize'] * imagesx($cropped) / imagesy($cropped);
+        $height = $scaleParameters['ysize'];
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+      elseif (isset($scaleParameters['scale']))
+      {
+        $width = imagesx($cropped) * $scaleParameters['scale'];
+        $height = imagesy($cropped)* $scaleParameters['scale'];
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+      elseif (isset($scaleParameters['xysize']))
+      {
+        $width = $scaleParameters['xysize'][0];
+        $height = $scaleParameters['xysize'][1];
+        // This was backwards until 05/31/2010, making things bigger rather than smaller if their
+        // aspect ratios differed from the original. Be consistent with netpbm which makes things
+        // smaller not bigger
+        if (($width / $height) > ($swidth / $sheight))
+        {
+          // Wider than the original. So it will be narrower than requested
+          $width = ceil($height * ($swidth / $sheight));
+        }
+        else
+        {
+          // Taller than the original. So it will be shorter than requested
+          $height = ceil($width * ($sheight / $swidth));
+        }
+        $out = self::createTrueColorAlpha($width, $height);
+        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, $swidth, $sheight);
+        imagedestroy($cropped);
+        $cropped = null;
+      }
+    }
+    else
+    {
+      // No scaling, don't waste time and memory
+      $out = $cropped;
+      $cropped = null;
+    }
+    
+    $extension = strtolower($infoOut['extension']);
+    if ($extension === 'gif')
+    {
+      imagegif($out, $fileOut);
+    }
+    elseif (($extension === 'jpg') || ($extension === 'jpeg'))
+    {
+      imagejpeg($out, $fileOut, $quality);
+    }
+    elseif ($extension === 'png')
+    {
+      imagepng($out, $fileOut);
+    }
+    else
+    {
+      return false;
+    }
+      
+    imagedestroy($out);
+    $out = null;
+    return true;
+  }
+  
+  // Flips the image in place
+  static protected function horizontalFlip($in)
+  {
+    $tmp = self::imageCreateTrueColor(1, $height);
+    for ($x = 0; ($x < ($width >> 1)); $x++)
+    {
+      imagecopy($tmp, $in, 0, 0, $x, 0, 1, $height);
+      imagecopy($in, $in, $x, 0, ($width - $x) - 1, 0, 1, $height);
+      imagecopy($in, $tmp, ($width - $x) - 1, 0, 0, 0, 1, $height);
+    }
+    imagedestroy($tmp);
+  }
+  
+  // Flips the image in place
+  static protected function verticalFlip($in)
+  {
+    $tmp = self::imageCreateTrueColor($width, 1);
+    for ($y = 0; ($y < ($height >> 1)); $y++)
+    {
+      imagecopy($tmp, $in, 0, 0, 0, $y, $width, 1);
+      imagecopy($in, $in, 0, $y, 0, ($height - $y) - 1, $width, 1);
+      imagecopy($in, $tmp, 0, ($height - $y) - 1, 0, 0, $width, 1);
+    }
+    imagedestroy($tmp);
+  }
+  
+  // Make sure the new image is capable of being saved with intact alpha channel;
+  // don't composite alpha channel in gd. If a designer uploads an alpha channel image
+  // they must have a reason for doing so
+  static public function createTrueColorAlpha($width, $height)
+  {
+    $im = imagecreatetruecolor($width, $height);
+    imagealphablending($im, false);
+    imagesavealpha($im, true);
+    return $im;
+  }
+  
+  // Retrieves what you really want to know about an image file, PDFs included,
+  // before making calls such as the above based on good information.
+  
+  // Returns as follows:
+  
+  // array('format' => 'file extension: gif, jpg, png or pdf', 'width' => width in pixels, 'height' => height in pixels);
+
+  // $format is the recommended file extension based on the actual file type, not the user's (possibly totally false or absent)
+  // claimed file extension.
+  
+  // If the file does not have a valid header identifying it as one of these types, false is returned.
+  
+  // If the 'format-only' option is true, only the format field is returned. This is much faster if the
+  // file is a PDF.
+  
+  static public function getInfo($file, $options = array())
+  {
+    $formatOnly = (isset($options['format-only']) && $options['format-only']);
+    $result = array();
+    $in = fopen($file, "rb");
+    $data = fread($in, 4);
+    fclose($in);
+    
+    
+    if ($data === '%PDF')
+    {
+      // format-only 
+      if ((!aImageConverter::supportsInput('pdf')) || $formatOnly)
+      {
+        // All we can do is confirm the format and allow
+        // download of the original (which, for PDF, is
+        // usually fine)
+        return array('format' => 'pdf');
+      }
+      $result['format'] = 'pdf';
+      $path = sfConfig::get("app_aimageconverter_path", "");
+      if (strlen($path)) {
+        if (!preg_match("/\/$/", $path)) {
+          $path .= "/";
+        }
+      }
+      // Bounding box goes to stderr, not stdout! Charming
+      // 5 second timeout for reading dimensions. Keeps us from getting stuck on
+      // PDFs that just barely work in Adobe but are noncompliant and hang ghostscript.
+      // Read the output one line at a time so we can catch the happy
+      // bounding box message without hanging
+      
+      // Problem: this doesn't work. We regain control but the process won't die for some reason. It helps
+      // with import but for now go with the simpler standard invocation and hope they fix gs
+
+      // $cmd = "(PATH=$path:\$PATH; export PATH; gs -sDEVICE=bbox -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q " . escapeshellarg($file) . " -c quit ) 2>&1";
+      
+      $cmd = "( PATH=$path:\$PATH; export PATH; gs -sDEVICE=bbox -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q " . escapeshellarg($file) . " -c quit & GS=$!; ( sleep 5; kill \$GS ) & TIMEOUT=\$!; wait \$GS; kill \$TIMEOUT ) 2>&1";
+
+      // For some reason system() does not get the same result when killing subshells as I get when executing
+      // $cmd directly. I don't know why this is this the case but it's easily reproduced
+      
+      $script = aFiles::getTemporaryFilename() . '.sh';
+      file_put_contents($script, $cmd);
+      $cmd = "/bin/sh " . escapeshellarg($script);
+      $in = popen($cmd, "r");
+      $data = stream_get_contents($in);
+      pclose($in);
+      // Actual nonfatal errors in the bbox output mean it's not safe to just
+      // read this naively with fscanf, look for the good part
+      if (preg_match("/%%BoundingBox: \d+ \d+ (\d+) (\d+)/", $data, $matches))
+      {
+        $result['width'] = $matches[1];
+        $result['height'] = $matches[2];
+      }
+      if (!isset($result['width']))
+      {
+        // Bad PDF
+        return false;
+      }
+      return $result;
+    }
+    else
+    {
+      $formats = array(
+        IMAGETYPE_JPEG => "jpg",
+        IMAGETYPE_PNG => "png",
+        IMAGETYPE_GIF => "gif"
+      );
+      $data = getimagesize($file);
+      if (count($data) < 3)
+      {
+        return false;
+      }
+      if (!isset($formats[$data[2]]))
+      {
+        return false;
+      }
+      $format = $formats[$data[2]];
+      $result['format'] = $format;
+      if ($formatOnly)
+      {
+        return $result;
+      }
+      $result['width'] = $data[0];
+      $result['height'] = $data[1];
+      if ($format === 'jpg')
+      {
+        // Some EXIF orientations swap width and height
+        switch (aImageConverter::getRotation($file, $data))
+        {
+          case 5: // vertical flip + 90 rotate right
+          case 6: // 90 rotate right
+          case 7: // horizontal flip + 90 rotate right
+          case 8:    // 90 rotate left
+          $result['width'] = $data[1];
+          $result['height'] = $data[0];
+          break;
+        }
+      }
+      return $result;
+    }
+  }
+
+  // Odds and ends missing from gd
+  
+  // As commonly found on the Internets
+
+  static private function imagecreatefromany($filename) 
+  {
+    foreach (array('png', 'jpeg', 'gif', 'bmp', 'ico') as $type) 
+    {
+      $func = 'imagecreatefrom' . $type;
+      if (is_callable($func)) 
+      {
+        $image = @call_user_func($func, $filename);
+        if ($image) return $image;
+      }
+    }
+    return false;
+  }
+  
+  // Can this box handle pdf, png, jpeg (also acdepts jpg), gif, bmp, ico...
+
+  // Mainly used to check for PDF support.
+  
+  // NOTE: this call is a performance hit, especially with netpbm and ghostscript available.
+  // So we cache the result for 5 minutes. Keep that in mind if you make configuration changes, install
+  // ghostscript, etc. and don't see an immediate difference.
+
+  static public function supportsInput($extension)
+  {
+    $hint = aImageConverter::getHint("input:$extension");
+    if (!is_null($hint))
+    {
+      return $hint;
+    }
+    
+    $result = false;
+    if (sfConfig::get('app_aimageconverter_netpbm', true))
+    {
+      if (aImageConverter::supportsInputNetpbm($extension))
+      {
+        $result = true;
+      }
+    }
+    if (!$result)
+    {
+      $result = aImageConverter::supportsInputGd($extension);
+    }
+    aImageConverter::setHint("input:$extension", $result);
+    return $result;
+  }
+
+  static public function supportsInputNetpbm($extension)
+  {
+    $types = array('gif' => 'gif', 'png' => 'png', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'bmp' => 'bmp', 'ico' => 'ico');
+    $path = sfConfig::get("app_aimageconverter_path", "");
+    if (strlen($path)) {
+      if (!preg_match("/\/$/", $path)) {
+        $path .= "/";
+      }
+    }
+    if ($extension === 'pdf')
+    {
+      // DEPRECATED. GhostScript just isn't reliable enough. It rejects too many valid
+      // PDFs which is a much bigger issue than lack of preview. See #558
+      if (sfConfig::get('app_a_pdf_preview', false))
+      {
+        $cmd = 'gs';
+      }
+      else
+      {
+        return false;
+      }
+    }
+    elseif (!isset($types[$extension]))
+    {
+      if (!preg_match('/^\w+$/', $extension))
+      {
+        return false;
+      }
+      $cmd = $extension . 'topnm';
+    }
+    else
+    {
+      $cmd = $types[$extension] . 'topnm';
+    }
+    $in = popen("(PATH=$path:\$PATH; export PATH; which $cmd)", "r");
+    $result = stream_get_contents($in);
+    pclose($in);
+    if (strlen($result))
+    {
+      return true;
+    }
+    return false;
+  }
+  
+  static public function supportsInputGd($extension)
+  {
+    $types = array('gif' => 'gif', 'png' => 'png', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'bmp' => 'bmp', 'ico' => 'ico');
+    if (!isset($types[$extension]))
+    {
+      return false;
+    }
+    $f = 'imagecreatefrom' . $types[$extension];
+    return is_callable($f);
+  }
+  
+  static public function getHint($hint)
+  {
+    $cache = aImageConverter::getHintCache();
+    $key = 'apostrophe:imageconverter:' . $hint;
+    return $cache->get($key, null);
+  }
+  
+  static public function setHint($hint, $value)
+  {
+    $cache = aImageConverter::getHintCache();
+    // The lifetime should be short to avoid annoying developers who are
+    // trying to fix their configuration and test with new possibilities
+    $key = 'apostrophe:imageconverter:' . $hint;
+    $cache->set($key, $value, 300);
+  }
+  static public function getHintCache()
+  {
+    $cacheClass = sfConfig::get('app_a_hint_cache_class', 'sfFileCache');
+    $cache = new $cacheClass(sfConfig::get('app_a_hint_cache_options', array('cache_dir' => aFiles::getWritableDataFolder(array('a_hint_cache')))));
+    return $cache;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aToolkitEvents.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aToolkitEvents.class.php	(revision 1971)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aToolkitEvents.class.php	(revision 1971)
@@ -0,0 +1,30 @@
+<?php
+
+class aToolkitEvents
+{
+  // command.post_command
+  static public function listenToCommandPostCommandEvent(sfEvent $event)
+  {
+    $task = $event->getSubject();
+    if ($task->getFullName() === 'project:permissions')
+    {
+      $writable = aFiles::getWritableDataFolder();
+      $task->getFilesystem()->chmod($writable, 0777);
+      $dirFinder = sfFinder::type('dir');
+      $fileFinder = sfFinder::type('file');
+      $task->getFilesystem()->chmod($dirFinder->in($writable), 0777);
+      $task->getFilesystem()->chmod($fileFinder->in($writable), 0666);
+    }
+    if ($task->getFullName() === 'cache:clear')
+    {
+      $dir = aFiles::getUploadFolder(array('asset-cache'));
+      $files = glob("$dir/*");
+      foreach ($files as $file)
+      {
+        echo("Unlinked CSS/JS cache file $file\n");
+        unlink($file);
+      }
+    }
+  }
+}
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDate.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDate.class.php	(revision 2802)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDate.class.php	(revision 2802)
@@ -0,0 +1,214 @@
+<?php
+
+class aDate
+{
+  // All date formatters here accept both Unix timestamps and MySQL date or datetime values.
+  // These methods output only the date, not the time (except for aDate::time()). Use these methods
+  // for consistency within and across our applications.
+  
+  // By default we now use the I18N format_date helper. But we can override this when we
+  // want our favorite English date treatment for an English-only site
+  
+  // Most compact: Sep 3 (2-digit year follows but only if not current year)
+  
+  static public function pretty($date)
+  {
+    $date = self::normalize($date);
+    if (!sfConfig::get('app_a_pretty_english_dates', false))
+    {
+      sfContext::getInstance()->getConfiguration()->loadHelpers('Date');
+      // Short date format is the closest I18N format to our proprietary version
+      return format_date($date, 'd');
+    }
+    $month = date('F', $date);
+    $day = date('j', $date);
+    $month = substr($month, 0, 3);
+    $year = date('Y', $date);
+    $yearNow = date('Y');
+    $result = "$month $day";
+    if ($year != $yearNow)
+    {
+      // Switch to 2 digit year for compactness. TBB
+      $result .= " '" . substr($year, 2);
+    }
+    return $result;
+  }
+  
+  // Saturday, 14 January 2009
+  
+  static public function long($date)
+  {
+    $date = self::normalize($date);
+    if (!sfConfig::get('app_a_pretty_english_dates', false))
+    {
+      sfContext::getInstance()->getConfiguration()->loadHelpers('Date');
+      // Long date format is the closest I18N format to our proprietary version
+      return format_date($date, 'D');
+    }
+    return date('l, j F Y', $date);
+  }
+
+  // Sat, 14 Jan 2009
+
+  static public function medium($date)
+  {
+    $date = self::normalize($date);
+    if (!sfConfig::get('app_a_pretty_english_dates', false))
+    {
+      sfContext::getInstance()->getConfiguration()->loadHelpers('Date');
+      // Long date format is the closest I18N format to our proprietary version
+      return format_date($date, 'p');
+    }
+    return date('D, M j Y', $date);
+  }
+
+  // 9/4/09 4PM
+
+  static public function short($date)
+  {
+    $date = self::normalize($date);
+    if (!sfConfig::get('app_a_pretty_english_dates', false))
+    {
+      sfContext::getInstance()->getConfiguration()->loadHelpers('Date');
+      // Short date format is the closest I18N format to our proprietary version
+      return format_date($date, 'd');
+    }
+    return date('n/j/y', $date);
+  }
+  
+  static public function date($date, $format)
+  {
+    if (!in_array($format, array('pretty', 'short', 'medium', 'long')))
+    {
+      throw new Exception("Unknown or missing date format: $format\n");
+    }
+    return self::$format($date);
+  }
+  
+  // IN: date as timestamp OR the following formats:
+  // YYYY-MM-DD 
+  // YYYY-MM-DD hh:mm:ss
+  // hh:mm:ss
+  // hh:mm:ss by itself is interpreted relative to the current day
+  // unless timeOnly is true, in which case you get back only the offset in seconds
+  // from midnight.
+  //
+  // OUT: timestamp
+  static public function normalize($date, $timeOnly = false)
+  {  
+    if (preg_match("/^(\d\d\d\d)-(\d\d)-(\d\d)( (\d\d):(\d\d):(\d\d))?$/", $date, $matches))
+    {
+      if (count($matches) == 4)
+      {
+        list($dummy1, $year, $month, $day) = $matches;
+        $hour = 0;
+        $min = 0;
+        $sec = 0;
+      }
+      else
+      {
+        list($dummy1, $year, $month, $day, $dummy2, $hour, $min, $sec) = $matches;
+      }
+      $date = mktime($hour, $min, $sec, $month, $day, $year);
+    }  
+    elseif (preg_match("/^(\d\d):(\d\d):(\d\d)?$/", $date, $matches))
+    {
+      list($dummy1, $hour, $min, $sec) = $matches;
+      if ($timeOnly)
+      {
+        return $hour * 3600 + $min * 60 + $sec;
+      }
+      $now = time();
+      $year = date('Y', $now);
+      $month = date('n', $now);
+      $day = date('j', $now);
+      $date = mktime($hour, $min, $sec, $month, $day, $year);
+    }
+    return $date;
+  }
+  
+  // By default just calls the I18N format_time. The rest of this description
+  // applies only if you set app_a_pretty_english_dates:
+  
+  // The only variation on our time format is turning on the display
+  // of :00 when the time is a round hour, such as 8PM. Set compact
+  // to false to bring back :00. Otherwise we remove it
+  
+  static public function time($date, $compact = true)
+  {
+    $date = self::normalize($date);
+    if (!sfConfig::get('app_a_pretty_english_dates', false))
+    {
+      sfContext::getInstance()->getConfiguration()->loadHelpers('Date');
+      // Short time format is the closest I18N format to our proprietary version
+      return format_date($date, 't');
+    }
+    
+    $hour = date('g', $date);
+    $min = date('i', $date);
+    $s = $hour;
+    if (($min != 0) || (!$compact))
+    {
+      $s .= ":$min";
+    }
+    $s .= date('A', $date);
+    return $s;
+  }
+
+
+	// 4:15 PM, Thursday
+
+	static public function dayAndTime($date)
+	{
+		$date = self::normalize($date);
+		if (!sfConfig::get('app_a_pretty_english_dates', false))
+    {
+      sfContext::getInstance()->getConfiguration()->loadHelpers('Date');
+      // Short date format is the closest I18N format to our proprietary version
+      return format_date($date, 'd');
+    }
+		$time = self::time($date);
+		$day = date('l', $date);
+		return $time.', '.$day;
+	}
+
+  // January 14, 2009
+  
+  static public function dayMonthYear($date)
+  {
+    $date = self::normalize($date);
+    if (!sfConfig::get('app_a_pretty_english_dates', false))
+    {
+      sfContext::getInstance()->getConfiguration()->loadHelpers('Date');
+      // Long date format is the closest I18N format to our proprietary version
+      return format_date($date, 'D');
+    }
+    return date('M j, Y', $date);
+  }
+  
+  // Subtracts $date2 from $date1 and returns the difference in whole days.
+  // For instance, if $date2 is 2009-09-30 and $date1 is 2009-10-01, the
+  // result will be 1.
+  
+  // Arguments should be Unix timestamps or Doctrine YYYY-MM-DD datestamps
+  // representing midnight on two days. Valid results are not guaranteed if 
+  // the timestamp does not represent midnight at the start of the day
+  static public function differenceDays($date1, $date2)
+  {
+    $date1 = self::normalize($date1);
+    $date2 = self::normalize($date2);
+    // This rounding logic allows for the difference to be less or more than a full day due to
+    // leap seconds and/or daylight savings time
+    return floor(($date1 - $date2) / 86400 + 0.5);
+  }
+  
+  static public function mysql($when = null)
+  {
+    if (is_null($when))
+    {
+      $when = time();
+    }
+    $when = self::normalize($when);
+    return date('Y-m-d H:i:s', $when);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aArray.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aArray.class.php	(revision 2976)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aArray.class.php	(revision 2976)
@@ -0,0 +1,431 @@
+<?php
+
+/**
+ * TBB: these may seem trivial but they eliminate a lot of dumb
+ * tests and gratuitous loop variables and accidental bugs in templates.
+ *
+ * I was passing things by reference unnecessarily. That is fixed.
+ */
+class aArray 
+{
+  /**
+   * Return first element of array. If there isn't one, or it's
+   * not an array, or it's not set, return false.
+   */
+  public static function first($array)
+  {
+    if (isset($array) && (is_array($array)) && (count($array) > 0))
+    {
+      return $array[0];
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+  /**
+   * Return last element of array. If there isn't one, or it's
+   * not an array, or it's not set, return false.
+   */
+  public static function last($array)
+  {
+    if (isset($array) && (is_array($array)) && (count($array) > 0))
+    {
+      return $array[count($array) - 1];
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+  /**
+   * "One of these fields is bound to contain something interesting..."
+   * Returns the first value in the array that isn't made up entirely
+   * of trimmable whitespace.
+   */
+  public static function firstNontrivial($array)
+  {
+    foreach ($array as $value)
+    {
+      if (strlen(trim($value)))
+      {
+        return $value;
+      }
+    }
+    return false;
+  }
+  
+  /**
+   * Sort an array by the stringification of each element.
+   * Works for objects; would work fine for strings too.
+   * Why this is not standard is a mystery to me.
+   */
+  public static function sort(&$array)
+  {
+    return usort($array, array('aArray', 'compare'));
+  }
+  
+  /**
+   * Same idea, case insensitive.
+   */
+  public static function sortInsensitive(&$array)
+  {
+    return usort($array, array('aArray', 'compareInsensitive'));
+  }
+  
+  /**
+   * Like array_search, this method returns the offset of the
+   * value within the array, if it is present, false otherwise.
+   *
+   * However, 'strict' has three possible values, extending its meaning in
+   * the standard PHP array functions:
+   *
+   * false: items are compared with ==
+   *
+   * true: items are compared with ===
+   *
+   * 'id': items are compared with the getId() method of the values,
+   * which must be objects
+   *
+   * If you find yourself calling this often in a loop, though, promise me 
+   * you'll create an associative array instead.
+   */
+  public static function search($array, $value, $strict)
+  {
+    if ($strict === 'id')
+    {
+      $count = count($array);
+      if (!$count)
+      {
+        return false;
+      }
+      $vid = $value->getId();
+      for ($i = 0; ($i < $count); $i++)
+      {
+        if ($vid == $array[$i]->getId())
+        {
+          return $i;
+        }
+      }
+      
+      return false;
+    }
+    
+    return array_search($array, $value, $strict);
+  }
+
+  /**
+   * Search the array, find the item, return the index of the *previous*
+   * item. If wrap is specified, a request for the first item
+   * returns the last item, otherwise it returns false. If the
+   * array is empty this function always returns false. If the item is
+   * not in the array this function always returns false. If the
+   * array is one element long the index of that element is returned.
+   * Uses aArray::search(), so 'id' is an allowed value for $strict.
+   *
+   * This is great for creating "Previous" links.
+   */
+  public static function before($array, $value, $strict = false, $wrap = false)
+  {
+    $index = self::search($array, $value, $strict);
+    if ($index === false)
+    {
+      return false;
+    }
+    if ($index == 0)
+    {
+      if ($wrap)
+      {
+        return count($array) - 1;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    
+    return $index - 1;
+  }
+
+  /**
+   * Search the array, find the item, return the index of the *next*
+   * item. If wrap is specified, a request for the last item
+   * returns the first item, otherwise it returns false. If the
+   * array is empty this function always returns false. If the item is
+   * not in the array this function always returns false. If the
+   * array is one element long the index of that sole element is returned.
+   * Uses aArray::search(), so 'id' is an allowed value for $strict.
+   *
+   * This is great for creating "Next" links.
+   */
+  public static function after($array, $value, $strict = false, $wrap = false)
+  {
+    $index = self::search($array, $value, $strict);
+    if ($index === false)
+    {
+      return false;
+    }
+    if ($index == (count($array) - 1))
+    {
+      if ($wrap)
+      {
+        return 0;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    
+    return $index + 1;
+  }
+
+  /**
+   * Given an array of objects or arrays, return an array of ids
+   * obtained by either calling getId() on each object or returning the 'id'
+   * value of each child array.
+   * 
+   * @see listToHashById
+   */
+  public static function getIds($array)
+  {
+    if (count($array) === 0)
+    {
+      return array();
+    }
+  
+    // is_object covers stdClass objects. instanceof Object doesn't. Yes this is weird.
+    if (is_object($array[0]))
+    {
+      if (isset($array[0]->id))
+      {
+        return self::getResultsForProperty($array, 'id');
+      }
+      else
+      {
+        return self::getResultsForMethod($array, 'getId');      
+      }
+    }
+    else
+    {
+      return self::getResultsForKey($array, 'id');      
+    }
+  }
+
+  /**
+   * Given an array of objects and a property name, return an array consisting
+   * of the results obtained by fetching that property on each object
+   */
+  public static function getResultsForProperty($array, $property)
+  {
+    $results = array();
+    foreach ($array as $item)
+    {
+      $results[] = $item->{$property};
+    }
+    
+    return $results;
+  }
+
+  /**
+   * Given an array of objects and a method, return an array consisting
+   * of the results obtained by calling the method on each object
+   */
+  public static function getResultsForMethod($array, $method)
+  {
+    $results = array();
+    foreach ($array as $item)
+    {
+      $results[] = call_user_func(array($item, $method));
+    }
+    
+    return $results;
+  }
+
+  /**
+   * Given an array of objects and a method, return an array consisting
+   * of the results obtained by calling the method on each object
+   */
+  public static function getResultsForKey($array, $key)
+  {
+    $results = array();
+    foreach ($array as $item)
+    {
+      $results[] = $item[$key];
+    }
+    
+    return $results;
+  }
+  
+  /**
+   * Given a flat array of objects, returns an associative
+   * array indexed by ids as returned by getId(). You can
+   * specify an alternate id-fetching method. If the elements
+   * are arrays, the 'id' field is retrieved instead
+   */
+  public static function listToHashById($array, $method = 'getId')
+  {
+    $hash = array();
+    foreach ($array as $item)
+    {
+      if (is_array($item))
+      {
+        $hash[$item['id']] = $item;
+      }
+      else
+      {
+        $hash[$item->$method()] = $item;
+      }
+    }
+    
+    return $hash;
+  }
+  
+  // Hashes 'id' to 'name', useful in select elements
+  public static function getChoices($array)
+  {
+    $hash = array();
+    foreach ($array as $item)
+    {
+      $hash[$item->getId()] = $item->getName();
+    }
+    return $hash;
+  }
+  
+  /**
+   * Given an array of items, rearrange them into subarrays
+   * by first letter of their string representation. Useful for directories 
+   * by first letter. You can specify an alternate callable to be used to fetch
+   * the name of the item if conversion to a string doesn't do what you want
+   * (for instance, if you're being lazy and using hashes where you really
+   * ought to be using an object and defining a __toString() method).  
+   */
+  public static function byFirstLetter($array, $getName = null)
+  {
+    $alphabet = array_map('chr', range(ord('A'), ord('Z')));
+    $result = array();
+    foreach ($alphabet as $letter)
+    {
+      $result[$letter] = array();
+    }
+    foreach ($array as $item)
+    {
+      if (isset($getName))
+      {
+        $name = call_user_func($getName, $item);
+      } else
+      {
+        $name = (string) $item;
+      }
+      $result[strtoupper(substr($name, 0, 1))][] = $item;
+    }
+    
+    return $result;
+  }
+
+  /**
+   * Remove the specified value, if present, from a flat array, returning a flat array lacking that element.
+   * Not for use with associative arrays
+   */
+  public static function removeValue($a, $v)
+  {
+    $a = array_flip($a);
+    unset($a[$v]);
+    
+    return array_keys($a);
+  }
+
+  /**
+   * Filter out null values. Works on both flat and associative arrays
+   */
+  public static function filterNulls($a)
+  {
+    $b = array();
+    foreach ($a as $key => $val)
+    {
+      if (is_null($val))
+      {
+        continue;
+      }
+      $b[$key] = $val;
+    }
+    return $b;
+  }
+
+  /**
+   * Helpers for the above. 
+   *
+   * Compare two objects as strings via their string conversion methods.
+   */
+  public static function compare($a, $b)
+  {
+    // PHP 5.1.x doesn't apply __toString outside of
+    // echo and print statements. Grr
+    $s1 = self::toString($a);
+    $s2 = self::toString($b);
+    
+    // If we knew we were on 5.2.x, we could just do this
+    // $s1 = "$a";
+    // $s2 = "$b";
+    if ($s1 == $s2)
+    {
+      return 0;
+    }
+    return ($s1 < $s2) ? -1 : 1;
+  }
+
+  /**
+   * Should be PHP 5.1.x-safe
+   */
+  private static function toString($a)
+  {
+    if (is_object($a) && method_exists($a, '__toString'))
+    {
+      return $a->__toString();
+    } 
+    else
+    {
+      return "$a";
+    }
+  }
+
+  /**
+   * Case insensitive version of the same thing
+   */
+  public static function compareInsensitive($a, $b)
+  {
+    // PHP 5.1.x doesn't apply __toString outside of
+    // echo and print statements. Grr
+    $s1 = strtolower(self::toString($a));
+    $s2 = strtolower(self::toString($b));
+    
+    // If we knew we were on 5.2.x, we could just do this
+    // $s1 = strtolower("$a");
+    // $s2 = strtolower("$b");
+    if ($s1 == $s2)
+    {
+      return 0;
+    }
+    
+    return ($s1 < $s2) ? -1 : 1;
+  }
+  
+  // Is this a numerically indexed array without gaps?
+  public static function isFlat($array)
+  {
+    $n = 0;
+    foreach ($array as $key => $val)
+    {
+      if ($key !== $n)
+      {
+        return false;
+      }
+      $n++;
+    }
+    return true;
+  }
+}
+
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aCsvReader.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aCsvReader.class.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aCsvReader.class.php	(revision 9)
@@ -0,0 +1,81 @@
+<?php
+
+/*
+ * Associative array access to CSV data. 
+ *
+ * Usage:
+ *
+ *
+ * try {
+ *   $reader = new aCsvReader("filename.csv");
+ * } catch (Exception $e) {
+ *   die("Not a happy CSV file!\n");
+ * }
+ *
+ * # Get array of heading names found
+ * $headings = $reader->getHeadings();
+ *
+ * # Loop through rows, doing something with a field by heading name
+ * while ($row = $reader->getRow()) {
+ *   echo("Name of User: " . $row['name'] . "\n");
+ * }
+ *
+ */
+
+
+class aCsvFileOpenException extends Exception
+{
+  public function __construct()
+  {
+    parent::__construct("Unable to open csv file");
+  }
+}
+
+class aCsvNoHeadingsException extends Exception
+{
+  public function __construct()
+  {
+    parent::__construct("No headings in CSV file (empty file?)");
+  }
+}
+
+class aCsvReader 
+{
+  private $in;
+  public function __construct($file) 
+  {
+    $this->in = fopen($file, "r");
+    if (!$this->in) {
+      throw new aCsvFileOpenException();
+    }
+    $headings = fgetcsv($this->in);
+    if ($headings === FALSE) {
+      throw new aCsvNoHeadingsException();
+    }
+    $this->headings = $headings;
+  }
+  public function getHeadings()
+  {
+    return $this->headings;
+  }
+  public function getRow() { 
+    $data = fgetcsv($this->in);
+    if ($data === false) {
+      fclose($this->in);
+      return false;
+    }
+    $row = array();
+    $count = 0;
+    foreach ($this->headings as $heading) {
+      # Tolerate trailing null columns not returned
+      if (isset($data[$count])) {
+        $row[$heading] = $data[$count++];
+      } else {
+        $row[$heading] = false;
+      }
+    }
+    return $row;
+  }
+}
+
+
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouteClass.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouteClass.php	(revision 9)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouteClass.php	(revision 9)
@@ -0,0 +1,8 @@
+<?php
+
+interface aRouteClass
+{
+  // No methods, we just need to be able to assert that
+  // we're a CMS route class so that the routing class
+  // can prepend the page URL 
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrineRoute.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrineRoute.php	(revision 2964)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aDoctrineRoute.php	(revision 2964)
@@ -0,0 +1,71 @@
+<?php
+
+// Used by engine pages.
+
+class aDoctrineRoute extends sfDoctrineRoute 
+{
+  public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array())
+  {
+    parent::__construct($pattern, $defaults, $requirements, $options);  
+  }
+
+  /**
+   * Returns true if the URL matches this route, false otherwise.
+   *
+   * @param  string  $url     The URL
+   * @param  array   $context The context
+   *
+   * @return array   An array of parameters
+   */
+  public function matchesUrl($url, $context = array())
+  {
+    $url = aRouteTools::removePageFromUrl($this, $url);
+    return parent::matchesUrl($url, $context);
+  }
+
+  /**
+   * Generates a URL from the given parameters.
+   *
+   * @param  mixed   $params    The parameter values
+   * @param  array   $context   The context
+   * @param  Boolean $absolute  Whether to generate an absolute URL
+   *
+   * @return string The generated URL
+   */
+  public function generate($params, $context = array(), $absolute = false)
+  {
+    $slug = null;
+    $defaults = $this->getDefaults();
+
+    if (isset($params['sf_subject']) && (!isset($params['engine-slug'])))
+    {
+      // Don't override the current page if it is an engine, or a previously
+      // pushed engine page
+      $slug = aRouteTools::getContextEngineSlug($this);
+      if ($slug)
+      {
+        $params['engine-slug'] = $slug;
+      }
+      else
+      {
+        if (method_exists($params['sf_subject'], 'getEngineSlug'))
+        {
+          $params['engine-slug'] = $params['sf_subject']->getEngineSlug();
+        }
+      }
+    }
+
+    if (isset($params['engine-slug']))
+    {
+      $slug = $params['engine-slug'];
+      aRouteTools::pushTargetEngineSlug($slug, $defaults['module']);
+      unset($params['engine-slug']);
+    } 
+    $result = aRouteTools::addPageToUrl($this, parent::generate($params, $context, false), $absolute);
+    if ($slug)
+    {
+      aRouteTools::popTargetEngine($defaults['module']);
+    }
+    return $result;
+  } 
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaRouting.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaRouting.php	(revision 2179)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aMediaRouting.php	(revision 2179)
@@ -0,0 +1,130 @@
+<?php
+
+class aMediaRouting
+{
+  static public function listenToRoutingLoadConfigurationEvent(sfEvent $event)
+  {
+    $r = $event->getSubject();
+    
+    if (aMediaTools::getOption("routes_register") && in_array('aMedia', sfConfig::get('sf_enabled_modules')))
+    {
+      // Since the media plugin is now an engine, we need our own
+      // catch-all rule for administrative URLs in the media area.
+      // Prepending it first means it matches last
+      $r->prependRoute('a_media_other', new aRoute('/:action', array(
+        'module' => 'aMedia'
+      )));
+
+      $r->prependRoute('a_media_image_show', new aRoute('/view/:slug', array(
+        'module' => 'aMedia',
+        'action' => 'show'
+      ), array('slug' => '^' . aTools::getSlugRegexpFragment() . '$')));
+
+      // Allow permalinks for PDF originals
+      $r->prependRoute('a_media_image_original', new sfRoute('/uploads/media_items/:slug.original.:format', array(
+        'module' => 'aMediaBackend',
+        'action' => 'original'
+      ), array('slug' => '^' . aTools::getSlugRegexpFragment() . '$', 'format' => '^(\w+)$')));
+
+      $route = new sfRoute('/uploads/media_items/:slug.:width.:height.:resizeType.:format', array(
+        'module' => 'aMediaBackend',
+        'action' => 'image'
+      ), array(
+        'slug' => '^' . aTools::getSlugRegexpFragment() . '$',
+        'width' => '^\d+$',
+        'height' => '^\d+$',
+        'resizeType' => '^\w$',
+        'format' => '^(jpg|png|gif)$'
+      ));
+      $r->prependRoute('a_media_image', $route);
+
+      $route = new sfRoute('/uploads/media_items/:slug.:cropLeft.:cropTop.:cropWidth.:cropHeight.:width.:height.:resizeType.:format', array(
+        'module' => 'aMediaBackend',
+        'action' => 'image'
+      ), array(
+        'slug' => '^' . aTools::getSlugRegexpFragment() . '$',
+        'width' => '^\d+$',
+        'height' => '^\d+$',
+        'cropLeft' => '^\d+$',
+        'cropTop' => '^\d+$',
+        'cropWidth' => '^\d+$',
+        'cropHeight' => '^\d+$',
+        'resizeType' => '^\w$',
+        'format' => '^(jpg|png|gif)$'
+      ));
+      $r->prependRoute('a_media_image_cropped', $route);
+      
+      // What we want:
+      // /media   <-- everything
+      // /image   <-- media of type image
+      // /video   <-- media of type video
+      // /tag/tagname <-- media with this tag
+      // /image/tag/tagname <-- images with this tag 
+      // /video/tag/tagname <-- video with this tag
+      // /media?search=blah blah blah  <-- searches are full of
+      //                                   dirty URL-unfriendly characters and
+      //                                   are traditionally query strings.
+      
+      $r->prependRoute('a_media_index', new aRoute('/', array(
+        'module' => 'aMedia', 
+        'action' => 'index'
+      )));
+      
+      $r->prependRoute('a_media_index_type', new aRoute('/:type', array(
+        'module' => 'aMedia',
+        'action' => 'index'
+      ), array('type' => '(image|video)')));
+      
+      $r->prependRoute('a_media_index_category', new aRoute('/category/:category', array(
+        'module' => 'aMedia',
+        'action' => 'index'
+      ), array('category' => '.*')));
+      
+      $r->prependRoute('a_media_index_tag', new aRoute('/tag/:tag', array(
+        'module' => 'aMedia',
+        'action' => 'index'
+      ), array('tag' => '.*')));
+
+      $r->prependRoute('a_media_select', new aRoute('/select', array(
+        'class' => 'aRoute',
+        'module' => 'aMedia',
+        'action' => 'select'
+      )));
+      
+      $r->prependRoute('a_media_info', new sfRoute('/info', array(
+        'module' => 'aMediaBackend',
+        'action' => 'info'
+      )));
+      
+      $r->prependRoute('a_media_tags', new sfRoute('/tags', array(
+        'module' => 'aMediaBackend',
+        'action' => 'tags'
+      )));
+      
+      $r->prependRoute('a_media_upload', new aRoute('/upload', array(
+        'module' => 'aMedia',
+        'action' => 'upload'
+      )));
+      
+      $r->prependRoute('a_media_edit_multiple', new aRoute('/editMultiple', array(
+        'module' => 'aMedia',
+        'action' => 'editMultiple'
+      )));
+
+      $r->prependRoute('a_media_edit', new aRoute('/edit', array(
+        'module' => 'aMedia',
+        'action' => 'edit'
+      )));
+      
+      $r->prependRoute('a_media_new_video', new aRoute('/newVideo', array(
+        'module' => 'aMedia',
+        'action' => 'newVideo'
+      )));
+      
+      $r->prependRoute('a_media_edit_video', new aRoute('/editVideo', array(
+        'module' => 'aMedia',
+        'action' => 'editVideo'
+      )));
+    }
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineActions.class.php.edited
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineActions.class.php.edited	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aEngineActions.class.php.edited	(revision 1957)
@@ -0,0 +1,9 @@
+<?php
+
+class aEngineActions extends sfActions
+{  
+  public function preExecute()
+  {
+    aEngineTools::preExecute($this);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouting.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouting.php	(revision 2979)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/toolkit/aRouting.php	(revision 2979)
@@ -0,0 +1,84 @@
+<?php
+
+class aRouting extends sfPatternRouting
+{
+  static public function listenToRoutingAdminLoadConfigurationEvent(sfEvent $event)
+  {
+    $r = $event->getSubject();
+    $enabledModules = array_flip(sfConfig::get('sf_enabled_modules', array()));
+    if (isset($enabledModules['aTagAdmin']))
+    {
+      $r->prependRoute('a_tag_admin', new sfDoctrineRouteCollection(array('name' => 'a_tag_admin',
+        'model' => 'Tag',
+        'module' => 'aTagAdmin',
+        'prefix_path' => 'admin/tags',
+        'collection_actions' => array('clean' => 'get'),
+        'column' => 'id',
+        'with_wildcard_routes' => true)));
+    }
+    if (isset($enabledModules['aCategoryAdmin']))
+    {
+      $r->prependRoute('a_category_admin', new sfDoctrineRouteCollection(array('name' => 'a_category_admin',
+        'model' => 'aCategory',
+        'module' => 'aCategoryAdmin',
+        'prefix_path' => 'admin/categories',
+        'column' => 'id',
+        'with_wildcard_routes' => true)));
+    }
+    if (isset($enabledModules['aUserAdmin']))
+    {
+      $r->prependRoute('a_user_admin', new sfDoctrineRouteCollection(array('name' => 'a_user_admin',
+        'model' => 'sfGuardUser',
+        'module' => 'aUserAdmin',
+        'prefix_path' => 'admin/user',
+        'column' => 'id',
+        'with_wildcard_routes' => true)));
+    }
+    if (isset($enabledModules['aGroupAdmin']))
+    {
+      $r->prependRoute('a_group_admin', new sfDoctrineRouteCollection(array('name' => 'a_group_admin',
+        'model' => 'sfGuardGroup',
+        'module' => 'aGroupAdmin',
+        'prefix_path' => 'admin/group',
+        'column' => 'id',
+        'with_wildcard_routes' => true)));
+    }
+    if (isset($enabledModules['aPermissionAdmin']))
+    {
+      $r->prependRoute('a_permission_admin', new sfDoctrineRouteCollection(array('name' => 'a_permission_admin',
+        'model' => 'sfGuardPermission',
+        'module' => 'aPermissionAdmin',
+        'prefix_path' => 'admin/permission',
+        'column' => 'id',
+        'with_wildcard_routes' => true)));
+    }
+    // Used by apostrophe:deploy to clear the APC cache, needs a consistent path
+    if (isset($enabledModules['aSync']))
+    {
+      $r->prependRoute('a_sync', new sfRoute('/async/:action', array(
+        'module' => 'aSync',
+        'url' => '/async/:action')));
+    }
+    // Right now the admin engine isn't terribly exciting,
+    // it just redirects away from the /admin page that belongs to it.
+    // Longer URLs starting with /admin are left alone as they often belong
+    // to non-engine modules like the users module
+    if (isset($enabledModules['aAdmin']))
+    {
+      $r->prependRoute('a_admin', new aRoute('/', array(
+        'module' => 'aAdmin',
+        'action' => 'index',
+        'url' => '/')));
+    }
+    if (isset($enabledModules['aCategoryAdmin']))
+    {
+      $r->prependRoute('a_category_admin', new sfDoctrineRouteCollection(array('name' => 'a_category_admin',
+        'model' => 'aCategory',
+        'module' => 'aCategoryAdmin',
+        'prefix_path' => 'admin/categories',
+        'collection_actions' => array('posts' => 'get', 'events' => 'get'),
+        'column' => 'id',
+        'with_wildcard_routes' => true)));
+    }
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatorFilePersistent.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatorFilePersistent.class.php	(revision 2452)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatorFilePersistent.class.php	(revision 2452)
@@ -0,0 +1,390 @@
+<?php
+
+// Copyright 2009, P'unk Ave LLC. Released under the MIT license.
+
+/**
+ * aValidatorFilePersistent validates an uploaded file, or
+ * revalidates the existing file of the same browser-side name
+ * uploaded on a previous submission by the same user in the case where 
+ * no new file has been specified. 
+ *
+ * The file should come from the aWidgetFormInputFilePersistent widget.
+ *
+ * Should behave like the parent class in all other respects.
+ *
+ * @see sfValidatorFile
+ */
+class aValidatorFilePersistent extends sfValidatorFile
+{
+  // Make the original name available to guessers. It's a nice thought to
+  // avoid this but with Microsoft Office formats there are no reliable
+  // magic numbers, and those that do exist can be misleading because
+  // Word can contain Excel and vice versa
+  protected $originalName;
+  
+  protected function configure($options = array(), $messages = array())
+  {
+    $guessersSet = isset($options['mime_type_guessers']);
+    parent::configure($options, $messages);
+    if (!$guessersSet)
+    {
+      // Extend the default list from the parent class with guessers that are more
+      // robust about spotting files that can't be picked up if Unix file is 
+      // unavailable, mime type files are out of date, Unix file has a bug that
+      // hates on certain valid MP3s, etc. Everything else falls back to the other guessers
+      $mimeTypeGuessers = $this->getOption('mime_type_guessers');
+      array_unshift($mimeTypeGuessers, array($this, 'guessFromImageconverter'));
+      array_unshift($mimeTypeGuessers, array($this, 'guessFromID3'));
+      array_unshift($mimeTypeGuessers, array($this, 'guessRTF'));
+      $this->setOption('mime_type_guessers', $mimeTypeGuessers);
+    }
+  }
+
+  /**
+   * The input value must be an array potentially containing two
+   * keys, newfile and persistid. newfile must contain an array of
+   * the following subkeys, if it is present:
+   *
+   *  * tmp_name: The absolute temporary path to the newly uploaded file
+   *  * name:     The browser-submitted file name (optional, but necessary to distinguish amongst Microsoft Office formats)
+   *  * type:     The browser-submitted file content type (required although our guessers never trust it)
+   *  * error:    The error code (optional)
+   *  * size:     The file size in bytes (optional)
+   * 
+   * The persistid key allows lookup of a previously uploaded file
+   * when no new file has been submitted. 
+   *
+   * A RARE BUT USEFUL CASE: if you need to prefill this cache before
+   * invoking the form for the first time, you can instantiate this 
+   * validator yourself:
+   * 
+   * $vfp = new aValidatorFilePersistent();
+   * $guid = aGuid::generate();
+   * $vfp->clean(
+   *   array(
+   *     'newfile' => 
+   *       array('tmp_name' => $myexistingfile), 
+   *     'persistid' => $guid));
+   *
+   * Then set array('persistid' => $guid) as the default value
+   * for the file widget. This logic is most easily encapsulated in
+   * the configure() method of your form class.
+   *
+   * @see sfValidatorFile
+   * @see sfValidatorBase
+   */
+
+  public function clean($value)
+  {
+    $user = sfContext::getInstance()->getUser();
+    $persistid = false;
+    if (isset($value['persistid']))
+    {
+      $persistid = $value['persistid'];      
+    }
+    $newFile = false;
+    $persistentDir = $this->getPersistentDir();
+    if (!self::validPersistId($persistid))
+    {
+      $persistid = false;
+    }
+    $cvalue = false;
+    // Why do we tolerate the newfile fork being entirely absent?
+    // Because with persistent file upload widgets, it's safe to
+    // redirect a form submission to another action via the GET method
+    // after validation... which is extremely useful if you want to
+    // split something into an iframed initial upload action and
+    // a non-iframed annotation action and you need to be able to
+    // stuff the state of the form into a URL and do window.parent.location =.
+    // As long as we tolerate the absence of the newfile button, we can
+    // rebuild the submission from what's in 
+    // getRequest()->getParameterHolder()->getAll(), and that is useful.
+    if ((!isset($value['newfile']) || ($this->isEmpty($value['newfile']))))
+    {
+      if ($persistid !== false)
+      {
+        $filePath = "$persistentDir/$persistid.file";
+        $data = false;
+        if (file_exists($filePath))
+        {
+          $dataPath = "$persistentDir/$persistid.data";
+          // Don't let them expire
+          touch($filePath);
+          touch($dataPath);
+          $data = file_get_contents($dataPath);
+          if (strlen($data))
+          {
+            $data = unserialize($data);
+          }
+        }
+        if ($data)
+        {
+          $cvalue = $data;
+        }
+      }
+    }
+    else
+    {
+      $newFile = true;
+      $cvalue = $value['newfile'];
+    }
+    if (isset($cvalue['name']))
+    {
+      $this->originalName = $cvalue['name'];
+    }
+    else
+    {
+      $this->originalName = '';
+    }
+    try
+    {
+      $result = parent::clean($cvalue);
+    } catch (Exception $e)
+    {
+      // If there is a validation error stop keeping this
+      // file around and don't display the reassuring
+      // "you don't have to upload again" message side by side
+      // with the validation error.
+      if ($persistid !== false)
+      {
+        $infoPath = "$persistentDir/$persistid.data";
+        $filePath = "$persistentDir/$persistid.file";
+        @unlink($infoPath);
+        @unlink($filePath);
+      }
+      throw $e;
+    }
+    if ($newFile)
+    {
+      // Expiration of abandoned stuff has to happen somewhere
+      self::removeOldFiles($persistentDir);
+      if ($persistid !== false)
+      {
+        $filePath = "$persistentDir/$persistid.file";
+        copy($cvalue['tmp_name'], $filePath);
+        $data = $cvalue;
+        $data['newfile'] = true;
+        $data['tmp_name'] = $filePath;
+        
+        // It's useful to know the mime type and true extension for 
+        // supplying previews and icons
+        $extensionsByMimeType = array_flip(aMediaTools::getOption('mime_types'));
+        if (!isset($cvalue['type']))
+        {
+          // It's not sensible to trust a browser-submitted mime type anyway,
+          // so don't force non-web invocations of this code to supply one
+          $cvalue['type'] = 'unknown/unknown';
+        }
+        $data['mime_type'] = $this->getMimeType($filePath, $cvalue['type']);
+        if (isset($extensionsByMimeType[$data['mime_type']]))
+        {
+          $data['extension'] = $extensionsByMimeType[$data['mime_type']];
+        }
+        
+        self::putFileInfo($persistid, $data);
+      }
+    } elseif ($persistid !== false)
+    {
+      $data = self::getFileInfo($persistid);
+      if ($data !== false)
+      {
+        $data['newfile'] = false;
+        self::putFileInfo($persistid, $data);
+      }
+    }
+    return $result;
+  }
+
+  static protected function getPersistentDir()
+  {
+    return aFiles::getWritableDataFolder(array("persistent_uploads"));
+  }
+
+  static public function removeOldFiles($dir)
+  {
+    // Age off any stale uploads in the cache
+    // (TODO: for performance, do this one time in a hundred or similar,
+    // it's simple to do that probabilistically).
+    $files = glob("$dir/*");
+    $now = time();
+    foreach ($files as $file)
+    {
+      if ($now - filemtime($file) > 
+        sfConfig::get('sf_persistent_upload_lifetime', 60) * 60)
+      {
+        unlink($file); 
+      }
+    }
+  }
+
+  static public function previewAvailable($value)
+  {
+    if (isset($value['persistid']))
+    {
+      $persistid = $value['persistid'];
+      $info = self::getFileInfo($persistid);
+      // Only web images are reasonable for preview. We could do
+      // PDFs but in practice it's very slow, slower than you
+      // want to wait for when annotating; it's worth it later
+      // for display in the media repository
+      return $info['tmp_name'] && getimagesize($info['tmp_name']);
+    }
+    return false;
+  }
+
+  static public function alreadyPersisting($value)
+  {
+    if (isset($value['persistid']))
+    {
+      $persistid = $value['persistid'];
+      $info = self::getFileInfo($persistid);
+      // Only web images are reasonable for preview. We could do
+      // PDFs but in practice it's very slow, slower than you
+      // want to wait for when annotating; it's worth it later
+      // for display in the media repository
+      return !!$info['tmp_name'];
+    }
+    return false;
+  }
+
+  
+  static public function getFileInfo($persistid)
+  {
+    if (!self::validPersistId($persistid))
+    {
+      // Roll our eyes at the hackers
+      return false;
+    }
+    $persistentDir = self::getPersistentDir();
+    $infoPath = "$persistentDir/$persistid.data";
+    if (file_exists($infoPath))
+    {
+      return unserialize(file_get_contents($infoPath));
+    }
+    else
+    {
+      return false; 
+    }
+  }
+
+  static public function putFileInfo($persistid, $data)
+  {
+    $persistentDir = self::getPersistentDir();
+    file_put_contents("$persistentDir/$persistid.data", serialize($data));
+  }
+  
+  static public function validPersistId($persistid)
+  {
+    return preg_match("/^[a-fA-F0-9]+$/", $persistid);
+  }
+  
+  /**
+   * Guess the file mime type with aImageConverter's getInfo method, which uses imagesize and
+   * magic numbers to be more robust than relying on a lot of badly configured external tools
+   *
+   * @param  string $file  The absolute path of a file
+   *
+   * @return string The mime type of the file (null if not guessable)
+   */
+  protected function guessFromImageconverter($file)
+  {
+    $info = aImageConverter::getInfo($file);
+    if (!$info)
+    {
+      return null;
+    }
+    $formats = array('jpg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'pdf' => 'application/pdf');
+    if (isset($formats[$info['format']]))
+    {
+      return $formats[$info['format']];
+    }
+    return null;
+  }
+  /**
+   * Guess the file mime type of MP3 audio files based on the ID3 tag at the beginning, more robust
+   * than the file command's buggy support for MP3s that seems to dislike VBR files
+   *
+   * @param  string $file  The absolute path of a file
+   *
+   * @return string The mime type of the file (null if not guessable)
+   */
+  protected function guessFromID3($file)
+  {
+    $in = fopen($file, 'rb');
+    $magic = fread($in, 3);
+    fclose($in);
+    if ($magic !== 'ID3')
+    {
+      return null;
+    }
+    return 'audio/mpeg';
+  }
+
+  protected function guessRTF($file)
+  {
+    $in = fopen($file, 'rb');
+    $magic = fread($in, 5);
+    fclose($in);
+    if ($magic !== '{\\rtf')
+    {
+      return null;
+    }
+    return 'text/rtf';
+  }
+  
+  protected function guessMicrosoft($file)
+  {
+    // We look at the original name to get the rest.
+    // Sorry, but there are no reliable magic numbers
+    // that don't sometimes mislead for Microsoft Office files.
+    $in = fopen($file, "rb");
+    $data = fread($in, 3);
+    fclose($in);
+    $maybeMicrosoft = false;
+    // Magic numbers: old Microsoft container and new zip-based Microsoft container
+    if (($data === sprintf("%c%c%c", 0xD0, 0xCF, 0x11)) || ($data === sprintf("%c%c%c", 0x50, 0x4B, 0x03)))
+    {
+      $maybeMicrosoft = true;
+    }
+    if (!$maybeMicrosoft)
+    {
+      return null;
+    }
+    $ms = array(
+      'xls' => 'application/vnd.ms-excel',
+      'ppt' => 'application/vnd.ms-powerpoint',
+      'doc' => 'application/msword',
+      'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+      'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+      'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+      'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+      'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+      'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+      'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template'
+    );
+    if (preg_match('/\.(\w+)$/', $this->originalName, $matches))
+    {
+      $extension = $matches[1];
+      if (isset($ms[$extension]))
+      {
+        return $ms[$extension];
+      }
+    }
+    return null;
+  }
+  
+  protected function getMimeType($file, $fallback)
+  {
+    // The microsoft guesser needs access to the original filename.
+    // For reasons I'm not sure of, it doesn't work as a dynamic method
+    // with call_user_func.
+    $match = $this->guessMicrosoft($file);
+    if (!is_null($match))
+    {
+      return $match;
+    }
+
+    return parent::getMimeType($file, $fallback);
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatedFile.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatedFile.class.php	(revision 2134)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatedFile.class.php	(revision 2134)
@@ -0,0 +1,14 @@
+<?php
+
+class aValidatedFile extends sfValidatedFile
+{
+  protected function getExtensionFromType($type, $default = '')
+  {
+    $extensionsByMimeType = array_flip(aMediaTools::getOption('mime_types'));
+    if (isset($extensionsByMimeType[$type]))
+    {
+      return '.' . $extensionsByMimeType[$type];
+    }
+    return $default;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatorSlug.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatorSlug.class.php	(revision 2682)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/aValidatorSlug.class.php	(revision 2682)
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * aValidatorSlug validates a user-supplied slug in a UTF-8 aware fashion. 
+ * It allows slashes if the 'allow-slashes' option is set to true. Otherwise it allows
+ * only letters and digits. It does not enforce a leading slash or look for 
+ * duplication, use other validators for those things if they apply to your
+ * application
+ *
+ * @package    symfony
+ * @subpackage validator
+ * @author     Tom Boutell <tom@punkave.com>
+ * @version    SVN: $Id: sfValidatorHtml.class.php 12641 2008-11-04 18:22:00Z fabien $
+ */
+class aValidatorSlug extends sfValidatorString
+{
+  /**
+   * Configures the current validator.
+   *
+   * @param array $options   An array of options
+   * @param array $messages  An array of error messages
+   *
+   * @see sfValidatorBase
+   */
+  protected function configure($options = array(), $messages = array())
+  {
+    // Typically you'll set both of these if you're using it for page slugs,
+    // and set neither for media item or blog post slugs
+    $this->addOption('allow_slashes', false);
+    $this->addOption('require_leading_slash', false);
+    // If strict is false, doClean will just clean the slug (potentially changing it).
+    // If strict is true, it will reject slugs that are not already clean.
+    // The latter is probably best when users are explicitly editing slugs
+    $this->addOption('strict', true);
+    
+    parent::configure($options, $messages);
+  }
+
+  /**
+   * @see sfValidatorBase
+   */
+  protected function doClean($value)
+  {
+    $clean = (string) parent::doClean($value);
+    $clean = aString::strtolower($clean);
+    $slugified = aTools::slugify($clean, $this->getOption('allow_slashes'));
+    if ($this->getOption('strict'))
+    {
+      if ($slugified !== $clean)
+      {
+        throw new sfValidatorError($this, 'invalid', array('value' => $value));
+      }
+    }
+    else
+    {
+      $clean = $slugified;
+    }
+    if ($this->getOption('require_leading_slash') && (substr($value, 0, 1) !== '/'))
+    {
+      throw new sfValidatorError($this, 'invalid', array('value' => $value));
+    }
+    return $clean;
+  }
+  
+  // aHtml::simplify uses false to skip things, not null
+  protected function getOptionOrFalse($s)
+  {
+    $option = $this->getOption($s);
+    if (is_null($option))
+    {
+      return false;
+    }
+    return $option;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/sfValidatorHtml.class.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/sfValidatorHtml.class.php	(revision 4)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/validator/sfValidatorHtml.class.php	(revision 4)
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * sfValidatorHtml validates an HTML string. It also converts the input value to a string.
+ * It utilizes aHtml::simplify
+ *
+ * @package    symfony
+ * @subpackage validator
+ * @author     Alex Gilbert <alex@punkave.com>
+ * @author     Tom Boutell <tom@punkave.com>
+ * @version    SVN: $Id: sfValidatorHtml.class.php 12641 2008-11-04 18:22:00Z fabien $
+ */
+class sfValidatorHtml extends sfValidatorString
+{
+  /**
+   * Configures the current validator.
+   *
+   * @param array $options   An array of options
+   * @param array $messages  An array of error messages
+   *
+   * @see sfValidatorBase
+   */
+  protected function configure($options = array(), $messages = array())
+  {
+    $this->addMessage('allowed_tags', 'Your field contains unsupported HTML tags.');
+
+    // See aHtml::simplify for the meaning of these options
+    $this->addOption('allowed_tags', null);
+    $this->addOption('allowed_attributes', null);
+    $this->addOption('allowed_styles', null);
+    $this->addOption('complete', false);
+
+    $this->addOption('strip', true);
+    // Mandatory. We don't complain about HTML here, we clean it
+    $this->setOption('strip', true);
+    
+    parent::configure($options, $messages);
+  }
+
+  /**
+   * @see sfValidatorBase
+   */
+  protected function doClean($value)
+  {
+    $clean = (string) $value;
+
+    if ($this->getOption('strip'))
+    {
+      $clean = aHtml::simplify($clean, $this->getOptionOrFalse('allowed_tags'), $this->getOptionOrFalse('complete'), $this->getOptionOrFalse('allowed_attributes'), $this->getOptionOrFalse('allowed_styles'));
+    }
+    else
+    {
+      throw new sfException('That should not happen strip is set in configure in sfValidatorHtml');
+    }
+    
+    $clean = parent::doClean($clean);
+    
+    return $clean;
+  }
+  
+  // aHtml::simplify uses false to skip things, not null
+  protected function getOptionOrFalse($s)
+  {
+    $option = $this->getOption($s);
+    if (is_null($option))
+    {
+      return false;
+    }
+    return $option;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date.php	(revision 2079)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date.php	(revision 2079)
@@ -0,0 +1,1465 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
+
+// {{{ Header
+
+/**
+ * Generic date handling class for PEAR
+ *
+ * Generic date handling class for PEAR.  Attempts to be time zone aware
+ * through the Date::TimeZone class.  Supports several operations from
+ * Date::Calc on Date objects.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 1997-2006 Baba Buehler, Pierre-Alain Joye
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted under the terms of the BSD License.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE
+ * COPYRIGHT OWNER OR 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.
+ *
+ * @category   Date and Time
+ * @package    Date
+ * @author     Baba Buehler <baba@babaz.com>
+ * @author     Pierre-Alain Joye <pajoye@php.net>
+ * @author     Firman Wandayandi <firman@php.net>
+ * @copyright  1997-2006 Baba Buehler, Pierre-Alain Joye
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    CVS: $Id: Date.php,v 1.41 2006/11/22 00:28:03 firman Exp $
+ * @link       http://pear.php.net/package/Date
+ */
+
+// }}}
+
+// {{{ Includes
+
+/**
+ * Load Date_TimeZone.
+ */
+require_once 'Date/TimeZone.php';
+
+/**
+ * Load Date_Calc.
+ */
+require_once 'Date/Calc.php';
+
+/**
+ * Load Date_Span.
+ */
+require_once 'Date/Span.php';
+
+// }}}
+// {{{ Constants
+
+// {{{ Output formats Pass this to getDate().
+
+/**
+ * "YYYY-MM-DD HH:MM:SS"
+ */
+define('DATE_FORMAT_ISO', 1);
+
+/**
+ * "YYYYMMSSTHHMMSS(Z|(+/-)HHMM)?"
+ */
+define('DATE_FORMAT_ISO_BASIC', 2);
+
+/**
+ * "YYYY-MM-SSTHH:MM:SS(Z|(+/-)HH:MM)?"
+ */
+define('DATE_FORMAT_ISO_EXTENDED', 3);
+
+/**
+ * "YYYY-MM-SSTHH:MM:SS(.S*)?(Z|(+/-)HH:MM)?"
+ */
+define('DATE_FORMAT_ISO_EXTENDED_MICROTIME', 6);
+
+/**
+ * "YYYYMMDDHHMMSS"
+ */
+define('DATE_FORMAT_TIMESTAMP', 4);
+
+/**
+ * long int, seconds since the unix epoch
+ */
+define('DATE_FORMAT_UNIXTIME', 5);
+
+// }}}
+
+// }}}
+// {{{ Class: Date
+
+/**
+ * Generic date handling class for PEAR
+ *
+ * Generic date handling class for PEAR.  Attempts to be time zone aware
+ * through the Date::TimeZone class.  Supports several operations from
+ * Date::Calc on Date objects.
+ *
+ * @author     Baba Buehler <baba@babaz.com>
+ * @author     Pierre-Alain Joye <pajoye@php.net>
+ * @author     Firman Wandayandi <firman@php.net>
+ * @copyright  1997-2006 Baba Buehler, Pierre-Alain Joye
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    Release: 1.4.7
+ * @link       http://pear.php.net/package/Date
+ */
+class Date
+{
+    // {{{ Properties
+
+    /**
+     * the year
+     * @var int
+     */
+    var $year;
+
+    /**
+     * the month
+     * @var int
+     */
+    var $month;
+
+    /**
+     * the day
+     * @var int
+     */
+    var $day;
+
+    /**
+     * the hour
+     * @var int
+     */
+    var $hour;
+
+    /**
+     * the minute
+     * @var int
+     */
+    var $minute;
+
+    /**
+     * the second
+     * @var int
+     */
+    var $second;
+
+    /**
+     * the parts of a second
+     * @var float
+     */
+    var $partsecond;
+
+    /**
+     * timezone for this date
+     * @var object Date_TimeZone
+     */
+    var $tz;
+
+    /**
+     * define the default weekday abbreviation length
+     * used by ::format()
+     * @var int
+     */
+    var $getWeekdayAbbrnameLength = 3;
+
+    // }}}
+    // {{{ Constructor
+
+    /**
+     * Constructor
+     *
+     * Creates a new Date Object initialized to the current date/time in the
+     * system-default timezone by default.  A date optionally
+     * passed in may be in the ISO 8601, TIMESTAMP or UNIXTIME format,
+     * or another Date object.  If no date is passed, the current date/time
+     * is used.
+     *
+     * @access public
+     * @see setDate()
+     * @param mixed $date optional - date/time to initialize
+     * @return object Date the new Date object
+     */
+    function Date($date = null)
+    {
+        $this->tz = Date_TimeZone::getDefault();
+        if (is_null($date)) {
+            $this->setDate(date("Y-m-d H:i:s"));
+        } elseif (is_a($date, 'Date')) {
+            $this->copy($date);
+        } else {
+            $this->setDate($date);
+        }
+    }
+
+    // }}}
+    // {{{ setDate()
+
+    /**
+     * Set the fields of a Date object based on the input date and format
+     *
+     * Set the fields of a Date object based on the input date and format,
+     * which is specified by the DATE_FORMAT_* constants.
+     *
+     * @access public
+     * @param string $date input date
+     * @param int $format Optional format constant (DATE_FORMAT_*) of the input date.
+     *                    This parameter isn't really needed anymore, but you could
+     *                    use it to force DATE_FORMAT_UNIXTIME.
+     */
+    function setDate($date, $format = DATE_FORMAT_ISO)
+    {
+        if (
+            preg_match('/^(\d{4})-?(\d{2})-?(\d{2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})(\.\d+)?(Z|[\+\-]\d{2}:?\d{2})?)?$/i', $date, $regs)
+            && $format != DATE_FORMAT_UNIXTIME) {
+            // DATE_FORMAT_ISO, ISO_BASIC, ISO_EXTENDED, and TIMESTAMP
+            // These formats are extremely close to each other.  This regex
+            // is very loose and accepts almost any butchered format you could
+            // throw at it.  e.g. 2003-10-07 19:45:15 and 2003-10071945:15
+            // are the same thing in the eyes of this regex, even though the
+            // latter is not a valid ISO 8601 date.
+            $this->year       = $regs[1];
+            $this->month      = $regs[2];
+            $this->day        = $regs[3];
+            $this->hour       = isset($regs[5])?$regs[5]:0;
+            $this->minute     = isset($regs[6])?$regs[6]:0;
+            $this->second     = isset($regs[7])?$regs[7]:0;
+            $this->partsecond = isset($regs[8])?(float)$regs[8]:(float)0;
+
+            // if an offset is defined, convert time to UTC
+            // Date currently can't set a timezone only by offset,
+            // so it has to store it as UTC
+            if (isset($regs[9])) {
+                $this->toUTCbyOffset($regs[9]);
+            }
+        } elseif (is_numeric($date)) {
+            // UNIXTIME
+            $this->setDate(date("Y-m-d H:i:s", $date));
+        } else {
+            // unknown format
+            $this->year       = 0;
+            $this->month      = 1;
+            $this->day        = 1;
+            $this->hour       = 0;
+            $this->minute     = 0;
+            $this->second     = 0;
+            $this->partsecond = (float)0;
+        }
+    }
+
+    // }}}
+    // {{{ getDate()
+
+    /**
+     * Get a string (or other) representation of this date
+     *
+     * Get a string (or other) representation of this date in the
+     * format specified by the DATE_FORMAT_* constants.
+     *
+     * @access public
+     * @param int $format format constant (DATE_FORMAT_*) of the output date
+     * @return string the date in the requested format
+     */
+    function getDate($format = DATE_FORMAT_ISO)
+    {
+        switch ($format) {
+        case DATE_FORMAT_ISO:
+            return $this->format("%Y-%m-%d %T");
+            break;
+        case DATE_FORMAT_ISO_BASIC:
+            $format = "%Y%m%dT%H%M%S";
+            if ($this->tz->getID() == 'UTC') {
+                $format .= "Z";
+            }
+            return $this->format($format);
+            break;
+        case DATE_FORMAT_ISO_EXTENDED:
+            $format = "%Y-%m-%dT%H:%M:%S";
+            if ($this->tz->getID() == 'UTC') {
+                $format .= "Z";
+            }
+            return $this->format($format);
+            break;
+        case DATE_FORMAT_ISO_EXTENDED_MICROTIME:
+            $format = "%Y-%m-%dT%H:%M:%s";
+            if ($this->tz->getID() == 'UTC') {
+                $format .= "Z";
+            }
+            return $this->format($format);
+            break;
+        case DATE_FORMAT_TIMESTAMP:
+            return $this->format("%Y%m%d%H%M%S");
+            break;
+        case DATE_FORMAT_UNIXTIME:
+            return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year);
+            break;
+        }
+    }
+
+    // }}}
+    // {{{ copy()
+
+    /**
+     * Copy values from another Date object
+     *
+     * Makes this Date a copy of another Date object.
+     *
+     * @access public
+     * @param object Date $date Date to copy from
+     */
+    function copy($date)
+    {
+        $this->year = $date->year;
+        $this->month = $date->month;
+        $this->day = $date->day;
+        $this->hour = $date->hour;
+        $this->minute = $date->minute;
+        $this->second = $date->second;
+        $this->tz = $date->tz;
+    }
+
+    // }}}
+    // {{{ format()
+
+    /**
+     *  Date pretty printing, similar to strftime()
+     *
+     *  Formats the date in the given format, much like
+     *  strftime().  Most strftime() options are supported.<br><br>
+     *
+     *  formatting options:<br><br>
+     *
+     *  <code>%a  </code>  abbreviated weekday name (Sun, Mon, Tue) <br>
+     *  <code>%A  </code>  full weekday name (Sunday, Monday, Tuesday) <br>
+     *  <code>%b  </code>  abbreviated month name (Jan, Feb, Mar) <br>
+     *  <code>%B  </code>  full month name (January, February, March) <br>
+     *  <code>%C  </code>  century number (the year divided by 100 and truncated to an integer, range 00 to 99) <br>
+     *  <code>%d  </code>  day of month (range 00 to 31) <br>
+     *  <code>%D  </code>  same as "%m/%d/%y" <br>
+     *  <code>%e  </code>  day of month, single digit (range 0 to 31) <br>
+     *  <code>%E  </code>  number of days since unspecified epoch (integer, Date_Calc::dateToDays()) <br>
+     *  <code>%H  </code>  hour as decimal number (00 to 23) <br>
+     *  <code>%I  </code>  hour as decimal number on 12-hour clock (01 to 12) <br>
+     *  <code>%j  </code>  day of year (range 001 to 366) <br>
+     *  <code>%m  </code>  month as decimal number (range 01 to 12) <br>
+     *  <code>%M  </code>  minute as a decimal number (00 to 59) <br>
+     *  <code>%n  </code>  newline character (\n) <br>
+     *  <code>%O  </code>  dst-corrected timezone offset expressed as "+/-HH:MM" <br>
+     *  <code>%o  </code>  raw timezone offset expressed as "+/-HH:MM" <br>
+     *  <code>%p  </code>  either 'am' or 'pm' depending on the time <br>
+     *  <code>%P  </code>  either 'AM' or 'PM' depending on the time <br>
+     *  <code>%r  </code>  time in am/pm notation, same as "%I:%M:%S %p" <br>
+     *  <code>%R  </code>  time in 24-hour notation, same as "%H:%M" <br>
+     *  <code>%s  </code>  seconds including the decimal representation smaller than one second <br>
+     *  <code>%S  </code>  seconds as a decimal number (00 to 59) <br>
+     *  <code>%t  </code>  tab character (\t) <br>
+     *  <code>%T  </code>  current time, same as "%H:%M:%S" <br>
+     *  <code>%w  </code>  weekday as decimal (0 = Sunday) <br>
+     *  <code>%U  </code>  week number of current year, first sunday as first week <br>
+     *  <code>%y  </code>  year as decimal (range 00 to 99) <br>
+     *  <code>%Y  </code>  year as decimal including century (range 0000 to 9999) <br>
+     *  <code>%%  </code>  literal '%' <br>
+     * <br>
+     *
+     * @access public
+     * @param string format the format string for returned date/time
+     * @return string date/time in given format
+     */
+    function format($format)
+    {
+        $output = "";
+
+        for($strpos = 0; $strpos < strlen($format); $strpos++) {
+            $char = substr($format,$strpos,1);
+            if ($char == "%") {
+                $nextchar = substr($format,$strpos + 1,1);
+                switch ($nextchar) {
+                case "a":
+                    $output .= Date_Calc::getWeekdayAbbrname($this->day,$this->month,$this->year, $this->getWeekdayAbbrnameLength);
+                    break;
+                case "A":
+                    $output .= Date_Calc::getWeekdayFullname($this->day,$this->month,$this->year);
+                    break;
+                case "b":
+                    $output .= Date_Calc::getMonthAbbrname($this->month);
+                    break;
+                case "B":
+                    $output .= Date_Calc::getMonthFullname($this->month);
+                    break;
+                case "C":
+                    $output .= sprintf("%02d",intval($this->year/100));
+                    break;
+                case "d":
+                    $output .= sprintf("%02d",$this->day);
+                    break;
+                case "D":
+                    $output .= sprintf("%02d/%02d/%02d",$this->month,$this->day,$this->year);
+                    break;
+                case "e":
+                    $output .= $this->day * 1; // get rid of leading zero
+                    break;
+                case "E":
+                    $output .= Date_Calc::dateToDays($this->day,$this->month,$this->year);
+                    break;
+                case "H":
+                    $output .= sprintf("%02d", $this->hour);
+                    break;
+                case 'h':
+                    $output .= sprintf("%d", $this->hour);
+                    break;
+                case "I":
+                    $hour = ($this->hour + 1) > 12 ? $this->hour - 12 : $this->hour;
+                    $output .= sprintf("%02d", $hour==0 ? 12 : $hour);
+                    break;
+                case "i":
+                    $hour = ($this->hour + 1) > 12 ? $this->hour - 12 : $this->hour;
+                    $output .= sprintf("%d", $hour==0 ? 12 : $hour);
+                    break;
+                case "j":
+                    $output .= Date_Calc::julianDate($this->day,$this->month,$this->year);
+                    break;
+                case "m":
+                    $output .= sprintf("%02d",$this->month);
+                    break;
+                case "M":
+                    $output .= sprintf("%02d",$this->minute);
+                    break;
+                case "n":
+                    $output .= "\n";
+                    break;
+                case "O":
+                    $offms = $this->tz->getOffset($this);
+                    $direction = $offms >= 0 ? "+" : "-";
+                    $offmins = abs($offms) / 1000 / 60;
+                    $hours = $offmins / 60;
+                    $minutes = $offmins % 60;
+                    $output .= sprintf("%s%02d:%02d", $direction, $hours, $minutes);
+                    break;
+                case "o":
+                    $offms = $this->tz->getRawOffset($this);
+                    $direction = $offms >= 0 ? "+" : "-";
+                    $offmins = abs($offms) / 1000 / 60;
+                    $hours = $offmins / 60;
+                    $minutes = $offmins % 60;
+                    $output .= sprintf("%s%02d:%02d", $direction, $hours, $minutes);
+                    break;
+                case "p":
+                    $output .= $this->hour >= 12 ? "pm" : "am";
+                    break;
+                case "P":
+                    $output .= $this->hour >= 12 ? "PM" : "AM";
+                    break;
+                case "r":
+                    $hour = ($this->hour + 1) > 12 ? $this->hour - 12 : $this->hour;
+                    $output .= sprintf("%02d:%02d:%02d %s", $hour==0 ?  12 : $hour, $this->minute, $this->second, $this->hour >= 12 ? "PM" : "AM");
+                    break;
+                case "R":
+                    $output .= sprintf("%02d:%02d", $this->hour, $this->minute);
+                    break;
+                case "s":
+                    $output .= str_replace(',', '.', sprintf("%09f", (float)((float)$this->second + $this->partsecond)));
+                    break;
+                case "S":
+                    $output .= sprintf("%02d", $this->second);
+                    break;
+                case "t":
+                    $output .= "\t";
+                    break;
+                case "T":
+                    $output .= sprintf("%02d:%02d:%02d", $this->hour, $this->minute, $this->second);
+                    break;
+                case "w":
+                    $output .= Date_Calc::dayOfWeek($this->day,$this->month,$this->year);
+                    break;
+                case "U":
+                    $output .= Date_Calc::weekOfYear($this->day,$this->month,$this->year);
+                    break;
+                case "y":
+                    $output .= substr($this->year,2,2);
+                    break;
+                case "Y":
+                    $output .= $this->year;
+                    break;
+                case "Z":
+                    $output .= $this->tz->inDaylightTime($this) ? $this->tz->getDSTShortName() : $this->tz->getShortName();
+                    break;
+                case "%":
+                    $output .= "%";
+                    break;
+                default:
+                    $output .= $char.$nextchar;
+                }
+                $strpos++;
+            } else {
+                $output .= $char;
+            }
+        }
+        return $output;
+
+    }
+
+    // }}}
+    // {{{ getTime()
+
+    /**
+     * Get this date/time in Unix time() format
+     *
+     * Get a representation of this date in Unix time() format.  This may only be
+     * valid for dates from 1970 to ~2038.
+     *
+     * @access public
+     * @return int number of seconds since the unix epoch
+     */
+    function getTime()
+    {
+        return $this->getDate(DATE_FORMAT_UNIXTIME);
+    }
+
+    // }}}
+    // {{{ setTZ()
+
+    /**
+     * Sets the time zone of this Date
+     *
+     * Sets the time zone of this date with the given
+     * Date_TimeZone object.  Does not alter the date/time,
+     * only assigns a new time zone.  For conversion, use
+     * convertTZ().
+     *
+     * @access public
+     * @param object Date_TimeZone $tz the Date_TimeZone object to use, if called
+     * with a paramater that is not a Date_TimeZone object, will fall through to
+     * setTZbyID().
+     */
+    function setTZ($tz)
+    {
+        if(is_a($tz, 'Date_Timezone')) {
+            $this->tz = $tz;
+        } else {
+            $this->setTZbyID($tz);
+        }
+    }
+
+    // }}}
+    // {{{ setTZbyID()
+
+    /**
+     * Sets the time zone of this date with the given time zone id
+     *
+     * Sets the time zone of this date with the given
+     * time zone id, or to the system default if the
+     * given id is invalid. Does not alter the date/time,
+     * only assigns a new time zone.  For conversion, use
+     * convertTZ().
+     *
+     * @access public
+     * @param string id a time zone id
+     */
+    function setTZbyID($id)
+    {
+        if (Date_TimeZone::isValidID($id)) {
+            $this->tz = new Date_TimeZone($id);
+        } else {
+            $this->tz = Date_TimeZone::getDefault();
+        }
+    }
+
+    // }}}
+    // {{{ inDaylightTime()
+
+    /**
+     * Tests if this date/time is in DST
+     *
+     * Returns true if daylight savings time is in effect for
+     * this date in this date's time zone.  See Date_TimeZone::inDaylightTime()
+     * for compatability information.
+     *
+     * @access public
+     * @return boolean true if DST is in effect for this date
+     */
+    function inDaylightTime()
+    {
+        return $this->tz->inDaylightTime($this);
+    }
+
+    // }}}
+    // {{{ toUTC()
+
+    /**
+     * Converts this date to UTC and sets this date's timezone to UTC
+     *
+     * Converts this date to UTC and sets this date's timezone to UTC
+     *
+     * @access public
+     */
+    function toUTC()
+    {
+        if ($this->tz->getOffset($this) > 0) {
+            $this->subtractSeconds(intval($this->tz->getOffset($this) / 1000));
+        } else {
+            $this->addSeconds(intval(abs($this->tz->getOffset($this)) / 1000));
+        }
+        $this->tz = new Date_TimeZone('UTC');
+    }
+
+    // }}}
+    // {{{ convertTZ()
+
+    /**
+     * Converts this date to a new time zone
+     *
+     * Converts this date to a new time zone.
+     * WARNING: This may not work correctly if your system does not allow
+     * putenv() or if localtime() does not work in your environment.  See
+     * Date::TimeZone::inDaylightTime() for more information.
+     *
+     * @access public
+     * @param object Date_TimeZone $tz the Date::TimeZone object for the conversion time zone
+     */
+    function convertTZ($tz)
+    {
+        // convert to UTC
+        if ($this->tz->getOffset($this) > 0) {
+            $this->subtractSeconds(intval(abs($this->tz->getOffset($this)) / 1000));
+        } else {
+            $this->addSeconds(intval(abs($this->tz->getOffset($this)) / 1000));
+        }
+        // convert UTC to new timezone
+        if ($tz->getOffset($this) > 0) {
+            $this->addSeconds(intval(abs($tz->getOffset($this)) / 1000));
+        } else {
+            $this->subtractSeconds(intval(abs($tz->getOffset($this)) / 1000));
+        }
+        $this->tz = $tz;
+    }
+
+    // }}}
+    // {{{ convertTZbyID()
+
+    /**
+     * Converts this date to a new time zone, given a valid time zone ID
+     *
+     * Converts this date to a new time zone, given a valid time zone ID
+     * WARNING: This may not work correctly if your system does not allow
+     * putenv() or if localtime() does not work in your environment.  See
+     * Date::TimeZone::inDaylightTime() for more information.
+     *
+     * @access public
+     * @param string id a time zone id
+     */
+    function convertTZbyID($id)
+    {
+       if (Date_TimeZone::isValidID($id)) {
+          $tz = new Date_TimeZone($id);
+       } else {
+          $tz = Date_TimeZone::getDefault();
+       }
+       $this->convertTZ($tz);
+    }
+
+    // }}}
+    // {{{ toUTCbyOffset()
+
+    function toUTCbyOffset($offset)
+    {
+        if ($offset == "Z" || $offset == "+00:00" || $offset == "+0000") {
+            $this->toUTC();
+            return true;
+        }
+
+        if (preg_match('/([\+\-])(\d{2}):?(\d{2})/', $offset, $regs)) {
+            // convert offset to seconds
+            $hours  = (int) isset($regs[2])?$regs[2]:0;
+            $mins   = (int) isset($regs[3])?$regs[3]:0;
+            $offset = ($hours * 3600) + ($mins * 60);
+
+            if (isset($regs[1]) && $regs[1] == "-") {
+                $offset *= -1;
+            }
+
+            if ($offset > 0) {
+                $this->subtractSeconds(intval($offset));
+            } else {
+                $this->addSeconds(intval(abs($offset)));
+            }
+
+            $this->tz = new Date_TimeZone('UTC');
+            return true;
+        }
+
+        return false;
+    }
+
+    // }}}
+    // {{{ addSeconds()
+
+    /**
+     * Adds a given number of seconds to the date
+     *
+     * Adds a given number of seconds to the date
+     *
+     * @access public
+     * @param int $sec the number of seconds to add
+     */
+    function addSeconds($sec)
+    {
+        settype($sec, 'int');
+
+        // Negative value given.
+        if ($sec < 0) {
+            $this->subtractSeconds(abs($sec));
+            return;
+        }
+
+        $this->addSpan(new Date_Span($sec));
+    }
+
+    // }}}
+    // {{{ addSpan()
+
+    /**
+     * Adds a time span to the date
+     *
+     * Adds a time span to the date
+     *
+     * @access public
+     * @param object Date_Span $span the time span to add
+     */
+    function addSpan($span)
+    {
+        if (!is_a($span, 'Date_Span')) {
+            return;
+        }
+
+        $this->second += $span->second;
+        if ($this->second >= 60) {
+            $this->minute++;
+            $this->second -= 60;
+        }
+
+        $this->minute += $span->minute;
+        if ($this->minute >= 60) {
+            $this->hour++;
+            if ($this->hour >= 24) {
+                list($this->year, $this->month, $this->day) =
+                    sscanf(Date_Calc::nextDay($this->day, $this->month, $this->year), "%04s%02s%02s");
+                $this->hour -= 24;
+            }
+            $this->minute -= 60;
+        }
+
+        $this->hour += $span->hour;
+        if ($this->hour >= 24) {
+            list($this->year, $this->month, $this->day) =
+                sscanf(Date_Calc::nextDay($this->day, $this->month, $this->year), "%04s%02s%02s");
+            $this->hour -= 24;
+        }
+
+        $d = Date_Calc::dateToDays($this->day, $this->month, $this->year);
+        $d += $span->day;
+
+        list($this->year, $this->month, $this->day) =
+            sscanf(Date_Calc::daysToDate($d), "%04s%02s%02s");
+        $this->year  = intval($this->year);
+        $this->month = intval($this->month);
+        $this->day   = intval($this->day);
+    }
+
+    // }}}
+    // {{{ subtractSeconds()
+
+    /**
+     * Subtracts a given number of seconds from the date
+     *
+     * Subtracts a given number of seconds from the date
+     *
+     * @access public
+     * @param int $sec the number of seconds to subtract
+     */
+    function subtractSeconds($sec)
+    {
+        settype($sec, 'int');
+
+        // Negative value given.
+        if ($sec < 0) {
+            $this->addSeconds(abs($sec));
+            return;
+        }
+
+        $this->subtractSpan(new Date_Span($sec));
+    }
+
+    // }}}
+    // {{{ subtractSpan()
+
+    /**
+     * Subtracts a time span to the date
+     *
+     * Subtracts a time span to the date
+     *
+     * @access public
+     * @param object Date_Span $span the time span to subtract
+     */
+    function subtractSpan($span)
+    {
+        if (!is_a($span, 'Date_Span')) {
+            return;
+        }
+        if ($span->isEmpty()) {
+            return;
+        }
+
+        $this->second -= $span->second;
+        if ($this->second < 0) {
+            $this->minute--;
+            $this->second += 60;
+        }
+
+        $this->minute -= $span->minute;
+        if ($this->minute < 0) {
+            $this->hour--;
+            if ($this->hour < 0) {
+                list($this->year, $this->month, $this->day) =
+                    sscanf(Date_Calc::prevDay($this->day, $this->month, $this->year), "%04s%02s%02s");
+                $this->hour += 24;
+            }
+            $this->minute += 60;
+        }
+
+        $this->hour -= $span->hour;
+        if ($this->hour < 0) {
+            list($this->year, $this->month, $this->day) =
+                sscanf(Date_Calc::prevDay($this->day, $this->month, $this->year), "%04s%02s%02s");
+            $this->hour += 24;
+        }
+
+        $d = Date_Calc::dateToDays($this->day, $this->month, $this->year);
+        $d -= $span->day;
+
+        list($this->year, $this->month, $this->day) =
+            sscanf(Date_Calc::daysToDate($d), "%04s%02s%02s");
+        $this->year  = intval($this->year);
+        $this->month = intval($this->month);
+        $this->day   = intval($this->day);
+    }
+
+    // }}}
+    // {{{ compare()
+
+    /**
+     * Compares two dates
+     *
+     * Compares two dates.  Suitable for use
+     * in sorting functions.
+     *
+     * @access public
+     * @param object Date $d1 the first date
+     * @param object Date $d2 the second date
+     * @return int 0 if the dates are equal, -1 if d1 is before d2, 1 if d1 is after d2
+     */
+    function compare($d1, $d2)
+    {
+        $d1->convertTZ(new Date_TimeZone('UTC'));
+        $d2->convertTZ(new Date_TimeZone('UTC'));
+        $days1 = Date_Calc::dateToDays($d1->day, $d1->month, $d1->year);
+        $days2 = Date_Calc::dateToDays($d2->day, $d2->month, $d2->year);
+        if ($days1 < $days2) return -1;
+        if ($days1 > $days2) return 1;
+        if ($d1->hour < $d2->hour) return -1;
+        if ($d1->hour > $d2->hour) return 1;
+        if ($d1->minute < $d2->minute) return -1;
+        if ($d1->minute > $d2->minute) return 1;
+        if ($d1->second < $d2->second) return -1;
+        if ($d1->second > $d2->second) return 1;
+        return 0;
+    }
+
+    // }}}
+    // {{{ before()
+
+    /**
+     * Test if this date/time is before a certain date/time
+     *
+     * Test if this date/time is before a certain date/time
+     *
+     * @access public
+     * @param object Date $when the date to test against
+     * @return boolean true if this date is before $when
+     */
+    function before($when)
+    {
+        if (Date::compare($this,$when) == -1) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ after()
+
+    /**
+     * Test if this date/time is after a certian date/time
+     *
+     * Test if this date/time is after a certian date/time
+     *
+     * @access public
+     * @param object Date $when the date to test against
+     * @return boolean true if this date is after $when
+     */
+    function after($when)
+    {
+        if (Date::compare($this,$when) == 1) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ equals()
+
+    /**
+     * Test if this date/time is exactly equal to a certian date/time
+     *
+     * Test if this date/time is exactly equal to a certian date/time
+     *
+     * @access public
+     * @param object Date $when the date to test against
+     * @return boolean true if this date is exactly equal to $when
+     */
+    function equals($when)
+    {
+        if (Date::compare($this,$when) == 0) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ isFuture()
+
+    /**
+     * Determine if this date is in the future
+     *
+     * Determine if this date is in the future
+     *
+     * @access public
+     * @return boolean true if this date is in the future
+     */
+    function isFuture()
+    {
+        $now = new Date();
+        if ($this->after($now)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ isPast()
+
+    /**
+     * Determine if this date is in the past
+     *
+     * Determine if this date is in the past
+     *
+     * @access public
+     * @return boolean true if this date is in the past
+     */
+    function isPast()
+    {
+        $now = new Date();
+        if ($this->before($now)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ isLeapYear()
+
+    /**
+     * Determine if the year in this date is a leap year
+     *
+     * Determine if the year in this date is a leap year
+     *
+     * @access public
+     * @return boolean true if this year is a leap year
+     */
+    function isLeapYear()
+    {
+        return Date_Calc::isLeapYear($this->year);
+    }
+
+    // }}}
+    // {{{ getJulianDate()
+
+    /**
+     * Get the Julian date for this date
+     *
+     * Get the Julian date for this date
+     *
+     * @access public
+     * @return int the Julian date
+     */
+    function getJulianDate()
+    {
+        return Date_Calc::julianDate($this->day, $this->month, $this->year);
+    }
+
+    // }}}
+    // {{{ getDayOfWeek()
+
+    /**
+     * Gets the day of the week for this date
+     *
+     * Gets the day of the week for this date (0=Sunday)
+     *
+     * @access public
+     * @return int the day of the week (0=Sunday)
+     */
+    function getDayOfWeek()
+    {
+        return Date_Calc::dayOfWeek($this->day, $this->month, $this->year);
+    }
+
+    // }}}
+    // {{{ getWeekOfYear()
+
+    /**
+     * Gets the week of the year for this date
+     *
+     * Gets the week of the year for this date
+     *
+     * @access public
+     * @return int the week of the year
+     */
+    function getWeekOfYear()
+    {
+        return Date_Calc::weekOfYear($this->day, $this->month, $this->year);
+    }
+
+    // }}}
+    // {{{ getQuarterOfYear()
+
+    /**
+     * Gets the quarter of the year for this date
+     *
+     * Gets the quarter of the year for this date
+     *
+     * @access public
+     * @return int the quarter of the year (1-4)
+     */
+    function getQuarterOfYear()
+    {
+        return Date_Calc::quarterOfYear($this->day, $this->month, $this->year);
+    }
+
+    // }}}
+    // {{{ getDaysInMonth()
+
+    /**
+     * Gets number of days in the month for this date
+     *
+     * Gets number of days in the month for this date
+     *
+     * @access public
+     * @return int number of days in this month
+     */
+    function getDaysInMonth()
+    {
+        return Date_Calc::daysInMonth($this->month, $this->year);
+    }
+
+    // }}}
+    // {{{ getWeeksInMonth()
+
+    /**
+     * Gets the number of weeks in the month for this date
+     *
+     * Gets the number of weeks in the month for this date
+     *
+     * @access public
+     * @return int number of weeks in this month
+     */
+    function getWeeksInMonth()
+    {
+        return Date_Calc::weeksInMonth($this->month, $this->year);
+    }
+
+    // }}}
+    // {{{ getDayName()
+
+    /**
+     * Gets the full name or abbriviated name of this weekday
+     *
+     * Gets the full name or abbriviated name of this weekday
+     *
+     * @access public
+     * @param boolean $abbr abbrivate the name
+     * @return string name of this day
+     */
+    function getDayName($abbr = false, $length = 3)
+    {
+        if ($abbr) {
+            return Date_Calc::getWeekdayAbbrname($this->day, $this->month, $this->year, $length);
+        } else {
+            return Date_Calc::getWeekdayFullname($this->day, $this->month, $this->year);
+        }
+    }
+
+    // }}}
+    // {{{ getMonthName()
+
+    /**
+     * Gets the full name or abbriviated name of this month
+     *
+     * Gets the full name or abbriviated name of this month
+     *
+     * @access public
+     * @param boolean $abbr abbrivate the name
+     * @return string name of this month
+     */
+    function getMonthName($abbr = false)
+    {
+        if ($abbr) {
+            return Date_Calc::getMonthAbbrname($this->month);
+        } else {
+            return Date_Calc::getMonthFullname($this->month);
+        }
+    }
+
+    // }}}
+    // {{{ getNextDay()
+
+    /**
+     * Get a Date object for the day after this one
+     *
+     * Get a Date object for the day after this one.
+     * The time of the returned Date object is the same as this time.
+     *
+     * @access public
+     * @return object Date Date representing the next day
+     */
+    function getNextDay()
+    {
+        $day = Date_Calc::nextDay($this->day, $this->month, $this->year, "%Y-%m-%d");
+        $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second);
+        $newDate = new Date();
+        $newDate->setDate($date);
+        return $newDate;
+    }
+
+    // }}}
+    // {{{ getPrevDay()
+
+    /**
+     * Get a Date object for the day before this one
+     *
+     * Get a Date object for the day before this one.
+     * The time of the returned Date object is the same as this time.
+     *
+     * @access public
+     * @return object Date Date representing the previous day
+     */
+    function getPrevDay()
+    {
+        $day = Date_Calc::prevDay($this->day, $this->month, $this->year, "%Y-%m-%d");
+        $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second);
+        $newDate = new Date();
+        $newDate->setDate($date);
+        return $newDate;
+    }
+
+    // }}}
+    // {{{ getNextWeekday()
+
+    /**
+     * Get a Date object for the weekday after this one
+     *
+     * Get a Date object for the weekday after this one.
+     * The time of the returned Date object is the same as this time.
+     *
+     * @access public
+     * @return object Date Date representing the next weekday
+     */
+    function getNextWeekday()
+    {
+        $day = Date_Calc::nextWeekday($this->day, $this->month, $this->year, "%Y-%m-%d");
+        $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second);
+        $newDate = new Date();
+        $newDate->setDate($date);
+        return $newDate;
+    }
+
+    // }}}
+    // {{{ getPrevWeekday()
+
+    /**
+     * Get a Date object for the weekday before this one
+     *
+     * Get a Date object for the weekday before this one.
+     * The time of the returned Date object is the same as this time.
+     *
+     * @access public
+     * @return object Date Date representing the previous weekday
+     */
+    function getPrevWeekday()
+    {
+        $day = Date_Calc::prevWeekday($this->day, $this->month, $this->year, "%Y-%m-%d");
+        $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second);
+        $newDate = new Date();
+        $newDate->setDate($date);
+        return $newDate;
+    }
+
+    // }}}
+    // {{{ getYear()
+
+    /**
+     * Returns the year field of the date object
+     *
+     * Returns the year field of the date object
+     *
+     * @access public
+     * @return int the year
+     */
+    function getYear()
+    {
+        return (int)$this->year;
+    }
+
+    // }}}
+    // {{{ getMonth()
+
+    /**
+     * Returns the month field of the date object
+     *
+     * Returns the month field of the date object
+     *
+     * @access public
+     * @return int the month
+     */
+    function getMonth()
+    {
+        return (int)$this->month;
+    }
+
+    // }}}
+    // {{{ getDay()
+
+    /**
+     * Returns the day field of the date object
+     *
+     * Returns the day field of the date object
+     *
+     * @access public
+     * @return int the day
+     */
+    function getDay()
+    {
+        return (int)$this->day;
+    }
+
+    // }}}
+    // {{{ getHour()
+
+    /**
+     * Returns the hour field of the date object
+     *
+     * Returns the hour field of the date object
+     *
+     * @access public
+     * @return int the hour
+     */
+    function getHour()
+    {
+        return $this->hour;
+    }
+
+    // }}}
+    // {{{ getMinute()
+
+    /**
+     * Returns the minute field of the date object
+     *
+     * Returns the minute field of the date object
+     *
+     * @access public
+     * @return int the minute
+     */
+    function getMinute()
+    {
+        return $this->minute;
+    }
+
+    // }}}
+    // {{{ getSecond()
+
+    /**
+     * Returns the second field of the date object
+     *
+     * Returns the second field of the date object
+     *
+     * @access public
+     * @return int the second
+     */
+    function getSecond()
+    {
+         return $this->second;
+    }
+
+    // }}}
+    // {{{ setYear()
+
+    /**
+     * Set the year field of the date object
+     *
+     * Set the year field of the date object, invalid years (not 0-9999) are set to 0.
+     *
+     * @access public
+     * @param int $y the year
+     */
+    function setYear($y)
+    {
+        if ($y < 0 || $y > 9999) {
+            $this->year = 0;
+        } else {
+            $this->year = $y;
+        }
+    }
+
+    // }}}
+    // {{{ setMonth()
+
+    /**
+     * Set the month field of the date object
+     *
+     * Set the month field of the date object, invalid months (not 1-12) are set to 1.
+     *
+     * @access public
+     * @param int $m the month
+     */
+    function setMonth($m)
+    {
+        if ($m < 1 || $m > 12) {
+            $this->month = 1;
+        } else {
+            $this->month = $m;
+        }
+    }
+
+    // }}}
+    // {{{ setDay()
+
+    /**
+     * Set the day field of the date object
+     *
+     * Set the day field of the date object, invalid days (not 1-31) are set to 1.
+     *
+     * @access public
+     * @param int $d the day
+     */
+    function setDay($d)
+    {
+        if ($d > 31 || $d < 1) {
+            $this->day = 1;
+        } else {
+            $this->day = $d;
+        }
+    }
+
+    // }}}
+    // {{{ setHour()
+
+    /**
+     * Set the hour field of the date object
+     *
+     * Set the hour field of the date object in 24-hour format.
+     * Invalid hours (not 0-23) are set to 0.
+     *
+     * @access public
+     * @param int $h the hour
+     */
+    function setHour($h)
+    {
+        if ($h > 23 || $h < 0) {
+            $this->hour = 0;
+        } else {
+            $this->hour = $h;
+        }
+    }
+
+    // }}}
+    // {{{ setMinute()
+
+    /**
+     * Set the minute field of the date object
+     *
+     * Set the minute field of the date object, invalid minutes (not 0-59) are set to 0.
+     *
+     * @access public
+     * @param int $m the minute
+     */
+    function setMinute($m)
+    {
+        if ($m > 59 || $m < 0) {
+            $this->minute = 0;
+        } else {
+            $this->minute = $m;
+        }
+    }
+
+    // }}}
+    // {{{ setSecond()
+
+    /**
+     * Set the second field of the date object
+     *
+     * Set the second field of the date object, invalid seconds (not 0-59) are set to 0.
+     *
+     * @access public
+     * @param int $s the second
+     */
+    function setSecond($s) {
+        if ($s > 59 || $s < 0) {
+            $this->second = 0;
+        } else {
+            $this->second = $s;
+        }
+    }
+
+    // }}}
+}
+
+// }}}
+
+/*
+ * Local variables:
+ * mode: php
+ * tab-width: 4
+ * c-basic-offset: 4
+ * c-hanging-comment-ender-p: nil
+ * End:
+ */
+?>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/TimeZone.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/TimeZone.php	(revision 2079)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/TimeZone.php	(revision 2079)
@@ -0,0 +1,4731 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
+
+// {{{ Header
+
+/**
+ * TimeZone representation class, along with time zone information data
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 1997-2006 Baba Buehler, Pierre-Alain Joye
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted under the terms of the BSD License.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE
+ * COPYRIGHT OWNER OR 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.
+ *
+ * @category   Date and Time
+ * @package    Date
+ * @author     Baba Buehler <baba@babaz.com>
+ * @author     Pierre-Alain Joye <pajoye@php.net>
+ * @copyright  1997-2006 Baba Buehler, Pierre-Alain Joye
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    CVS: $Id: TimeZone.php,v 1.14 2006/11/22 01:03:12 firman Exp $
+ * @link       http://pear.php.net/package/Date
+ */
+
+// }}}
+// {{{ Class: Date_TimeZone
+
+/**
+ * TimeZone representation class, along with time zone information data
+ *
+ * The default timezone is set from the first valid timezone id found
+ * in one of the following places, in this order:
+ *   + global $_DATE_TIMEZONE_DEFAULT
+ *   + system environment variable PHP_TZ
+ *   + system environment variable TZ
+ *   + the result of date('T')
+ *
+ * If no valid timezone id is found, the default timezone is set to 'UTC'.
+ * You may also manually set the default timezone by passing a valid id to
+ * Date_TimeZone::setDefault().
+ *
+ * This class includes time zone data (from zoneinfo) in the form of a
+ * global array, $_DATE_TIMEZONE_DATA.
+ *
+ * @author     Baba Buehler <baba@babaz.com>
+ * @copyright  1997-2006 Baba Buehler, Pierre-Alain Joye
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    Release: 1.4.7
+ * @link       http://pear.php.net/package/Date
+ */
+class Date_TimeZone
+{
+    // {{{ Properties
+
+    /**
+     * Time Zone ID of this time zone
+     * @var string
+     */
+    var $id;
+
+    /**
+     * Long Name of this time zone (ie Central Standard Time)
+     * @var string
+     */
+    var $longname;
+
+    /**
+     * Short Name of this time zone (ie CST)
+     * @var string
+     */
+    var $shortname;
+
+    /**
+     * true if this time zone observes daylight savings time
+     * @var boolean
+     */
+    var $hasdst;
+
+    /**
+     * DST Long Name of this time zone
+     * @var string
+     */
+    var $dstlongname;
+
+    /**
+     * DST Short Name of this timezone
+     * @var string
+     */
+    var $dstshortname;
+
+    /**
+     * offset, in milliseconds, of this timezone
+     * @var int
+     */
+    var $offset;
+
+    /**
+     * System Default Time Zone
+     * @var object Date_TimeZone
+     */
+    var $default;
+
+    // }}}
+    // {{{ Constructor
+
+    /**
+     * Constructor
+     *
+     * Creates a new Date::TimeZone object, representing the time zone
+     * specified in $id.  If the supplied ID is invalid, the created
+     * time zone is UTC.
+     *
+     * @access public
+     * @param string $id the time zone id
+     * @return object Date_TimeZone the new Date_TimeZone object
+     */
+    function Date_TimeZone($id)
+    {
+        $_DATE_TIMEZONE_DATA =& $GLOBALS['_DATE_TIMEZONE_DATA'];
+        if(Date_TimeZone::isValidID($id)) {
+            $this->id = $id;
+            $this->longname = $_DATE_TIMEZONE_DATA[$id]['longname'];
+            $this->shortname = $_DATE_TIMEZONE_DATA[$id]['shortname'];
+            $this->offset = $_DATE_TIMEZONE_DATA[$id]['offset'];
+            if($_DATE_TIMEZONE_DATA[$id]['hasdst']) {
+                $this->hasdst = true;
+                $this->dstlongname = $_DATE_TIMEZONE_DATA[$id]['dstlongname'];
+                $this->dstshortname = $_DATE_TIMEZONE_DATA[$id]['dstshortname'];
+            } else {
+                $this->hasdst = false;
+                $this->dstlongname = $this->longname;
+                $this->dstshortname = $this->shortname;
+            }
+        } else {
+            $this->id = 'UTC';
+            $this->longname = $_DATE_TIMEZONE_DATA[$this->id]['longname'];
+            $this->shortname = $_DATE_TIMEZONE_DATA[$this->id]['shortname'];
+            $this->hasdst = $_DATE_TIMEZONE_DATA[$this->id]['hasdst'];
+            $this->offset = $_DATE_TIMEZONE_DATA[$this->id]['offset'];
+        }
+    }
+
+    // }}}
+    // {{{ getDefault()
+
+    /**
+     * Return a TimeZone object representing the system default time zone
+     *
+     * Return a TimeZone object representing the system default time zone,
+     * which is initialized during the loading of TimeZone.php.
+     *
+     * @access public
+     * @return object Date_TimeZone the default time zone
+     */
+    function getDefault()
+    {
+        return new Date_TimeZone($GLOBALS['_DATE_TIMEZONE_DEFAULT']);
+    }
+
+    // }}}
+    // {{{ setDefault()
+
+    /**
+     * Sets the system default time zone to the time zone in $id
+     *
+     * Sets the system default time zone to the time zone in $id
+     *
+     * @access public
+     * @param string $id the time zone id to use
+     */
+    function setDefault($id)
+    {
+        if(Date_TimeZone::isValidID($id)) {
+            $GLOBALS['_DATE_TIMEZONE_DEFAULT'] = $id;
+        }
+    }
+
+    // }}}
+    // {{{ isValidID()
+
+    /**
+     * Tests if given id is represented in the $_DATE_TIMEZONE_DATA time zone data
+     *
+     * Tests if given id is represented in the $_DATE_TIMEZONE_DATA time zone data
+     *
+     * @access public
+     * @param string $id the id to test
+     * @return boolean true if the supplied ID is valid
+     */
+    function isValidID($id)
+    {
+        if(isset($GLOBALS['_DATE_TIMEZONE_DATA'][$id])) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ isEqual()
+
+    /**
+     * Is this time zone equal to another
+     *
+     * Tests to see if this time zone is equal (ids match)
+     * to a given Date_TimeZone object.
+     *
+     * @access public
+     * @param object Date_TimeZone $tz the timezone to test
+     * @return boolean true if this time zone is equal to the supplied time zone
+     */
+    function isEqual($tz)
+    {
+        if(strcasecmp($this->id, $tz->id) == 0) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ isEquivalent()
+
+    /**
+     * Is this time zone equivalent to another
+     *
+     * Tests to see if this time zone is equivalent to
+     * a given time zone object.  Equivalence in this context
+     * is defined by the two time zones having an equal raw
+     * offset and an equal setting of "hasdst".  This is not true
+     * equivalence, as the two time zones may have different rules
+     * for the observance of DST, but this implementation does not
+     * know DST rules.
+     *
+     * @access public
+     * @param object Date_TimeZone $tz the timezone object to test
+     * @return boolean true if this time zone is equivalent to the supplied time zone
+     */
+    function isEquivalent($tz)
+    {
+        if($this->offset == $tz->offset && $this->hasdst == $tz->hasdst) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ hasDaylightTime()
+
+    /**
+     * Returns true if this zone observes daylight savings time
+     *
+     * Returns true if this zone observes daylight savings time
+     *
+     * @access public
+     * @return boolean true if this time zone has DST
+     */
+    function hasDaylightTime()
+    {
+        return $this->hasdst;
+    }
+
+    // }}}
+    // {{{ inDaylightTime()
+
+    /**
+     * Is the given date/time in DST for this time zone
+     *
+     * Attempts to determine if a given Date object represents a date/time
+     * that is in DST for this time zone.  WARNINGS: this basically attempts to
+     * "trick" the system into telling us if we're in DST for a given time zone.
+     * This uses putenv() which may not work in safe mode, and relies on unix time
+     * which is only valid for dates from 1970 to ~2038.  This relies on the
+     * underlying OS calls, so it may not work on Windows or on a system where
+     * zoneinfo is not installed or configured properly.
+     *
+     * @access public
+     * @param object Date $date the date/time to test
+     * @return boolean true if this date is in DST for this time zone
+     */
+    function inDaylightTime($date)
+    {
+        $env_tz = '';
+        if(isset($_ENV['TZ']) && getenv('TZ')) {
+            $env_tz = getenv('TZ');
+        }
+
+        putenv('TZ=' . $this->id);
+        $ltime = localtime($date->getTime(), true);
+        if ($env_tz != '') {
+            putenv('TZ=' . $env_tz);
+        }
+        return $ltime['tm_isdst'];
+    }
+
+    // }}}
+    // {{{ getDSTSavings()
+
+    /**
+     * Get the DST offset for this time zone
+     *
+     * Returns the DST offset of this time zone, in milliseconds,
+     * if the zone observes DST, zero otherwise.  Currently the
+     * DST offset is hard-coded to one hour.
+     *
+     * @access public
+     * @return int the DST offset, in milliseconds or zero if the zone does not observe DST
+     */
+    function getDSTSavings()
+    {
+        if($this->hasdst) {
+            return 3600000;
+        } else {
+            return 0;
+        }
+    }
+
+    // }}}
+    // {{{ getOffset()
+
+    /**
+     * Get the DST-corrected offset to UTC for the given date
+     *
+     * Attempts to get the offset to UTC for a given date/time, taking into
+     * account daylight savings time, if the time zone observes it and if
+     * it is in effect.  Please see the WARNINGS on Date::TimeZone::inDaylightTime().
+     *
+     *
+     * @access public
+     * @param object Date $date the Date to test
+     * @return int the corrected offset to UTC in milliseconds
+     */
+    function getOffset($date)
+    {
+        if($this->inDaylightTime($date)) {
+            return $this->offset + $this->getDSTSavings();
+        } else {
+            return $this->offset;
+        }
+    }
+
+    // }}}
+    // {{{ getAvailableIDs()
+
+    /**
+     * Returns the list of valid time zone id strings
+     *
+     * Returns the list of valid time zone id strings
+     *
+     * @access public
+     * @return mixed an array of strings with the valid time zone IDs
+     */
+    function getAvailableIDs()
+    {
+        return array_keys($GLOBALS['_DATE_TIMEZONE_DATA']);
+    }
+
+    // }}}
+    // {{{ getID()
+
+    /**
+     * Returns the id for this time zone
+     *
+     * Returns the time zone id  for this time zone, i.e. "America/Chicago"
+     *
+     * @access public
+     * @return string the id
+     */
+    function getID()
+    {
+        return $this->id;
+    }
+
+    // }}}
+    // {{{ getLongName()
+
+    /**
+     * Returns the long name for this time zone
+     *
+     * Returns the long name for this time zone,
+     * i.e. "Central Standard Time"
+     *
+     * @access public
+     * @return string the long name
+     */
+    function getLongName()
+    {
+        return $this->longname;
+    }
+
+    // }}}
+    // {{{ getShortName()
+
+    /**
+     * Returns the short name for this time zone
+     *
+     * Returns the short name for this time zone, i.e. "CST"
+     *
+     * @access public
+     * @return string the short name
+     */
+    function getShortName()
+    {
+        return $this->shortname;
+    }
+
+    // }}}
+    // {{{ getDSTLongName()
+
+    /**
+     * Returns the DST long name for this time zone
+     *
+     * Returns the DST long name for this time zone, i.e. "Central Daylight Time"
+     *
+     * @access public
+     * @return string the daylight savings time long name
+     */
+    function getDSTLongName()
+    {
+        return $this->dstlongname;
+    }
+
+    // }}}
+    // {{{ getDSTShortName()
+
+    /**
+     * Returns the DST short name for this time zone
+     *
+     * Returns the DST short name for this time zone, i.e. "CDT"
+     *
+     * @access public
+     * @return string the daylight savings time short name
+     */
+    function getDSTShortName()
+    {
+        return $this->dstshortname;
+    }
+
+    // }}}
+    // {{{ getRawOffset()
+
+    /**
+     * Returns the raw (non-DST-corrected) offset from UTC/GMT for this time zone
+     *
+     * Returns the raw (non-DST-corrected) offset from UTC/GMT for this time zone
+     *
+     * @access public
+     * @return int the offset, in milliseconds
+     */
+    function getRawOffset()
+    {
+        return $this->offset;
+    }
+
+    // }}}
+}
+
+// }}}
+
+/**
+ * Time Zone Data offset is in miliseconds
+ *
+ * @global array $GLOBALS['_DATE_TIMEZONE_DATA']
+ */
+$GLOBALS['_DATE_TIMEZONE_DATA'] = array(
+    'Etc/GMT+12' => array(
+        'offset' => -43200000,
+        'longname' => 'GMT-12:00',
+        'shortname' => 'GMT-12:00',
+        'hasdst' => false ),
+    'Etc/GMT+11' => array(
+        'offset' => -39600000,
+        'longname' => 'GMT-11:00',
+        'shortname' => 'GMT-11:00',
+        'hasdst' => false ),
+    'MIT' => array(
+        'offset' => -39600000,
+        'longname' => 'West Samoa Time',
+        'shortname' => 'WST',
+        'hasdst' => false ),
+    'Pacific/Apia' => array(
+        'offset' => -39600000,
+        'longname' => 'West Samoa Time',
+        'shortname' => 'WST',
+        'hasdst' => false ),
+    'Pacific/Midway' => array(
+        'offset' => -39600000,
+        'longname' => 'Samoa Standard Time',
+        'shortname' => 'SST',
+        'hasdst' => false ),
+    'Pacific/Niue' => array(
+        'offset' => -39600000,
+        'longname' => 'Niue Time',
+        'shortname' => 'NUT',
+        'hasdst' => false ),
+    'Pacific/Pago_Pago' => array(
+        'offset' => -39600000,
+        'longname' => 'Samoa Standard Time',
+        'shortname' => 'SST',
+        'hasdst' => false ),
+    'Pacific/Samoa' => array(
+        'offset' => -39600000,
+        'longname' => 'Samoa Standard Time',
+        'shortname' => 'SST',
+        'hasdst' => false ),
+    'US/Samoa' => array(
+        'offset' => -39600000,
+        'longname' => 'Samoa Standard Time',
+        'shortname' => 'SST',
+        'hasdst' => false ),
+    'America/Adak' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii-Aleutian Standard Time',
+        'shortname' => 'HAST',
+        'hasdst' => true,
+        'dstlongname' => 'Hawaii-Aleutian Daylight Time',
+        'dstshortname' => 'HADT' ),
+    'America/Atka' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii-Aleutian Standard Time',
+        'shortname' => 'HAST',
+        'hasdst' => true,
+        'dstlongname' => 'Hawaii-Aleutian Daylight Time',
+        'dstshortname' => 'HADT' ),
+    'Etc/GMT+10' => array(
+        'offset' => -36000000,
+        'longname' => 'GMT-10:00',
+        'shortname' => 'GMT-10:00',
+        'hasdst' => false ),
+    'HST' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii Standard Time',
+        'shortname' => 'HST',
+        'hasdst' => false ),
+    'Pacific/Fakaofo' => array(
+        'offset' => -36000000,
+        'longname' => 'Tokelau Time',
+        'shortname' => 'TKT',
+        'hasdst' => false ),
+    'Pacific/Honolulu' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii Standard Time',
+        'shortname' => 'HST',
+        'hasdst' => false ),
+    'Pacific/Johnston' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii Standard Time',
+        'shortname' => 'HST',
+        'hasdst' => false ),
+    'Pacific/Rarotonga' => array(
+        'offset' => -36000000,
+        'longname' => 'Cook Is. Time',
+        'shortname' => 'CKT',
+        'hasdst' => false ),
+    'Pacific/Tahiti' => array(
+        'offset' => -36000000,
+        'longname' => 'Tahiti Time',
+        'shortname' => 'TAHT',
+        'hasdst' => false ),
+    'SystemV/HST10' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii Standard Time',
+        'shortname' => 'HST',
+        'hasdst' => false ),
+    'US/Aleutian' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii-Aleutian Standard Time',
+        'shortname' => 'HAST',
+        'hasdst' => true,
+        'dstlongname' => 'Hawaii-Aleutian Daylight Time',
+        'dstshortname' => 'HADT' ),
+    'US/Hawaii' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii Standard Time',
+        'shortname' => 'HST',
+        'hasdst' => false ),
+    'Pacific/Marquesas' => array(
+        'offset' => -34200000,
+        'longname' => 'Marquesas Time',
+        'shortname' => 'MART',
+        'hasdst' => false ),
+    'AST' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'America/Anchorage' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'America/Juneau' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'America/Nome' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'America/Yakutat' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'Etc/GMT+9' => array(
+        'offset' => -32400000,
+        'longname' => 'GMT-09:00',
+        'shortname' => 'GMT-09:00',
+        'hasdst' => false ),
+    'Pacific/Gambier' => array(
+        'offset' => -32400000,
+        'longname' => 'Gambier Time',
+        'shortname' => 'GAMT',
+        'hasdst' => false ),
+    'SystemV/YST9' => array(
+        'offset' => -32400000,
+        'longname' => 'Gambier Time',
+        'shortname' => 'GAMT',
+        'hasdst' => false ),
+    'SystemV/YST9YDT' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'US/Alaska' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'America/Dawson' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'America/Ensenada' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'America/Los_Angeles' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'America/Tijuana' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'America/Vancouver' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'America/Whitehorse' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'Canada/Pacific' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'Canada/Yukon' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'Etc/GMT+8' => array(
+        'offset' => -28800000,
+        'longname' => 'GMT-08:00',
+        'shortname' => 'GMT-08:00',
+        'hasdst' => false ),
+    'Mexico/BajaNorte' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'PST' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'PST8PDT' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'Pacific/Pitcairn' => array(
+        'offset' => -28800000,
+        'longname' => 'Pitcairn Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => false ),
+    'SystemV/PST8' => array(
+        'offset' => -28800000,
+        'longname' => 'Pitcairn Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => false ),
+    'SystemV/PST8PDT' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'US/Pacific' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'US/Pacific-New' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'America/Boise' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Cambridge_Bay' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Chihuahua' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Dawson_Creek' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => false ),
+    'America/Denver' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Edmonton' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Hermosillo' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => false ),
+    'America/Inuvik' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Mazatlan' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Phoenix' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => false ),
+    'America/Shiprock' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Yellowknife' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'Canada/Mountain' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'Etc/GMT+7' => array(
+        'offset' => -25200000,
+        'longname' => 'GMT-07:00',
+        'shortname' => 'GMT-07:00',
+        'hasdst' => false ),
+    'MST' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'MST7MDT' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'Mexico/BajaSur' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'Navajo' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'PNT' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => false ),
+    'SystemV/MST7' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => false ),
+    'SystemV/MST7MDT' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'US/Arizona' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => false ),
+    'US/Mountain' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'America/Belize' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Cancun' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Chicago' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Costa_Rica' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/El_Salvador' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Guatemala' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Managua' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Menominee' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Merida' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Mexico_City' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Monterrey' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/North_Dakota/Center' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Rainy_River' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Rankin_Inlet' => array(
+        'offset' => -21600000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Regina' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Swift_Current' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Tegucigalpa' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'America/Winnipeg' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'CST' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'CST6CDT' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'Canada/Central' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'Canada/East-Saskatchewan' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Canada/Saskatchewan' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Chile/EasterIsland' => array(
+        'offset' => -21600000,
+        'longname' => 'Easter Is. Time',
+        'shortname' => 'EAST',
+        'hasdst' => true,
+        'dstlongname' => 'Easter Is. Summer Time',
+        'dstshortname' => 'EASST' ),
+    'Etc/GMT+6' => array(
+        'offset' => -21600000,
+        'longname' => 'GMT-06:00',
+        'shortname' => 'GMT-06:00',
+        'hasdst' => false ),
+    'Mexico/General' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Pacific/Easter' => array(
+        'offset' => -21600000,
+        'longname' => 'Easter Is. Time',
+        'shortname' => 'EAST',
+        'hasdst' => true,
+        'dstlongname' => 'Easter Is. Summer Time',
+        'dstshortname' => 'EASST' ),
+    'Pacific/Galapagos' => array(
+        'offset' => -21600000,
+        'longname' => 'Galapagos Time',
+        'shortname' => 'GALT',
+        'hasdst' => false ),
+    'SystemV/CST6' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'SystemV/CST6CDT' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'US/Central' => array(
+        'offset' => -21600000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Bogota' => array(
+        'offset' => -18000000,
+        'longname' => 'Colombia Time',
+        'shortname' => 'COT',
+        'hasdst' => false ),
+    'America/Cayman' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Detroit' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Eirunepe' => array(
+        'offset' => -18000000,
+        'longname' => 'Acre Time',
+        'shortname' => 'ACT',
+        'hasdst' => false ),
+    'America/Fort_Wayne' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Grand_Turk' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Guayaquil' => array(
+        'offset' => -18000000,
+        'longname' => 'Ecuador Time',
+        'shortname' => 'ECT',
+        'hasdst' => false ),
+    'America/Havana' => array(
+        'offset' => -18000000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'America/Indiana/Indianapolis' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Indiana/Knox' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Indiana/Marengo' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Indiana/Vevay' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Indianapolis' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Iqaluit' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Jamaica' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Kentucky/Louisville' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Kentucky/Monticello' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Knox_IN' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Lima' => array(
+        'offset' => -18000000,
+        'longname' => 'Peru Time',
+        'shortname' => 'PET',
+        'hasdst' => false ),
+    'America/Louisville' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Montreal' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Nassau' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/New_York' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Nipigon' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Panama' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Pangnirtung' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Port-au-Prince' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'America/Porto_Acre' => array(
+        'offset' => -18000000,
+        'longname' => 'Acre Time',
+        'shortname' => 'ACT',
+        'hasdst' => false ),
+    'America/Rio_Branco' => array(
+        'offset' => -18000000,
+        'longname' => 'Acre Time',
+        'shortname' => 'ACT',
+        'hasdst' => false ),
+    'America/Thunder_Bay' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'Brazil/Acre' => array(
+        'offset' => -18000000,
+        'longname' => 'Acre Time',
+        'shortname' => 'ACT',
+        'hasdst' => false ),
+    'Canada/Eastern' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'Cuba' => array(
+        'offset' => -18000000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'EST' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'EST5EDT' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'Etc/GMT+5' => array(
+        'offset' => -18000000,
+        'longname' => 'GMT-05:00',
+        'shortname' => 'GMT-05:00',
+        'hasdst' => false ),
+    'IET' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'Jamaica' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'SystemV/EST5' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'SystemV/EST5EDT' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'US/East-Indiana' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'US/Eastern' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'US/Indiana-Starke' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'US/Michigan' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'America/Anguilla' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Antigua' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Aruba' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Asuncion' => array(
+        'offset' => -14400000,
+        'longname' => 'Paraguay Time',
+        'shortname' => 'PYT',
+        'hasdst' => true,
+        'dstlongname' => 'Paraguay Summer Time',
+        'dstshortname' => 'PYST' ),
+    'America/Barbados' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Boa_Vista' => array(
+        'offset' => -14400000,
+        'longname' => 'Amazon Standard Time',
+        'shortname' => 'AMT',
+        'hasdst' => false ),
+    'America/Caracas' => array(
+        'offset' => -14400000,
+        'longname' => 'Venezuela Time',
+        'shortname' => 'VET',
+        'hasdst' => false ),
+    'America/Cuiaba' => array(
+        'offset' => -14400000,
+        'longname' => 'Amazon Standard Time',
+        'shortname' => 'AMT',
+        'hasdst' => true,
+        'dstlongname' => 'Amazon Summer Time',
+        'dstshortname' => 'AMST' ),
+    'America/Curacao' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Dominica' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Glace_Bay' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Atlantic Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'America/Goose_Bay' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Atlantic Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'America/Grenada' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Guadeloupe' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Guyana' => array(
+        'offset' => -14400000,
+        'longname' => 'Guyana Time',
+        'shortname' => 'GYT',
+        'hasdst' => false ),
+    'America/Halifax' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Atlantic Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'America/La_Paz' => array(
+        'offset' => -14400000,
+        'longname' => 'Bolivia Time',
+        'shortname' => 'BOT',
+        'hasdst' => false ),
+    'America/Manaus' => array(
+        'offset' => -14400000,
+        'longname' => 'Amazon Standard Time',
+        'shortname' => 'AMT',
+        'hasdst' => false ),
+    'America/Martinique' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Montserrat' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Port_of_Spain' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Porto_Velho' => array(
+        'offset' => -14400000,
+        'longname' => 'Amazon Standard Time',
+        'shortname' => 'AMT',
+        'hasdst' => false ),
+    'America/Puerto_Rico' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Santiago' => array(
+        'offset' => -14400000,
+        'longname' => 'Chile Time',
+        'shortname' => 'CLT',
+        'hasdst' => true,
+        'dstlongname' => 'Chile Summer Time',
+        'dstshortname' => 'CLST' ),
+    'America/Santo_Domingo' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/St_Kitts' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/St_Lucia' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/St_Thomas' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/St_Vincent' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Thule' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Tortola' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'America/Virgin' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'Antarctica/Palmer' => array(
+        'offset' => -14400000,
+        'longname' => 'Chile Time',
+        'shortname' => 'CLT',
+        'hasdst' => true,
+        'dstlongname' => 'Chile Summer Time',
+        'dstshortname' => 'CLST' ),
+    'Atlantic/Bermuda' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Atlantic Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'Atlantic/Stanley' => array(
+        'offset' => -14400000,
+        'longname' => 'Falkland Is. Time',
+        'shortname' => 'FKT',
+        'hasdst' => true,
+        'dstlongname' => 'Falkland Is. Summer Time',
+        'dstshortname' => 'FKST' ),
+    'Brazil/West' => array(
+        'offset' => -14400000,
+        'longname' => 'Amazon Standard Time',
+        'shortname' => 'AMT',
+        'hasdst' => false ),
+    'Canada/Atlantic' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Atlantic Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'Chile/Continental' => array(
+        'offset' => -14400000,
+        'longname' => 'Chile Time',
+        'shortname' => 'CLT',
+        'hasdst' => true,
+        'dstlongname' => 'Chile Summer Time',
+        'dstshortname' => 'CLST' ),
+    'Etc/GMT+4' => array(
+        'offset' => -14400000,
+        'longname' => 'GMT-04:00',
+        'shortname' => 'GMT-04:00',
+        'hasdst' => false ),
+    'PRT' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'SystemV/AST4' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'SystemV/AST4ADT' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Atlantic Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'America/St_Johns' => array(
+        'offset' => -12600000,
+        'longname' => 'Newfoundland Standard Time',
+        'shortname' => 'NST',
+        'hasdst' => true,
+        'dstlongname' => 'Newfoundland Daylight Time',
+        'dstshortname' => 'NDT' ),
+    'CNT' => array(
+        'offset' => -12600000,
+        'longname' => 'Newfoundland Standard Time',
+        'shortname' => 'NST',
+        'hasdst' => true,
+        'dstlongname' => 'Newfoundland Daylight Time',
+        'dstshortname' => 'NDT' ),
+    'Canada/Newfoundland' => array(
+        'offset' => -12600000,
+        'longname' => 'Newfoundland Standard Time',
+        'shortname' => 'NST',
+        'hasdst' => true,
+        'dstlongname' => 'Newfoundland Daylight Time',
+        'dstshortname' => 'NDT' ),
+    'AGT' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'America/Araguaina' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'America/Belem' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => false ),
+    'America/Buenos_Aires' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'America/Catamarca' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'America/Cayenne' => array(
+        'offset' => -10800000,
+        'longname' => 'French Guiana Time',
+        'shortname' => 'GFT',
+        'hasdst' => false ),
+    'America/Cordoba' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'America/Fortaleza' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'America/Godthab' => array(
+        'offset' => -10800000,
+        'longname' => 'Western Greenland Time',
+        'shortname' => 'WGT',
+        'hasdst' => true,
+        'dstlongname' => 'Western Greenland Summer Time',
+        'dstshortname' => 'WGST' ),
+    'America/Jujuy' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'America/Maceio' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'America/Mendoza' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'America/Miquelon' => array(
+        'offset' => -10800000,
+        'longname' => 'Pierre & Miquelon Standard Time',
+        'shortname' => 'PMST',
+        'hasdst' => true,
+        'dstlongname' => 'Pierre & Miquelon Daylight Time',
+        'dstshortname' => 'PMDT' ),
+    'America/Montevideo' => array(
+        'offset' => -10800000,
+        'longname' => 'Uruguay Time',
+        'shortname' => 'UYT',
+        'hasdst' => false ),
+    'America/Paramaribo' => array(
+        'offset' => -10800000,
+        'longname' => 'Suriname Time',
+        'shortname' => 'SRT',
+        'hasdst' => false ),
+    'America/Recife' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'America/Rosario' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'America/Sao_Paulo' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'BET' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'Brazil/East' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'Etc/GMT+3' => array(
+        'offset' => -10800000,
+        'longname' => 'GMT-03:00',
+        'shortname' => 'GMT-03:00',
+        'hasdst' => false ),
+    'America/Noronha' => array(
+        'offset' => -7200000,
+        'longname' => 'Fernando de Noronha Time',
+        'shortname' => 'FNT',
+        'hasdst' => false ),
+    'Atlantic/South_Georgia' => array(
+        'offset' => -7200000,
+        'longname' => 'South Georgia Standard Time',
+        'shortname' => 'GST',
+        'hasdst' => false ),
+    'Brazil/DeNoronha' => array(
+        'offset' => -7200000,
+        'longname' => 'Fernando de Noronha Time',
+        'shortname' => 'FNT',
+        'hasdst' => false ),
+    'Etc/GMT+2' => array(
+        'offset' => -7200000,
+        'longname' => 'GMT-02:00',
+        'shortname' => 'GMT-02:00',
+        'hasdst' => false ),
+    'America/Scoresbysund' => array(
+        'offset' => -3600000,
+        'longname' => 'Eastern Greenland Time',
+        'shortname' => 'EGT',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Greenland Summer Time',
+        'dstshortname' => 'EGST' ),
+    'Atlantic/Azores' => array(
+        'offset' => -3600000,
+        'longname' => 'Azores Time',
+        'shortname' => 'AZOT',
+        'hasdst' => true,
+        'dstlongname' => 'Azores Summer Time',
+        'dstshortname' => 'AZOST' ),
+    'Atlantic/Cape_Verde' => array(
+        'offset' => -3600000,
+        'longname' => 'Cape Verde Time',
+        'shortname' => 'CVT',
+        'hasdst' => false ),
+    'Etc/GMT+1' => array(
+        'offset' => -3600000,
+        'longname' => 'GMT-01:00',
+        'shortname' => 'GMT-01:00',
+        'hasdst' => false ),
+    'Africa/Abidjan' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Accra' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Bamako' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Banjul' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Bissau' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Casablanca' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => false ),
+    'Africa/Conakry' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Dakar' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/El_Aaiun' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => false ),
+    'Africa/Freetown' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Lome' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Monrovia' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Nouakchott' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Ouagadougou' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Sao_Tome' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Africa/Timbuktu' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'America/Danmarkshavn' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Atlantic/Canary' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => true,
+        'dstlongname' => 'Western European Summer Time',
+        'dstshortname' => 'WEST' ),
+    'Atlantic/Faeroe' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => true,
+        'dstlongname' => 'Western European Summer Time',
+        'dstshortname' => 'WEST' ),
+    'Atlantic/Madeira' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => true,
+        'dstlongname' => 'Western European Summer Time',
+        'dstshortname' => 'WEST' ),
+    'Atlantic/Reykjavik' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Atlantic/St_Helena' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Eire' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => true,
+        'dstlongname' => 'Irish Summer Time',
+        'dstshortname' => 'IST' ),
+    'Etc/GMT' => array(
+        'offset' => 0,
+        'longname' => 'GMT+00:00',
+        'shortname' => 'GMT+00:00',
+        'hasdst' => false ),
+    'Etc/GMT+0' => array(
+        'offset' => 0,
+        'longname' => 'GMT+00:00',
+        'shortname' => 'GMT+00:00',
+        'hasdst' => false ),
+    'Etc/GMT-0' => array(
+        'offset' => 0,
+        'longname' => 'GMT+00:00',
+        'shortname' => 'GMT+00:00',
+        'hasdst' => false ),
+    'Etc/GMT0' => array(
+        'offset' => 0,
+        'longname' => 'GMT+00:00',
+        'shortname' => 'GMT+00:00',
+        'hasdst' => false ),
+    'Etc/Greenwich' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Etc/UCT' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'Etc/UTC' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'Etc/Universal' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'Etc/Zulu' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'Europe/Belfast' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => true,
+        'dstlongname' => 'British Summer Time',
+        'dstshortname' => 'BST' ),
+    'Europe/Dublin' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => true,
+        'dstlongname' => 'Irish Summer Time',
+        'dstshortname' => 'IST' ),
+    'Europe/Lisbon' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => true,
+        'dstlongname' => 'Western European Summer Time',
+        'dstshortname' => 'WEST' ),
+    'Europe/London' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => true,
+        'dstlongname' => 'British Summer Time',
+        'dstshortname' => 'BST' ),
+    'GB' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => true,
+        'dstlongname' => 'British Summer Time',
+        'dstshortname' => 'BST' ),
+    'GB-Eire' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => true,
+        'dstlongname' => 'British Summer Time',
+        'dstshortname' => 'BST' ),
+    'GMT' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'GMT0' => array(
+        'offset' => 0,
+        'longname' => 'GMT+00:00',
+        'shortname' => 'GMT+00:00',
+        'hasdst' => false ),
+    'Greenwich' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Iceland' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Portugal' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => true,
+        'dstlongname' => 'Western European Summer Time',
+        'dstshortname' => 'WEST' ),
+    'UCT' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'UTC' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'Universal' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'WET' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => true,
+        'dstlongname' => 'Western European Summer Time',
+        'dstshortname' => 'WEST' ),
+    'Zulu' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'Africa/Algiers' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => false ),
+    'Africa/Bangui' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Brazzaville' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Ceuta' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Africa/Douala' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Kinshasa' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Lagos' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Libreville' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Luanda' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Malabo' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Ndjamena' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Niamey' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Porto-Novo' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => false ),
+    'Africa/Tunis' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => false ),
+    'Africa/Windhoek' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => true,
+        'dstlongname' => 'Western African Summer Time',
+        'dstshortname' => 'WAST' ),
+    'Arctic/Longyearbyen' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Atlantic/Jan_Mayen' => array(
+        'offset' => 3600000,
+        'longname' => 'Eastern Greenland Time',
+        'shortname' => 'EGT',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Greenland Summer Time',
+        'dstshortname' => 'EGST' ),
+    'CET' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'CEST' => array(
+        'offset' => 3600000,
+        'longname' => "Central European Time",
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => "Central European Summer Time",
+        'dstshortname' => 'CEST' ),
+    'ECT' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Etc/GMT-1' => array(
+        'offset' => 3600000,
+        'longname' => 'GMT+01:00',
+        'shortname' => 'GMT+01:00',
+        'hasdst' => false ),
+    'Europe/Amsterdam' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Andorra' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Belgrade' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Berlin' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Bratislava' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Brussels' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Budapest' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Copenhagen' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Gibraltar' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Ljubljana' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Luxembourg' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Madrid' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Malta' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Monaco' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Oslo' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Paris' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Prague' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Rome' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/San_Marino' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Sarajevo' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Skopje' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Stockholm' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Tirane' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Vaduz' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Vatican' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Vienna' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Warsaw' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Zagreb' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Europe/Zurich' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'MET' => array(
+        'offset' => 3600000,
+        'longname' => 'Middle Europe Time',
+        'shortname' => 'MET',
+        'hasdst' => true,
+        'dstlongname' => 'Middle Europe Summer Time',
+        'dstshortname' => 'MEST' ),
+    'Poland' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'ART' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Africa/Blantyre' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Bujumbura' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Cairo' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Africa/Gaborone' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Harare' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Johannesburg' => array(
+        'offset' => 7200000,
+        'longname' => 'South Africa Standard Time',
+        'shortname' => 'SAST',
+        'hasdst' => false ),
+    'Africa/Kigali' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Lubumbashi' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Lusaka' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Maputo' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'Africa/Maseru' => array(
+        'offset' => 7200000,
+        'longname' => 'South Africa Standard Time',
+        'shortname' => 'SAST',
+        'hasdst' => false ),
+    'Africa/Mbabane' => array(
+        'offset' => 7200000,
+        'longname' => 'South Africa Standard Time',
+        'shortname' => 'SAST',
+        'hasdst' => false ),
+    'Africa/Tripoli' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => false ),
+    'Asia/Amman' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Asia/Beirut' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Asia/Damascus' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Asia/Gaza' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Asia/Istanbul' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Asia/Jerusalem' => array(
+        'offset' => 7200000,
+        'longname' => 'Israel Standard Time',
+        'shortname' => 'IST',
+        'hasdst' => true,
+        'dstlongname' => 'Israel Daylight Time',
+        'dstshortname' => 'IDT' ),
+    'Asia/Nicosia' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Asia/Tel_Aviv' => array(
+        'offset' => 7200000,
+        'longname' => 'Israel Standard Time',
+        'shortname' => 'IST',
+        'hasdst' => true,
+        'dstlongname' => 'Israel Daylight Time',
+        'dstshortname' => 'IDT' ),
+    'CAT' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'EET' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Egypt' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Etc/GMT-2' => array(
+        'offset' => 7200000,
+        'longname' => 'GMT+02:00',
+        'shortname' => 'GMT+02:00',
+        'hasdst' => false ),
+    'Europe/Athens' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Bucharest' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Chisinau' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Helsinki' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Istanbul' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Kaliningrad' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Kiev' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Minsk' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Nicosia' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Riga' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Simferopol' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Sofia' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Tallinn' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => false ),
+    'Europe/Tiraspol' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Uzhgorod' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Europe/Vilnius' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => false ),
+    'Europe/Zaporozhye' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Israel' => array(
+        'offset' => 7200000,
+        'longname' => 'Israel Standard Time',
+        'shortname' => 'IST',
+        'hasdst' => true,
+        'dstlongname' => 'Israel Daylight Time',
+        'dstshortname' => 'IDT' ),
+    'Libya' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => false ),
+    'Turkey' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Africa/Addis_Ababa' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Africa/Asmera' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Africa/Dar_es_Salaam' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Africa/Djibouti' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Africa/Kampala' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Africa/Khartoum' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Africa/Mogadishu' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Africa/Nairobi' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Antarctica/Syowa' => array(
+        'offset' => 10800000,
+        'longname' => 'Syowa Time',
+        'shortname' => 'SYOT',
+        'hasdst' => false ),
+    'Asia/Aden' => array(
+        'offset' => 10800000,
+        'longname' => 'Arabia Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'Asia/Baghdad' => array(
+        'offset' => 10800000,
+        'longname' => 'Arabia Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Arabia Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'Asia/Bahrain' => array(
+        'offset' => 10800000,
+        'longname' => 'Arabia Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'Asia/Kuwait' => array(
+        'offset' => 10800000,
+        'longname' => 'Arabia Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'Asia/Qatar' => array(
+        'offset' => 10800000,
+        'longname' => 'Arabia Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'Asia/Riyadh' => array(
+        'offset' => 10800000,
+        'longname' => 'Arabia Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'EAT' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Etc/GMT-3' => array(
+        'offset' => 10800000,
+        'longname' => 'GMT+03:00',
+        'shortname' => 'GMT+03:00',
+        'hasdst' => false ),
+    'Europe/Moscow' => array(
+        'offset' => 10800000,
+        'longname' => 'Moscow Standard Time',
+        'shortname' => 'MSK',
+        'hasdst' => true,
+        'dstlongname' => 'Moscow Daylight Time',
+        'dstshortname' => 'MSD' ),
+    'Indian/Antananarivo' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Indian/Comoro' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Indian/Mayotte' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'W-SU' => array(
+        'offset' => 10800000,
+        'longname' => 'Moscow Standard Time',
+        'shortname' => 'MSK',
+        'hasdst' => true,
+        'dstlongname' => 'Moscow Daylight Time',
+        'dstshortname' => 'MSD' ),
+    'Asia/Riyadh87' => array(
+        'offset' => 11224000,
+        'longname' => 'GMT+03:07',
+        'shortname' => 'GMT+03:07',
+        'hasdst' => false ),
+    'Asia/Riyadh88' => array(
+        'offset' => 11224000,
+        'longname' => 'GMT+03:07',
+        'shortname' => 'GMT+03:07',
+        'hasdst' => false ),
+    'Asia/Riyadh89' => array(
+        'offset' => 11224000,
+        'longname' => 'GMT+03:07',
+        'shortname' => 'GMT+03:07',
+        'hasdst' => false ),
+    'Mideast/Riyadh87' => array(
+        'offset' => 11224000,
+        'longname' => 'GMT+03:07',
+        'shortname' => 'GMT+03:07',
+        'hasdst' => false ),
+    'Mideast/Riyadh88' => array(
+        'offset' => 11224000,
+        'longname' => 'GMT+03:07',
+        'shortname' => 'GMT+03:07',
+        'hasdst' => false ),
+    'Mideast/Riyadh89' => array(
+        'offset' => 11224000,
+        'longname' => 'GMT+03:07',
+        'shortname' => 'GMT+03:07',
+        'hasdst' => false ),
+    'Asia/Tehran' => array(
+        'offset' => 12600000,
+        'longname' => 'Iran Time',
+        'shortname' => 'IRT',
+        'hasdst' => true,
+        'dstlongname' => 'Iran Sumer Time',
+        'dstshortname' => 'IRST' ),
+    'Iran' => array(
+        'offset' => 12600000,
+        'longname' => 'Iran Time',
+        'shortname' => 'IRT',
+        'hasdst' => true,
+        'dstlongname' => 'Iran Sumer Time',
+        'dstshortname' => 'IRST' ),
+    'Asia/Aqtau' => array(
+        'offset' => 14400000,
+        'longname' => 'Aqtau Time',
+        'shortname' => 'AQTT',
+        'hasdst' => true,
+        'dstlongname' => 'Aqtau Summer Time',
+        'dstshortname' => 'AQTST' ),
+    'Asia/Baku' => array(
+        'offset' => 14400000,
+        'longname' => 'Azerbaijan Time',
+        'shortname' => 'AZT',
+        'hasdst' => true,
+        'dstlongname' => 'Azerbaijan Summer Time',
+        'dstshortname' => 'AZST' ),
+    'Asia/Dubai' => array(
+        'offset' => 14400000,
+        'longname' => 'Gulf Standard Time',
+        'shortname' => 'GST',
+        'hasdst' => false ),
+    'Asia/Muscat' => array(
+        'offset' => 14400000,
+        'longname' => 'Gulf Standard Time',
+        'shortname' => 'GST',
+        'hasdst' => false ),
+    'Asia/Tbilisi' => array(
+        'offset' => 14400000,
+        'longname' => 'Georgia Time',
+        'shortname' => 'GET',
+        'hasdst' => true,
+        'dstlongname' => 'Georgia Summer Time',
+        'dstshortname' => 'GEST' ),
+    'Asia/Yerevan' => array(
+        'offset' => 14400000,
+        'longname' => 'Armenia Time',
+        'shortname' => 'AMT',
+        'hasdst' => true,
+        'dstlongname' => 'Armenia Summer Time',
+        'dstshortname' => 'AMST' ),
+    'Etc/GMT-4' => array(
+        'offset' => 14400000,
+        'longname' => 'GMT+04:00',
+        'shortname' => 'GMT+04:00',
+        'hasdst' => false ),
+    'Europe/Samara' => array(
+        'offset' => 14400000,
+        'longname' => 'Samara Time',
+        'shortname' => 'SAMT',
+        'hasdst' => true,
+        'dstlongname' => 'Samara Summer Time',
+        'dstshortname' => 'SAMST' ),
+    'Indian/Mahe' => array(
+        'offset' => 14400000,
+        'longname' => 'Seychelles Time',
+        'shortname' => 'SCT',
+        'hasdst' => false ),
+    'Indian/Mauritius' => array(
+        'offset' => 14400000,
+        'longname' => 'Mauritius Time',
+        'shortname' => 'MUT',
+        'hasdst' => false ),
+    'Indian/Reunion' => array(
+        'offset' => 14400000,
+        'longname' => 'Reunion Time',
+        'shortname' => 'RET',
+        'hasdst' => false ),
+    'NET' => array(
+        'offset' => 14400000,
+        'longname' => 'Armenia Time',
+        'shortname' => 'AMT',
+        'hasdst' => true,
+        'dstlongname' => 'Armenia Summer Time',
+        'dstshortname' => 'AMST' ),
+    'Asia/Kabul' => array(
+        'offset' => 16200000,
+        'longname' => 'Afghanistan Time',
+        'shortname' => 'AFT',
+        'hasdst' => false ),
+    'Asia/Aqtobe' => array(
+        'offset' => 18000000,
+        'longname' => 'Aqtobe Time',
+        'shortname' => 'AQTT',
+        'hasdst' => true,
+        'dstlongname' => 'Aqtobe Summer Time',
+        'dstshortname' => 'AQTST' ),
+    'Asia/Ashgabat' => array(
+        'offset' => 18000000,
+        'longname' => 'Turkmenistan Time',
+        'shortname' => 'TMT',
+        'hasdst' => false ),
+    'Asia/Ashkhabad' => array(
+        'offset' => 18000000,
+        'longname' => 'Turkmenistan Time',
+        'shortname' => 'TMT',
+        'hasdst' => false ),
+    'Asia/Bishkek' => array(
+        'offset' => 18000000,
+        'longname' => 'Kirgizstan Time',
+        'shortname' => 'KGT',
+        'hasdst' => true,
+        'dstlongname' => 'Kirgizstan Summer Time',
+        'dstshortname' => 'KGST' ),
+    'Asia/Dushanbe' => array(
+        'offset' => 18000000,
+        'longname' => 'Tajikistan Time',
+        'shortname' => 'TJT',
+        'hasdst' => false ),
+    'Asia/Karachi' => array(
+        'offset' => 18000000,
+        'longname' => 'Pakistan Time',
+        'shortname' => 'PKT',
+        'hasdst' => false ),
+    'Asia/Samarkand' => array(
+        'offset' => 18000000,
+        'longname' => 'Turkmenistan Time',
+        'shortname' => 'TMT',
+        'hasdst' => false ),
+    'Asia/Tashkent' => array(
+        'offset' => 18000000,
+        'longname' => 'Uzbekistan Time',
+        'shortname' => 'UZT',
+        'hasdst' => false ),
+    'Asia/Yekaterinburg' => array(
+        'offset' => 18000000,
+        'longname' => 'Yekaterinburg Time',
+        'shortname' => 'YEKT',
+        'hasdst' => true,
+        'dstlongname' => 'Yekaterinburg Summer Time',
+        'dstshortname' => 'YEKST' ),
+    'Etc/GMT-5' => array(
+        'offset' => 18000000,
+        'longname' => 'GMT+05:00',
+        'shortname' => 'GMT+05:00',
+        'hasdst' => false ),
+    'Indian/Kerguelen' => array(
+        'offset' => 18000000,
+        'longname' => 'French Southern & Antarctic Lands Time',
+        'shortname' => 'TFT',
+        'hasdst' => false ),
+    'Indian/Maldives' => array(
+        'offset' => 18000000,
+        'longname' => 'Maldives Time',
+        'shortname' => 'MVT',
+        'hasdst' => false ),
+    'PLT' => array(
+        'offset' => 18000000,
+        'longname' => 'Pakistan Time',
+        'shortname' => 'PKT',
+        'hasdst' => false ),
+    'Asia/Calcutta' => array(
+        'offset' => 19800000,
+        'longname' => 'India Standard Time',
+        'shortname' => 'IST',
+        'hasdst' => false ),
+    'IST' => array(
+        'offset' => 19800000,
+        'longname' => 'India Standard Time',
+        'shortname' => 'IST',
+        'hasdst' => false ),
+    'Asia/Katmandu' => array(
+        'offset' => 20700000,
+        'longname' => 'Nepal Time',
+        'shortname' => 'NPT',
+        'hasdst' => false ),
+    'Antarctica/Mawson' => array(
+        'offset' => 21600000,
+        'longname' => 'Mawson Time',
+        'shortname' => 'MAWT',
+        'hasdst' => false ),
+    'Antarctica/Vostok' => array(
+        'offset' => 21600000,
+        'longname' => 'Vostok time',
+        'shortname' => 'VOST',
+        'hasdst' => false ),
+    'Asia/Almaty' => array(
+        'offset' => 21600000,
+        'longname' => 'Alma-Ata Time',
+        'shortname' => 'ALMT',
+        'hasdst' => true,
+        'dstlongname' => 'Alma-Ata Summer Time',
+        'dstshortname' => 'ALMST' ),
+    'Asia/Colombo' => array(
+        'offset' => 21600000,
+        'longname' => 'Sri Lanka Time',
+        'shortname' => 'LKT',
+        'hasdst' => false ),
+    'Asia/Dacca' => array(
+        'offset' => 21600000,
+        'longname' => 'Bangladesh Time',
+        'shortname' => 'BDT',
+        'hasdst' => false ),
+    'Asia/Dhaka' => array(
+        'offset' => 21600000,
+        'longname' => 'Bangladesh Time',
+        'shortname' => 'BDT',
+        'hasdst' => false ),
+    'Asia/Novosibirsk' => array(
+        'offset' => 21600000,
+        'longname' => 'Novosibirsk Time',
+        'shortname' => 'NOVT',
+        'hasdst' => true,
+        'dstlongname' => 'Novosibirsk Summer Time',
+        'dstshortname' => 'NOVST' ),
+    'Asia/Omsk' => array(
+        'offset' => 21600000,
+        'longname' => 'Omsk Time',
+        'shortname' => 'OMST',
+        'hasdst' => true,
+        'dstlongname' => 'Omsk Summer Time',
+        'dstshortname' => 'OMSST' ),
+    'Asia/Thimbu' => array(
+        'offset' => 21600000,
+        'longname' => 'Bhutan Time',
+        'shortname' => 'BTT',
+        'hasdst' => false ),
+    'Asia/Thimphu' => array(
+        'offset' => 21600000,
+        'longname' => 'Bhutan Time',
+        'shortname' => 'BTT',
+        'hasdst' => false ),
+    'BDT' => array(
+        'offset' => 21600000,
+        'longname' => 'Bangladesh Time',
+        'shortname' => 'BDT',
+        'hasdst' => false ),
+    'Etc/GMT-6' => array(
+        'offset' => 21600000,
+        'longname' => 'GMT+06:00',
+        'shortname' => 'GMT+06:00',
+        'hasdst' => false ),
+    'Indian/Chagos' => array(
+        'offset' => 21600000,
+        'longname' => 'Indian Ocean Territory Time',
+        'shortname' => 'IOT',
+        'hasdst' => false ),
+    'Asia/Rangoon' => array(
+        'offset' => 23400000,
+        'longname' => 'Myanmar Time',
+        'shortname' => 'MMT',
+        'hasdst' => false ),
+    'Indian/Cocos' => array(
+        'offset' => 23400000,
+        'longname' => 'Cocos Islands Time',
+        'shortname' => 'CCT',
+        'hasdst' => false ),
+    'Antarctica/Davis' => array(
+        'offset' => 25200000,
+        'longname' => 'Davis Time',
+        'shortname' => 'DAVT',
+        'hasdst' => false ),
+    'Asia/Bangkok' => array(
+        'offset' => 25200000,
+        'longname' => 'Indochina Time',
+        'shortname' => 'ICT',
+        'hasdst' => false ),
+    'Asia/Hovd' => array(
+        'offset' => 25200000,
+        'longname' => 'Hovd Time',
+        'shortname' => 'HOVT',
+        'hasdst' => false ),
+    'Asia/Jakarta' => array(
+        'offset' => 25200000,
+        'longname' => 'West Indonesia Time',
+        'shortname' => 'WIT',
+        'hasdst' => false ),
+    'Asia/Krasnoyarsk' => array(
+        'offset' => 25200000,
+        'longname' => 'Krasnoyarsk Time',
+        'shortname' => 'KRAT',
+        'hasdst' => true,
+        'dstlongname' => 'Krasnoyarsk Summer Time',
+        'dstshortname' => 'KRAST' ),
+    'Asia/Phnom_Penh' => array(
+        'offset' => 25200000,
+        'longname' => 'Indochina Time',
+        'shortname' => 'ICT',
+        'hasdst' => false ),
+    'Asia/Pontianak' => array(
+        'offset' => 25200000,
+        'longname' => 'West Indonesia Time',
+        'shortname' => 'WIT',
+        'hasdst' => false ),
+    'Asia/Saigon' => array(
+        'offset' => 25200000,
+        'longname' => 'Indochina Time',
+        'shortname' => 'ICT',
+        'hasdst' => false ),
+    'Asia/Vientiane' => array(
+        'offset' => 25200000,
+        'longname' => 'Indochina Time',
+        'shortname' => 'ICT',
+        'hasdst' => false ),
+    'Etc/GMT-7' => array(
+        'offset' => 25200000,
+        'longname' => 'GMT+07:00',
+        'shortname' => 'GMT+07:00',
+        'hasdst' => false ),
+    'Indian/Christmas' => array(
+        'offset' => 25200000,
+        'longname' => 'Christmas Island Time',
+        'shortname' => 'CXT',
+        'hasdst' => false ),
+    'VST' => array(
+        'offset' => 25200000,
+        'longname' => 'Indochina Time',
+        'shortname' => 'ICT',
+        'hasdst' => false ),
+    'Antarctica/Casey' => array(
+        'offset' => 28800000,
+        'longname' => 'Western Standard Time (Australia)',
+        'shortname' => 'WST',
+        'hasdst' => false ),
+    'Asia/Brunei' => array(
+        'offset' => 28800000,
+        'longname' => 'Brunei Time',
+        'shortname' => 'BNT',
+        'hasdst' => false ),
+    'Asia/Chongqing' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Asia/Chungking' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Asia/Harbin' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Asia/Hong_Kong' => array(
+        'offset' => 28800000,
+        'longname' => 'Hong Kong Time',
+        'shortname' => 'HKT',
+        'hasdst' => false ),
+    'Asia/Irkutsk' => array(
+        'offset' => 28800000,
+        'longname' => 'Irkutsk Time',
+        'shortname' => 'IRKT',
+        'hasdst' => true,
+        'dstlongname' => 'Irkutsk Summer Time',
+        'dstshortname' => 'IRKST' ),
+    'Asia/Kashgar' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Asia/Kuala_Lumpur' => array(
+        'offset' => 28800000,
+        'longname' => 'Malaysia Time',
+        'shortname' => 'MYT',
+        'hasdst' => false ),
+    'Asia/Kuching' => array(
+        'offset' => 28800000,
+        'longname' => 'Malaysia Time',
+        'shortname' => 'MYT',
+        'hasdst' => false ),
+    'Asia/Macao' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Asia/Manila' => array(
+        'offset' => 28800000,
+        'longname' => 'Philippines Time',
+        'shortname' => 'PHT',
+        'hasdst' => false ),
+    'Asia/Shanghai' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Asia/Singapore' => array(
+        'offset' => 28800000,
+        'longname' => 'Singapore Time',
+        'shortname' => 'SGT',
+        'hasdst' => false ),
+    'Asia/Taipei' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Asia/Ujung_Pandang' => array(
+        'offset' => 28800000,
+        'longname' => 'Central Indonesia Time',
+        'shortname' => 'CIT',
+        'hasdst' => false ),
+    'Asia/Ulaanbaatar' => array(
+        'offset' => 28800000,
+        'longname' => 'Ulaanbaatar Time',
+        'shortname' => 'ULAT',
+        'hasdst' => false ),
+    'Asia/Ulan_Bator' => array(
+        'offset' => 28800000,
+        'longname' => 'Ulaanbaatar Time',
+        'shortname' => 'ULAT',
+        'hasdst' => false ),
+    'Asia/Urumqi' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Australia/Perth' => array(
+        'offset' => 28800000,
+        'longname' => 'Western Standard Time (Australia)',
+        'shortname' => 'WST',
+        'hasdst' => false ),
+    'Australia/West' => array(
+        'offset' => 28800000,
+        'longname' => 'Western Standard Time (Australia)',
+        'shortname' => 'WST',
+        'hasdst' => false ),
+    'CTT' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Etc/GMT-8' => array(
+        'offset' => 28800000,
+        'longname' => 'GMT+08:00',
+        'shortname' => 'GMT+08:00',
+        'hasdst' => false ),
+    'Hongkong' => array(
+        'offset' => 28800000,
+        'longname' => 'Hong Kong Time',
+        'shortname' => 'HKT',
+        'hasdst' => false ),
+    'PRC' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Singapore' => array(
+        'offset' => 28800000,
+        'longname' => 'Singapore Time',
+        'shortname' => 'SGT',
+        'hasdst' => false ),
+    'Asia/Choibalsan' => array(
+        'offset' => 32400000,
+        'longname' => 'Choibalsan Time',
+        'shortname' => 'CHOT',
+        'hasdst' => false ),
+    'Asia/Dili' => array(
+        'offset' => 32400000,
+        'longname' => 'East Timor Time',
+        'shortname' => 'TPT',
+        'hasdst' => false ),
+    'Asia/Jayapura' => array(
+        'offset' => 32400000,
+        'longname' => 'East Indonesia Time',
+        'shortname' => 'EIT',
+        'hasdst' => false ),
+    'Asia/Pyongyang' => array(
+        'offset' => 32400000,
+        'longname' => 'Korea Standard Time',
+        'shortname' => 'KST',
+        'hasdst' => false ),
+    'Asia/Seoul' => array(
+        'offset' => 32400000,
+        'longname' => 'Korea Standard Time',
+        'shortname' => 'KST',
+        'hasdst' => false ),
+    'Asia/Tokyo' => array(
+        'offset' => 32400000,
+        'longname' => 'Japan Standard Time',
+        'shortname' => 'JST',
+        'hasdst' => false ),
+    'Asia/Yakutsk' => array(
+        'offset' => 32400000,
+        'longname' => 'Yakutsk Time',
+        'shortname' => 'YAKT',
+        'hasdst' => true,
+        'dstlongname' => 'Yaktsk Summer Time',
+        'dstshortname' => 'YAKST' ),
+    'Etc/GMT-9' => array(
+        'offset' => 32400000,
+        'longname' => 'GMT+09:00',
+        'shortname' => 'GMT+09:00',
+        'hasdst' => false ),
+    'JST' => array(
+        'offset' => 32400000,
+        'longname' => 'Japan Standard Time',
+        'shortname' => 'JST',
+        'hasdst' => false ),
+    'Japan' => array(
+        'offset' => 32400000,
+        'longname' => 'Japan Standard Time',
+        'shortname' => 'JST',
+        'hasdst' => false ),
+    'Pacific/Palau' => array(
+        'offset' => 32400000,
+        'longname' => 'Palau Time',
+        'shortname' => 'PWT',
+        'hasdst' => false ),
+    'ROK' => array(
+        'offset' => 32400000,
+        'longname' => 'Korea Standard Time',
+        'shortname' => 'KST',
+        'hasdst' => false ),
+    'ACT' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (Northern Territory)',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Australia/Adelaide' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (South Australia)',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Summer Time (South Australia)',
+        'dstshortname' => 'CST' ),
+    'Australia/Broken_Hill' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (South Australia/New South Wales)',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Summer Time (South Australia/New South Wales)',
+        'dstshortname' => 'CST' ),
+    'Australia/Darwin' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (Northern Territory)',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Australia/North' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (Northern Territory)',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Australia/South' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (South Australia)',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Summer Time (South Australia)',
+        'dstshortname' => 'CST' ),
+    'Australia/Yancowinna' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (South Australia/New South Wales)',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Summer Time (South Australia/New South Wales)',
+        'dstshortname' => 'CST' ),
+    'AET' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (New South Wales)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (New South Wales)',
+        'dstshortname' => 'EST' ),
+    'Antarctica/DumontDUrville' => array(
+        'offset' => 36000000,
+        'longname' => 'Dumont-d\'Urville Time',
+        'shortname' => 'DDUT',
+        'hasdst' => false ),
+    'Asia/Sakhalin' => array(
+        'offset' => 36000000,
+        'longname' => 'Sakhalin Time',
+        'shortname' => 'SAKT',
+        'hasdst' => true,
+        'dstlongname' => 'Sakhalin Summer Time',
+        'dstshortname' => 'SAKST' ),
+    'Asia/Vladivostok' => array(
+        'offset' => 36000000,
+        'longname' => 'Vladivostok Time',
+        'shortname' => 'VLAT',
+        'hasdst' => true,
+        'dstlongname' => 'Vladivostok Summer Time',
+        'dstshortname' => 'VLAST' ),
+    'Australia/ACT' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (New South Wales)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (New South Wales)',
+        'dstshortname' => 'EST' ),
+    'Australia/Brisbane' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Queensland)',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'Australia/Canberra' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (New South Wales)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (New South Wales)',
+        'dstshortname' => 'EST' ),
+    'Australia/Hobart' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Tasmania)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (Tasmania)',
+        'dstshortname' => 'EST' ),
+    'Australia/Lindeman' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Queensland)',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'Australia/Melbourne' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Victoria)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (Victoria)',
+        'dstshortname' => 'EST' ),
+    'Australia/NSW' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (New South Wales)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (New South Wales)',
+        'dstshortname' => 'EST' ),
+    'Australia/Queensland' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Queensland)',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'Australia/Sydney' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (New South Wales)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (New South Wales)',
+        'dstshortname' => 'EST' ),
+    'Australia/Tasmania' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Tasmania)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (Tasmania)',
+        'dstshortname' => 'EST' ),
+    'Australia/Victoria' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Victoria)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (Victoria)',
+        'dstshortname' => 'EST' ),
+    'Etc/GMT-10' => array(
+        'offset' => 36000000,
+        'longname' => 'GMT+10:00',
+        'shortname' => 'GMT+10:00',
+        'hasdst' => false ),
+    'Pacific/Guam' => array(
+        'offset' => 36000000,
+        'longname' => 'Chamorro Standard Time',
+        'shortname' => 'ChST',
+        'hasdst' => false ),
+    'Pacific/Port_Moresby' => array(
+        'offset' => 36000000,
+        'longname' => 'Papua New Guinea Time',
+        'shortname' => 'PGT',
+        'hasdst' => false ),
+    'Pacific/Saipan' => array(
+        'offset' => 36000000,
+        'longname' => 'Chamorro Standard Time',
+        'shortname' => 'ChST',
+        'hasdst' => false ),
+    'Pacific/Truk' => array(
+        'offset' => 36000000,
+        'longname' => 'Truk Time',
+        'shortname' => 'TRUT',
+        'hasdst' => false ),
+    'Pacific/Yap' => array(
+        'offset' => 36000000,
+        'longname' => 'Yap Time',
+        'shortname' => 'YAPT',
+        'hasdst' => false ),
+    'Australia/LHI' => array(
+        'offset' => 37800000,
+        'longname' => 'Load Howe Standard Time',
+        'shortname' => 'LHST',
+        'hasdst' => true,
+        'dstlongname' => 'Load Howe Summer Time',
+        'dstshortname' => 'LHST' ),
+    'Australia/Lord_Howe' => array(
+        'offset' => 37800000,
+        'longname' => 'Load Howe Standard Time',
+        'shortname' => 'LHST',
+        'hasdst' => true,
+        'dstlongname' => 'Load Howe Summer Time',
+        'dstshortname' => 'LHST' ),
+    'Asia/Magadan' => array(
+        'offset' => 39600000,
+        'longname' => 'Magadan Time',
+        'shortname' => 'MAGT',
+        'hasdst' => true,
+        'dstlongname' => 'Magadan Summer Time',
+        'dstshortname' => 'MAGST' ),
+    'Etc/GMT-11' => array(
+        'offset' => 39600000,
+        'longname' => 'GMT+11:00',
+        'shortname' => 'GMT+11:00',
+        'hasdst' => false ),
+    'Pacific/Efate' => array(
+        'offset' => 39600000,
+        'longname' => 'Vanuatu Time',
+        'shortname' => 'VUT',
+        'hasdst' => false ),
+    'Pacific/Guadalcanal' => array(
+        'offset' => 39600000,
+        'longname' => 'Solomon Is. Time',
+        'shortname' => 'SBT',
+        'hasdst' => false ),
+    'Pacific/Kosrae' => array(
+        'offset' => 39600000,
+        'longname' => 'Kosrae Time',
+        'shortname' => 'KOST',
+        'hasdst' => false ),
+    'Pacific/Noumea' => array(
+        'offset' => 39600000,
+        'longname' => 'New Caledonia Time',
+        'shortname' => 'NCT',
+        'hasdst' => false ),
+    'Pacific/Ponape' => array(
+        'offset' => 39600000,
+        'longname' => 'Ponape Time',
+        'shortname' => 'PONT',
+        'hasdst' => false ),
+    'SST' => array(
+        'offset' => 39600000,
+        'longname' => 'Solomon Is. Time',
+        'shortname' => 'SBT',
+        'hasdst' => false ),
+    'Pacific/Norfolk' => array(
+        'offset' => 41400000,
+        'longname' => 'Norfolk Time',
+        'shortname' => 'NFT',
+        'hasdst' => false ),
+    'Antarctica/McMurdo' => array(
+        'offset' => 43200000,
+        'longname' => 'New Zealand Standard Time',
+        'shortname' => 'NZST',
+        'hasdst' => true,
+        'dstlongname' => 'New Zealand Daylight Time',
+        'dstshortname' => 'NZDT' ),
+    'Antarctica/South_Pole' => array(
+        'offset' => 43200000,
+        'longname' => 'New Zealand Standard Time',
+        'shortname' => 'NZST',
+        'hasdst' => true,
+        'dstlongname' => 'New Zealand Daylight Time',
+        'dstshortname' => 'NZDT' ),
+    'Asia/Anadyr' => array(
+        'offset' => 43200000,
+        'longname' => 'Anadyr Time',
+        'shortname' => 'ANAT',
+        'hasdst' => true,
+        'dstlongname' => 'Anadyr Summer Time',
+        'dstshortname' => 'ANAST' ),
+    'Asia/Kamchatka' => array(
+        'offset' => 43200000,
+        'longname' => 'Petropavlovsk-Kamchatski Time',
+        'shortname' => 'PETT',
+        'hasdst' => true,
+        'dstlongname' => 'Petropavlovsk-Kamchatski Summer Time',
+        'dstshortname' => 'PETST' ),
+    'Etc/GMT-12' => array(
+        'offset' => 43200000,
+        'longname' => 'GMT+12:00',
+        'shortname' => 'GMT+12:00',
+        'hasdst' => false ),
+    'Kwajalein' => array(
+        'offset' => 43200000,
+        'longname' => 'Marshall Islands Time',
+        'shortname' => 'MHT',
+        'hasdst' => false ),
+    'NST' => array(
+        'offset' => 43200000,
+        'longname' => 'New Zealand Standard Time',
+        'shortname' => 'NZST',
+        'hasdst' => true,
+        'dstlongname' => 'New Zealand Daylight Time',
+        'dstshortname' => 'NZDT' ),
+    'NZ' => array(
+        'offset' => 43200000,
+        'longname' => 'New Zealand Standard Time',
+        'shortname' => 'NZST',
+        'hasdst' => true,
+        'dstlongname' => 'New Zealand Daylight Time',
+        'dstshortname' => 'NZDT' ),
+    'Pacific/Auckland' => array(
+        'offset' => 43200000,
+        'longname' => 'New Zealand Standard Time',
+        'shortname' => 'NZST',
+        'hasdst' => true,
+        'dstlongname' => 'New Zealand Daylight Time',
+        'dstshortname' => 'NZDT' ),
+    'Pacific/Fiji' => array(
+        'offset' => 43200000,
+        'longname' => 'Fiji Time',
+        'shortname' => 'FJT',
+        'hasdst' => false ),
+    'Pacific/Funafuti' => array(
+        'offset' => 43200000,
+        'longname' => 'Tuvalu Time',
+        'shortname' => 'TVT',
+        'hasdst' => false ),
+    'Pacific/Kwajalein' => array(
+        'offset' => 43200000,
+        'longname' => 'Marshall Islands Time',
+        'shortname' => 'MHT',
+        'hasdst' => false ),
+    'Pacific/Majuro' => array(
+        'offset' => 43200000,
+        'longname' => 'Marshall Islands Time',
+        'shortname' => 'MHT',
+        'hasdst' => false ),
+    'Pacific/Nauru' => array(
+        'offset' => 43200000,
+        'longname' => 'Nauru Time',
+        'shortname' => 'NRT',
+        'hasdst' => false ),
+    'Pacific/Tarawa' => array(
+        'offset' => 43200000,
+        'longname' => 'Gilbert Is. Time',
+        'shortname' => 'GILT',
+        'hasdst' => false ),
+    'Pacific/Wake' => array(
+        'offset' => 43200000,
+        'longname' => 'Wake Time',
+        'shortname' => 'WAKT',
+        'hasdst' => false ),
+    'Pacific/Wallis' => array(
+        'offset' => 43200000,
+        'longname' => 'Wallis & Futuna Time',
+        'shortname' => 'WFT',
+        'hasdst' => false ),
+    'NZ-CHAT' => array(
+        'offset' => 45900000,
+        'longname' => 'Chatham Standard Time',
+        'shortname' => 'CHAST',
+        'hasdst' => true,
+        'dstlongname' => 'Chatham Daylight Time',
+        'dstshortname' => 'CHADT' ),
+    'Pacific/Chatham' => array(
+        'offset' => 45900000,
+        'longname' => 'Chatham Standard Time',
+        'shortname' => 'CHAST',
+        'hasdst' => true,
+        'dstlongname' => 'Chatham Daylight Time',
+        'dstshortname' => 'CHADT' ),
+    'Etc/GMT-13' => array(
+        'offset' => 46800000,
+        'longname' => 'GMT+13:00',
+        'shortname' => 'GMT+13:00',
+        'hasdst' => false ),
+    'Pacific/Enderbury' => array(
+        'offset' => 46800000,
+        'longname' => 'Phoenix Is. Time',
+        'shortname' => 'PHOT',
+        'hasdst' => false ),
+    'Pacific/Tongatapu' => array(
+        'offset' => 46800000,
+        'longname' => 'Tonga Time',
+        'shortname' => 'TOT',
+        'hasdst' => false ),
+    'Etc/GMT-14' => array(
+        'offset' => 50400000,
+        'longname' => 'GMT+14:00',
+        'shortname' => 'GMT+14:00',
+        'hasdst' => false ),
+    'Pacific/Kiritimati' => array(
+        'offset' => 50400000,
+        'longname' => 'Line Is. Time',
+        'shortname' => 'LINT',
+        'hasdst' => false ),
+    'GMT-12:00' => array(
+        'offset' => -43200000,
+        'longname' => 'GMT-12:00',
+        'shortname' => 'GMT-12:00',
+        'hasdst' => false ),
+    'GMT-11:00' => array(
+        'offset' => -39600000,
+        'longname' => 'GMT-11:00',
+        'shortname' => 'GMT-11:00',
+        'hasdst' => false ),
+    'West Samoa Time' => array(
+        'offset' => -39600000,
+        'longname' => 'West Samoa Time',
+        'shortname' => 'WST',
+        'hasdst' => false ),
+    'Samoa Standard Time' => array(
+        'offset' => -39600000,
+        'longname' => 'Samoa Standard Time',
+        'shortname' => 'SST',
+        'hasdst' => false ),
+    'Niue Time' => array(
+        'offset' => -39600000,
+        'longname' => 'Niue Time',
+        'shortname' => 'NUT',
+        'hasdst' => false ),
+    'Hawaii-Aleutian Standard Time' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii-Aleutian Standard Time',
+        'shortname' => 'HAST',
+        'hasdst' => true,
+        'dstlongname' => 'Hawaii-Aleutian Daylight Time',
+        'dstshortname' => 'HADT' ),
+    'GMT-10:00' => array(
+        'offset' => -36000000,
+        'longname' => 'GMT-10:00',
+        'shortname' => 'GMT-10:00',
+        'hasdst' => false ),
+    'Hawaii Standard Time' => array(
+        'offset' => -36000000,
+        'longname' => 'Hawaii Standard Time',
+        'shortname' => 'HST',
+        'hasdst' => false ),
+    'Tokelau Time' => array(
+        'offset' => -36000000,
+        'longname' => 'Tokelau Time',
+        'shortname' => 'TKT',
+        'hasdst' => false ),
+    'Cook Is. Time' => array(
+        'offset' => -36000000,
+        'longname' => 'Cook Is. Time',
+        'shortname' => 'CKT',
+        'hasdst' => false ),
+    'Tahiti Time' => array(
+        'offset' => -36000000,
+        'longname' => 'Tahiti Time',
+        'shortname' => 'TAHT',
+        'hasdst' => false ),
+    'Marquesas Time' => array(
+        'offset' => -34200000,
+        'longname' => 'Marquesas Time',
+        'shortname' => 'MART',
+        'hasdst' => false ),
+    'Alaska Standard Time' => array(
+        'offset' => -32400000,
+        'longname' => 'Alaska Standard Time',
+        'shortname' => 'AKST',
+        'hasdst' => true,
+        'dstlongname' => 'Alaska Daylight Time',
+        'dstshortname' => 'AKDT' ),
+    'GMT-09:00' => array(
+        'offset' => -32400000,
+        'longname' => 'GMT-09:00',
+        'shortname' => 'GMT-09:00',
+        'hasdst' => false ),
+    'Gambier Time' => array(
+        'offset' => -32400000,
+        'longname' => 'Gambier Time',
+        'shortname' => 'GAMT',
+        'hasdst' => false ),
+    'Pacific Standard Time' => array(
+        'offset' => -28800000,
+        'longname' => 'Pacific Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => true,
+        'dstlongname' => 'Pacific Daylight Time',
+        'dstshortname' => 'PDT' ),
+    'GMT-08:00' => array(
+        'offset' => -28800000,
+        'longname' => 'GMT-08:00',
+        'shortname' => 'GMT-08:00',
+        'hasdst' => false ),
+    'Pitcairn Standard Time' => array(
+        'offset' => -28800000,
+        'longname' => 'Pitcairn Standard Time',
+        'shortname' => 'PST',
+        'hasdst' => false ),
+    'Mountain Standard Time' => array(
+        'offset' => -25200000,
+        'longname' => 'Mountain Standard Time',
+        'shortname' => 'MST',
+        'hasdst' => true,
+        'dstlongname' => 'Mountain Daylight Time',
+        'dstshortname' => 'MDT' ),
+    'GMT-07:00' => array(
+        'offset' => -25200000,
+        'longname' => 'GMT-07:00',
+        'shortname' => 'GMT-07:00',
+        'hasdst' => false ),
+    'Central Standard Time' => array(
+        'offset' => -18000000,
+        'longname' => 'Central Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Daylight Time',
+        'dstshortname' => 'CDT' ),
+    'Eastern Standard Time' => array(
+        'offset' => -18000000,
+        'longname' => 'Eastern Standard Time',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Daylight Time',
+        'dstshortname' => 'EDT' ),
+    'Easter Is. Time' => array(
+        'offset' => -21600000,
+        'longname' => 'Easter Is. Time',
+        'shortname' => 'EAST',
+        'hasdst' => true,
+        'dstlongname' => 'Easter Is. Summer Time',
+        'dstshortname' => 'EASST' ),
+    'GMT-06:00' => array(
+        'offset' => -21600000,
+        'longname' => 'GMT-06:00',
+        'shortname' => 'GMT-06:00',
+        'hasdst' => false ),
+    'Galapagos Time' => array(
+        'offset' => -21600000,
+        'longname' => 'Galapagos Time',
+        'shortname' => 'GALT',
+        'hasdst' => false ),
+    'Colombia Time' => array(
+        'offset' => -18000000,
+        'longname' => 'Colombia Time',
+        'shortname' => 'COT',
+        'hasdst' => false ),
+    'Acre Time' => array(
+        'offset' => -18000000,
+        'longname' => 'Acre Time',
+        'shortname' => 'ACT',
+        'hasdst' => false ),
+    'Ecuador Time' => array(
+        'offset' => -18000000,
+        'longname' => 'Ecuador Time',
+        'shortname' => 'ECT',
+        'hasdst' => false ),
+    'Peru Time' => array(
+        'offset' => -18000000,
+        'longname' => 'Peru Time',
+        'shortname' => 'PET',
+        'hasdst' => false ),
+    'GMT-05:00' => array(
+        'offset' => -18000000,
+        'longname' => 'GMT-05:00',
+        'shortname' => 'GMT-05:00',
+        'hasdst' => false ),
+    'Atlantic Standard Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Atlantic Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => true,
+        'dstlongname' => 'Atlantic Daylight Time',
+        'dstshortname' => 'ADT' ),
+    'Paraguay Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Paraguay Time',
+        'shortname' => 'PYT',
+        'hasdst' => true,
+        'dstlongname' => 'Paraguay Summer Time',
+        'dstshortname' => 'PYST' ),
+    'Amazon Standard Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Amazon Standard Time',
+        'shortname' => 'AMT',
+        'hasdst' => false ),
+    'Venezuela Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Venezuela Time',
+        'shortname' => 'VET',
+        'hasdst' => false ),
+    'Guyana Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Guyana Time',
+        'shortname' => 'GYT',
+        'hasdst' => false ),
+    'Bolivia Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Bolivia Time',
+        'shortname' => 'BOT',
+        'hasdst' => false ),
+    'Chile Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Chile Time',
+        'shortname' => 'CLT',
+        'hasdst' => true,
+        'dstlongname' => 'Chile Summer Time',
+        'dstshortname' => 'CLST' ),
+    'Falkland Is. Time' => array(
+        'offset' => -14400000,
+        'longname' => 'Falkland Is. Time',
+        'shortname' => 'FKT',
+        'hasdst' => true,
+        'dstlongname' => 'Falkland Is. Summer Time',
+        'dstshortname' => 'FKST' ),
+    'GMT-04:00' => array(
+        'offset' => -14400000,
+        'longname' => 'GMT-04:00',
+        'shortname' => 'GMT-04:00',
+        'hasdst' => false ),
+    'Newfoundland Standard Time' => array(
+        'offset' => -12600000,
+        'longname' => 'Newfoundland Standard Time',
+        'shortname' => 'NST',
+        'hasdst' => true,
+        'dstlongname' => 'Newfoundland Daylight Time',
+        'dstshortname' => 'NDT' ),
+    'Argentine Time' => array(
+        'offset' => -10800000,
+        'longname' => 'Argentine Time',
+        'shortname' => 'ART',
+        'hasdst' => false ),
+    'Brazil Time' => array(
+        'offset' => -10800000,
+        'longname' => 'Brazil Time',
+        'shortname' => 'BRT',
+        'hasdst' => true,
+        'dstlongname' => 'Brazil Summer Time',
+        'dstshortname' => 'BRST' ),
+    'French Guiana Time' => array(
+        'offset' => -10800000,
+        'longname' => 'French Guiana Time',
+        'shortname' => 'GFT',
+        'hasdst' => false ),
+    'Western Greenland Time' => array(
+        'offset' => -10800000,
+        'longname' => 'Western Greenland Time',
+        'shortname' => 'WGT',
+        'hasdst' => true,
+        'dstlongname' => 'Western Greenland Summer Time',
+        'dstshortname' => 'WGST' ),
+    'Pierre & Miquelon Standard Time' => array(
+        'offset' => -10800000,
+        'longname' => 'Pierre & Miquelon Standard Time',
+        'shortname' => 'PMST',
+        'hasdst' => true,
+        'dstlongname' => 'Pierre & Miquelon Daylight Time',
+        'dstshortname' => 'PMDT' ),
+    'Uruguay Time' => array(
+        'offset' => -10800000,
+        'longname' => 'Uruguay Time',
+        'shortname' => 'UYT',
+        'hasdst' => false ),
+    'Suriname Time' => array(
+        'offset' => -10800000,
+        'longname' => 'Suriname Time',
+        'shortname' => 'SRT',
+        'hasdst' => false ),
+    'GMT-03:00' => array(
+        'offset' => -10800000,
+        'longname' => 'GMT-03:00',
+        'shortname' => 'GMT-03:00',
+        'hasdst' => false ),
+    'Fernando de Noronha Time' => array(
+        'offset' => -7200000,
+        'longname' => 'Fernando de Noronha Time',
+        'shortname' => 'FNT',
+        'hasdst' => false ),
+    'South Georgia Standard Time' => array(
+        'offset' => -7200000,
+        'longname' => 'South Georgia Standard Time',
+        'shortname' => 'GST',
+        'hasdst' => false ),
+    'GMT-02:00' => array(
+        'offset' => -7200000,
+        'longname' => 'GMT-02:00',
+        'shortname' => 'GMT-02:00',
+        'hasdst' => false ),
+    'Eastern Greenland Time' => array(
+        'offset' => 3600000,
+        'longname' => 'Eastern Greenland Time',
+        'shortname' => 'EGT',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Greenland Summer Time',
+        'dstshortname' => 'EGST' ),
+    'Azores Time' => array(
+        'offset' => -3600000,
+        'longname' => 'Azores Time',
+        'shortname' => 'AZOT',
+        'hasdst' => true,
+        'dstlongname' => 'Azores Summer Time',
+        'dstshortname' => 'AZOST' ),
+    'Cape Verde Time' => array(
+        'offset' => -3600000,
+        'longname' => 'Cape Verde Time',
+        'shortname' => 'CVT',
+        'hasdst' => false ),
+    'GMT-01:00' => array(
+        'offset' => -3600000,
+        'longname' => 'GMT-01:00',
+        'shortname' => 'GMT-01:00',
+        'hasdst' => false ),
+    'Greenwich Mean Time' => array(
+        'offset' => 0,
+        'longname' => 'Greenwich Mean Time',
+        'shortname' => 'GMT',
+        'hasdst' => false ),
+    'Western European Time' => array(
+        'offset' => 0,
+        'longname' => 'Western European Time',
+        'shortname' => 'WET',
+        'hasdst' => true,
+        'dstlongname' => 'Western European Summer Time',
+        'dstshortname' => 'WEST' ),
+    'GMT+00:00' => array(
+        'offset' => 0,
+        'longname' => 'GMT+00:00',
+        'shortname' => 'GMT+00:00',
+        'hasdst' => false ),
+    'Coordinated Universal Time' => array(
+        'offset' => 0,
+        'longname' => 'Coordinated Universal Time',
+        'shortname' => 'UTC',
+        'hasdst' => false ),
+    'Central European Time' => array(
+        'offset' => 3600000,
+        'longname' => 'Central European Time',
+        'shortname' => 'CET',
+        'hasdst' => true,
+        'dstlongname' => 'Central European Summer Time',
+        'dstshortname' => 'CEST' ),
+    'Western African Time' => array(
+        'offset' => 3600000,
+        'longname' => 'Western African Time',
+        'shortname' => 'WAT',
+        'hasdst' => true,
+        'dstlongname' => 'Western African Summer Time',
+        'dstshortname' => 'WAST' ),
+    'GMT+01:00' => array(
+        'offset' => 3600000,
+        'longname' => 'GMT+01:00',
+        'shortname' => 'GMT+01:00',
+        'hasdst' => false ),
+    'Middle Europe Time' => array(
+        'offset' => 3600000,
+        'longname' => 'Middle Europe Time',
+        'shortname' => 'MET',
+        'hasdst' => true,
+        'dstlongname' => 'Middle Europe Summer Time',
+        'dstshortname' => 'MEST' ),
+    'Eastern European Time' => array(
+        'offset' => 7200000,
+        'longname' => 'Eastern European Time',
+        'shortname' => 'EET',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern European Summer Time',
+        'dstshortname' => 'EEST' ),
+    'Central African Time' => array(
+        'offset' => 7200000,
+        'longname' => 'Central African Time',
+        'shortname' => 'CAT',
+        'hasdst' => false ),
+    'South Africa Standard Time' => array(
+        'offset' => 7200000,
+        'longname' => 'South Africa Standard Time',
+        'shortname' => 'SAST',
+        'hasdst' => false ),
+    'Israel Standard Time' => array(
+        'offset' => 7200000,
+        'longname' => 'Israel Standard Time',
+        'shortname' => 'IST',
+        'hasdst' => true,
+        'dstlongname' => 'Israel Daylight Time',
+        'dstshortname' => 'IDT' ),
+    'GMT+02:00' => array(
+        'offset' => 7200000,
+        'longname' => 'GMT+02:00',
+        'shortname' => 'GMT+02:00',
+        'hasdst' => false ),
+    'Eastern African Time' => array(
+        'offset' => 10800000,
+        'longname' => 'Eastern African Time',
+        'shortname' => 'EAT',
+        'hasdst' => false ),
+    'Syowa Time' => array(
+        'offset' => 10800000,
+        'longname' => 'Syowa Time',
+        'shortname' => 'SYOT',
+        'hasdst' => false ),
+    'Arabia Standard Time' => array(
+        'offset' => 10800000,
+        'longname' => 'Arabia Standard Time',
+        'shortname' => 'AST',
+        'hasdst' => false ),
+    'GMT+03:00' => array(
+        'offset' => 10800000,
+        'longname' => 'GMT+03:00',
+        'shortname' => 'GMT+03:00',
+        'hasdst' => false ),
+    'Moscow Standard Time' => array(
+        'offset' => 10800000,
+        'longname' => 'Moscow Standard Time',
+        'shortname' => 'MSK',
+        'hasdst' => true,
+        'dstlongname' => 'Moscow Daylight Time',
+        'dstshortname' => 'MSD' ),
+    'GMT+03:07' => array(
+        'offset' => 11224000,
+        'longname' => 'GMT+03:07',
+        'shortname' => 'GMT+03:07',
+        'hasdst' => false ),
+    'Iran Time' => array(
+        'offset' => 12600000,
+        'longname' => 'Iran Time',
+        'shortname' => 'IRT',
+        'hasdst' => true,
+        'dstlongname' => 'Iran Sumer Time',
+        'dstshortname' => 'IRST' ),
+    'Aqtau Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Aqtau Time',
+        'shortname' => 'AQTT',
+        'hasdst' => true,
+        'dstlongname' => 'Aqtau Summer Time',
+        'dstshortname' => 'AQTST' ),
+    'Azerbaijan Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Azerbaijan Time',
+        'shortname' => 'AZT',
+        'hasdst' => true,
+        'dstlongname' => 'Azerbaijan Summer Time',
+        'dstshortname' => 'AZST' ),
+    'Gulf Standard Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Gulf Standard Time',
+        'shortname' => 'GST',
+        'hasdst' => false ),
+    'Georgia Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Georgia Time',
+        'shortname' => 'GET',
+        'hasdst' => true,
+        'dstlongname' => 'Georgia Summer Time',
+        'dstshortname' => 'GEST' ),
+    'Armenia Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Armenia Time',
+        'shortname' => 'AMT',
+        'hasdst' => true,
+        'dstlongname' => 'Armenia Summer Time',
+        'dstshortname' => 'AMST' ),
+    'GMT+04:00' => array(
+        'offset' => 14400000,
+        'longname' => 'GMT+04:00',
+        'shortname' => 'GMT+04:00',
+        'hasdst' => false ),
+    'Samara Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Samara Time',
+        'shortname' => 'SAMT',
+        'hasdst' => true,
+        'dstlongname' => 'Samara Summer Time',
+        'dstshortname' => 'SAMST' ),
+    'Seychelles Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Seychelles Time',
+        'shortname' => 'SCT',
+        'hasdst' => false ),
+    'Mauritius Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Mauritius Time',
+        'shortname' => 'MUT',
+        'hasdst' => false ),
+    'Reunion Time' => array(
+        'offset' => 14400000,
+        'longname' => 'Reunion Time',
+        'shortname' => 'RET',
+        'hasdst' => false ),
+    'Afghanistan Time' => array(
+        'offset' => 16200000,
+        'longname' => 'Afghanistan Time',
+        'shortname' => 'AFT',
+        'hasdst' => false ),
+    'Aqtobe Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Aqtobe Time',
+        'shortname' => 'AQTT',
+        'hasdst' => true,
+        'dstlongname' => 'Aqtobe Summer Time',
+        'dstshortname' => 'AQTST' ),
+    'Turkmenistan Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Turkmenistan Time',
+        'shortname' => 'TMT',
+        'hasdst' => false ),
+    'Kirgizstan Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Kirgizstan Time',
+        'shortname' => 'KGT',
+        'hasdst' => true,
+        'dstlongname' => 'Kirgizstan Summer Time',
+        'dstshortname' => 'KGST' ),
+    'Tajikistan Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Tajikistan Time',
+        'shortname' => 'TJT',
+        'hasdst' => false ),
+    'Pakistan Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Pakistan Time',
+        'shortname' => 'PKT',
+        'hasdst' => false ),
+    'Uzbekistan Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Uzbekistan Time',
+        'shortname' => 'UZT',
+        'hasdst' => false ),
+    'Yekaterinburg Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Yekaterinburg Time',
+        'shortname' => 'YEKT',
+        'hasdst' => true,
+        'dstlongname' => 'Yekaterinburg Summer Time',
+        'dstshortname' => 'YEKST' ),
+    'GMT+05:00' => array(
+        'offset' => 18000000,
+        'longname' => 'GMT+05:00',
+        'shortname' => 'GMT+05:00',
+        'hasdst' => false ),
+    'French Southern & Antarctic Lands Time' => array(
+        'offset' => 18000000,
+        'longname' => 'French Southern & Antarctic Lands Time',
+        'shortname' => 'TFT',
+        'hasdst' => false ),
+    'Maldives Time' => array(
+        'offset' => 18000000,
+        'longname' => 'Maldives Time',
+        'shortname' => 'MVT',
+        'hasdst' => false ),
+    'India Standard Time' => array(
+        'offset' => 19800000,
+        'longname' => 'India Standard Time',
+        'shortname' => 'IST',
+        'hasdst' => false ),
+    'Nepal Time' => array(
+        'offset' => 20700000,
+        'longname' => 'Nepal Time',
+        'shortname' => 'NPT',
+        'hasdst' => false ),
+    'Mawson Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Mawson Time',
+        'shortname' => 'MAWT',
+        'hasdst' => false ),
+    'Vostok time' => array(
+        'offset' => 21600000,
+        'longname' => 'Vostok time',
+        'shortname' => 'VOST',
+        'hasdst' => false ),
+    'Alma-Ata Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Alma-Ata Time',
+        'shortname' => 'ALMT',
+        'hasdst' => true,
+        'dstlongname' => 'Alma-Ata Summer Time',
+        'dstshortname' => 'ALMST' ),
+    'Sri Lanka Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Sri Lanka Time',
+        'shortname' => 'LKT',
+        'hasdst' => false ),
+    'Bangladesh Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Bangladesh Time',
+        'shortname' => 'BDT',
+        'hasdst' => false ),
+    'Novosibirsk Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Novosibirsk Time',
+        'shortname' => 'NOVT',
+        'hasdst' => true,
+        'dstlongname' => 'Novosibirsk Summer Time',
+        'dstshortname' => 'NOVST' ),
+    'Omsk Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Omsk Time',
+        'shortname' => 'OMST',
+        'hasdst' => true,
+        'dstlongname' => 'Omsk Summer Time',
+        'dstshortname' => 'OMSST' ),
+    'Bhutan Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Bhutan Time',
+        'shortname' => 'BTT',
+        'hasdst' => false ),
+    'GMT+06:00' => array(
+        'offset' => 21600000,
+        'longname' => 'GMT+06:00',
+        'shortname' => 'GMT+06:00',
+        'hasdst' => false ),
+    'Indian Ocean Territory Time' => array(
+        'offset' => 21600000,
+        'longname' => 'Indian Ocean Territory Time',
+        'shortname' => 'IOT',
+        'hasdst' => false ),
+    'Myanmar Time' => array(
+        'offset' => 23400000,
+        'longname' => 'Myanmar Time',
+        'shortname' => 'MMT',
+        'hasdst' => false ),
+    'Cocos Islands Time' => array(
+        'offset' => 23400000,
+        'longname' => 'Cocos Islands Time',
+        'shortname' => 'CCT',
+        'hasdst' => false ),
+    'Davis Time' => array(
+        'offset' => 25200000,
+        'longname' => 'Davis Time',
+        'shortname' => 'DAVT',
+        'hasdst' => false ),
+    'Indochina Time' => array(
+        'offset' => 25200000,
+        'longname' => 'Indochina Time',
+        'shortname' => 'ICT',
+        'hasdst' => false ),
+    'Hovd Time' => array(
+        'offset' => 25200000,
+        'longname' => 'Hovd Time',
+        'shortname' => 'HOVT',
+        'hasdst' => false ),
+    'West Indonesia Time' => array(
+        'offset' => 25200000,
+        'longname' => 'West Indonesia Time',
+        'shortname' => 'WIT',
+        'hasdst' => false ),
+    'Krasnoyarsk Time' => array(
+        'offset' => 25200000,
+        'longname' => 'Krasnoyarsk Time',
+        'shortname' => 'KRAT',
+        'hasdst' => true,
+        'dstlongname' => 'Krasnoyarsk Summer Time',
+        'dstshortname' => 'KRAST' ),
+    'GMT+07:00' => array(
+        'offset' => 25200000,
+        'longname' => 'GMT+07:00',
+        'shortname' => 'GMT+07:00',
+        'hasdst' => false ),
+    'Christmas Island Time' => array(
+        'offset' => 25200000,
+        'longname' => 'Christmas Island Time',
+        'shortname' => 'CXT',
+        'hasdst' => false ),
+    'Western Standard Time (Australia)' => array(
+        'offset' => 28800000,
+        'longname' => 'Western Standard Time (Australia)',
+        'shortname' => 'WST',
+        'hasdst' => false ),
+    'Brunei Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Brunei Time',
+        'shortname' => 'BNT',
+        'hasdst' => false ),
+    'China Standard Time' => array(
+        'offset' => 28800000,
+        'longname' => 'China Standard Time',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Hong Kong Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Hong Kong Time',
+        'shortname' => 'HKT',
+        'hasdst' => false ),
+    'Irkutsk Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Irkutsk Time',
+        'shortname' => 'IRKT',
+        'hasdst' => true,
+        'dstlongname' => 'Irkutsk Summer Time',
+        'dstshortname' => 'IRKST' ),
+    'Malaysia Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Malaysia Time',
+        'shortname' => 'MYT',
+        'hasdst' => false ),
+    'Philippines Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Philippines Time',
+        'shortname' => 'PHT',
+        'hasdst' => false ),
+    'Singapore Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Singapore Time',
+        'shortname' => 'SGT',
+        'hasdst' => false ),
+    'Central Indonesia Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Central Indonesia Time',
+        'shortname' => 'CIT',
+        'hasdst' => false ),
+    'Ulaanbaatar Time' => array(
+        'offset' => 28800000,
+        'longname' => 'Ulaanbaatar Time',
+        'shortname' => 'ULAT',
+        'hasdst' => false ),
+    'GMT+08:00' => array(
+        'offset' => 28800000,
+        'longname' => 'GMT+08:00',
+        'shortname' => 'GMT+08:00',
+        'hasdst' => false ),
+    'Choibalsan Time' => array(
+        'offset' => 32400000,
+        'longname' => 'Choibalsan Time',
+        'shortname' => 'CHOT',
+        'hasdst' => false ),
+    'East Timor Time' => array(
+        'offset' => 32400000,
+        'longname' => 'East Timor Time',
+        'shortname' => 'TPT',
+        'hasdst' => false ),
+    'East Indonesia Time' => array(
+        'offset' => 32400000,
+        'longname' => 'East Indonesia Time',
+        'shortname' => 'EIT',
+        'hasdst' => false ),
+    'Korea Standard Time' => array(
+        'offset' => 32400000,
+        'longname' => 'Korea Standard Time',
+        'shortname' => 'KST',
+        'hasdst' => false ),
+    'Japan Standard Time' => array(
+        'offset' => 32400000,
+        'longname' => 'Japan Standard Time',
+        'shortname' => 'JST',
+        'hasdst' => false ),
+    'Yakutsk Time' => array(
+        'offset' => 32400000,
+        'longname' => 'Yakutsk Time',
+        'shortname' => 'YAKT',
+        'hasdst' => true,
+        'dstlongname' => 'Yaktsk Summer Time',
+        'dstshortname' => 'YAKST' ),
+    'GMT+09:00' => array(
+        'offset' => 32400000,
+        'longname' => 'GMT+09:00',
+        'shortname' => 'GMT+09:00',
+        'hasdst' => false ),
+    'Palau Time' => array(
+        'offset' => 32400000,
+        'longname' => 'Palau Time',
+        'shortname' => 'PWT',
+        'hasdst' => false ),
+    'Central Standard Time (Northern Territory)' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (Northern Territory)',
+        'shortname' => 'CST',
+        'hasdst' => false ),
+    'Central Standard Time (South Australia)' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (South Australia)',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Summer Time (South Australia)',
+        'dstshortname' => 'CST' ),
+    'Central Standard Time (South Australia/New South Wales)' => array(
+        'offset' => 34200000,
+        'longname' => 'Central Standard Time (South Australia/New South Wales)',
+        'shortname' => 'CST',
+        'hasdst' => true,
+        'dstlongname' => 'Central Summer Time (South Australia/New South Wales)',
+        'dstshortname' => 'CST' ),
+    'Eastern Standard Time (New South Wales)' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (New South Wales)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (New South Wales)',
+        'dstshortname' => 'EST' ),
+    'Dumont-d\'Urville Time' => array(
+        'offset' => 36000000,
+        'longname' => 'Dumont-d\'Urville Time',
+        'shortname' => 'DDUT',
+        'hasdst' => false ),
+    'Sakhalin Time' => array(
+        'offset' => 36000000,
+        'longname' => 'Sakhalin Time',
+        'shortname' => 'SAKT',
+        'hasdst' => true,
+        'dstlongname' => 'Sakhalin Summer Time',
+        'dstshortname' => 'SAKST' ),
+    'Vladivostok Time' => array(
+        'offset' => 36000000,
+        'longname' => 'Vladivostok Time',
+        'shortname' => 'VLAT',
+        'hasdst' => true,
+        'dstlongname' => 'Vladivostok Summer Time',
+        'dstshortname' => 'VLAST' ),
+    'Eastern Standard Time (Queensland)' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Queensland)',
+        'shortname' => 'EST',
+        'hasdst' => false ),
+    'Eastern Standard Time (Tasmania)' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Tasmania)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (Tasmania)',
+        'dstshortname' => 'EST' ),
+    'Eastern Standard Time (Victoria)' => array(
+        'offset' => 36000000,
+        'longname' => 'Eastern Standard Time (Victoria)',
+        'shortname' => 'EST',
+        'hasdst' => true,
+        'dstlongname' => 'Eastern Summer Time (Victoria)',
+        'dstshortname' => 'EST' ),
+    'GMT+10:00' => array(
+        'offset' => 36000000,
+        'longname' => 'GMT+10:00',
+        'shortname' => 'GMT+10:00',
+        'hasdst' => false ),
+    'Chamorro Standard Time' => array(
+        'offset' => 36000000,
+        'longname' => 'Chamorro Standard Time',
+        'shortname' => 'ChST',
+        'hasdst' => false ),
+    'Papua New Guinea Time' => array(
+        'offset' => 36000000,
+        'longname' => 'Papua New Guinea Time',
+        'shortname' => 'PGT',
+        'hasdst' => false ),
+    'Truk Time' => array(
+        'offset' => 36000000,
+        'longname' => 'Truk Time',
+        'shortname' => 'TRUT',
+        'hasdst' => false ),
+    'Yap Time' => array(
+        'offset' => 36000000,
+        'longname' => 'Yap Time',
+        'shortname' => 'YAPT',
+        'hasdst' => false ),
+    'Load Howe Standard Time' => array(
+        'offset' => 37800000,
+        'longname' => 'Load Howe Standard Time',
+        'shortname' => 'LHST',
+        'hasdst' => true,
+        'dstlongname' => 'Load Howe Summer Time',
+        'dstshortname' => 'LHST' ),
+    'Magadan Time' => array(
+        'offset' => 39600000,
+        'longname' => 'Magadan Time',
+        'shortname' => 'MAGT',
+        'hasdst' => true,
+        'dstlongname' => 'Magadan Summer Time',
+        'dstshortname' => 'MAGST' ),
+    'GMT+11:00' => array(
+        'offset' => 39600000,
+        'longname' => 'GMT+11:00',
+        'shortname' => 'GMT+11:00',
+        'hasdst' => false ),
+    'Vanuatu Time' => array(
+        'offset' => 39600000,
+        'longname' => 'Vanuatu Time',
+        'shortname' => 'VUT',
+        'hasdst' => false ),
+    'Solomon Is. Time' => array(
+        'offset' => 39600000,
+        'longname' => 'Solomon Is. Time',
+        'shortname' => 'SBT',
+        'hasdst' => false ),
+    'Kosrae Time' => array(
+        'offset' => 39600000,
+        'longname' => 'Kosrae Time',
+        'shortname' => 'KOST',
+        'hasdst' => false ),
+    'New Caledonia Time' => array(
+        'offset' => 39600000,
+        'longname' => 'New Caledonia Time',
+        'shortname' => 'NCT',
+        'hasdst' => false ),
+    'Ponape Time' => array(
+        'offset' => 39600000,
+        'longname' => 'Ponape Time',
+        'shortname' => 'PONT',
+        'hasdst' => false ),
+    'Norfolk Time' => array(
+        'offset' => 41400000,
+        'longname' => 'Norfolk Time',
+        'shortname' => 'NFT',
+        'hasdst' => false ),
+    'New Zealand Standard Time' => array(
+        'offset' => 43200000,
+        'longname' => 'New Zealand Standard Time',
+        'shortname' => 'NZST',
+        'hasdst' => true,
+        'dstlongname' => 'New Zealand Daylight Time',
+        'dstshortname' => 'NZDT' ),
+    'Anadyr Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Anadyr Time',
+        'shortname' => 'ANAT',
+        'hasdst' => true,
+        'dstlongname' => 'Anadyr Summer Time',
+        'dstshortname' => 'ANAST' ),
+    'Petropavlovsk-Kamchatski Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Petropavlovsk-Kamchatski Time',
+        'shortname' => 'PETT',
+        'hasdst' => true,
+        'dstlongname' => 'Petropavlovsk-Kamchatski Summer Time',
+        'dstshortname' => 'PETST' ),
+    'GMT+12:00' => array(
+        'offset' => 43200000,
+        'longname' => 'GMT+12:00',
+        'shortname' => 'GMT+12:00',
+        'hasdst' => false ),
+    'Marshall Islands Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Marshall Islands Time',
+        'shortname' => 'MHT',
+        'hasdst' => false ),
+    'Fiji Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Fiji Time',
+        'shortname' => 'FJT',
+        'hasdst' => false ),
+    'Tuvalu Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Tuvalu Time',
+        'shortname' => 'TVT',
+        'hasdst' => false ),
+    'Nauru Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Nauru Time',
+        'shortname' => 'NRT',
+        'hasdst' => false ),
+    'Gilbert Is. Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Gilbert Is. Time',
+        'shortname' => 'GILT',
+        'hasdst' => false ),
+    'Wake Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Wake Time',
+        'shortname' => 'WAKT',
+        'hasdst' => false ),
+    'Wallis & Futuna Time' => array(
+        'offset' => 43200000,
+        'longname' => 'Wallis & Futuna Time',
+        'shortname' => 'WFT',
+        'hasdst' => false ),
+    'Chatham Standard Time' => array(
+        'offset' => 45900000,
+        'longname' => 'Chatham Standard Time',
+        'shortname' => 'CHAST',
+        'hasdst' => true,
+        'dstlongname' => 'Chatham Daylight Time',
+        'dstshortname' => 'CHADT' ),
+    'GMT+13:00' => array(
+        'offset' => 46800000,
+        'longname' => 'GMT+13:00',
+        'shortname' => 'GMT+13:00',
+        'hasdst' => false ),
+    'Phoenix Is. Time' => array(
+        'offset' => 46800000,
+        'longname' => 'Phoenix Is. Time',
+        'shortname' => 'PHOT',
+        'hasdst' => false ),
+    'Tonga Time' => array(
+        'offset' => 46800000,
+        'longname' => 'Tonga Time',
+        'shortname' => 'TOT',
+        'hasdst' => false ),
+    'GMT+14:00' => array(
+        'offset' => 50400000,
+        'longname' => 'GMT+14:00',
+        'shortname' => 'GMT+14:00',
+        'hasdst' => false ),
+    'Line Is. Time' => array(
+        'offset' => 50400000,
+        'longname' => 'Line Is. Time',
+        'shortname' => 'LINT',
+        'hasdst' => false ),
+);
+
+/**
+ * Initialize default timezone
+ *
+ * First try _DATE_TIMEZONE_DEFAULT global, then PHP_TZ environment var,
+ * then TZ environment var
+ */
+if(isset($GLOBALS['_DATE_TIMEZONE_DEFAULT'])
+   && Date_TimeZone::isValidID($GLOBALS['_DATE_TIMEZONE_DEFAULT']))
+{
+    Date_TimeZone::setDefault($GLOBALS['_DATE_TIMEZONE_DEFAULT']);
+} elseif (getenv('PHP_TZ') && Date_TimeZone::isValidID(getenv('PHP_TZ'))) {
+    Date_TimeZone::setDefault(getenv('PHP_TZ'));
+} elseif (getenv('TZ') && Date_TimeZone::isValidID(getenv('TZ'))) {
+    Date_TimeZone::setDefault(getenv('TZ'));
+} elseif (Date_TimeZone::isValidID(date('T'))) {
+    Date_TimeZone::setDefault(date('T'));
+} else {
+    Date_TimeZone::setDefault('UTC');
+}
+
+/*
+ * Local variables:
+ * mode: php
+ * tab-width: 4
+ * c-basic-offset: 4
+ * c-hanging-comment-ender-p: nil
+ * End:
+ */
+?>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Span.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Span.php	(revision 2079)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Span.php	(revision 2079)
@@ -0,0 +1,1083 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
+
+// {{{ Header
+
+/**
+ * Generic time span handling class for PEAR
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 1997-2005 Leandro Lucarella, Pierre-Alain Joye
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted under the terms of the BSD License.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE
+ * COPYRIGHT OWNER OR 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.
+ *
+ * @category   Date and Time
+ * @package    Date
+ * @author     Leandro Lucarella <llucax@php.net>
+ * @author     Pierre-Alain Joye <pajoye@php.net>
+ * @copyright  1997-2006 Leandro Lucarella, Pierre-Alain Joye
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    CVS: $Id: Span.php,v 1.9 2006/11/21 17:38:15 firman Exp $
+ * @link       http://pear.php.net/package/Date
+ * @since      File available since Release 1.4
+ */
+
+// }}}
+// {{{ Includes
+
+/**
+ * Get the Date class
+ */
+require_once 'Date.php';
+
+/**
+ * Get the Date_Calc class
+ */
+require_once 'Date/Calc.php';
+
+// }}}
+// {{{ Constants
+
+/**
+ * Non Numeric Separated Values (NNSV) Input Format.
+ *
+ * Input format guessed from something like this:
+ * days<sep>hours<sep>minutes<sep>seconds
+ * Where <sep> is any quantity of non numeric chars. If no values are
+ * given, time span is set to zero, if one value is given, it's used for
+ * hours, if two values are given it's used for hours and minutes and if
+ * three values are given, it's used for hours, minutes and seconds.<br>
+ * Examples:<br>
+ * ''                   -> 0, 0, 0, 0 (days, hours, minutes, seconds)<br>
+ * '12'                 -> 0, 12, 0, 0
+ * '12.30'              -> 0, 12, 30, 0<br>
+ * '12:30:18'           -> 0, 12, 30, 18<br>
+ * '3-12-30-18'         -> 3, 12, 30, 18<br>
+ * '3 days, 12-30-18'   -> 3, 12, 30, 18<br>
+ * '12:30 with 18 secs' -> 0, 12, 30, 18<br>
+ *
+ * @const int
+ */
+define('DATE_SPAN_INPUT_FORMAT_NNSV', 1);
+
+// }}}
+// {{{ Global Variables
+
+/**
+ * Default time format when converting to a string.
+ *
+ * @global string
+ */
+$GLOBALS['_DATE_SPAN_FORMAT']  = '%C';
+
+/**
+ * Default time format when converting from a string.
+ *
+ * @global mixed
+ */
+$GLOBALS['_DATE_SPAN_INPUT_FORMAT'] = DATE_SPAN_INPUT_FORMAT_NNSV;
+
+// }}}
+// {{{ Class: Date_Span
+
+/**
+ * Generic time span handling class for PEAR
+ *
+ * @author     Leandro Lucarella <llucax@php.net>
+ * @author     Pierre-Alain Joye <pajoye@php.net>
+ * @copyright  1997-2006 Leandro Lucarella, Pierre-Alain Joye
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    Release: 1.4.7
+ * @link       http://pear.php.net/package/Date
+ * @since      Class available since Release 1.4
+ */
+class Date_Span
+{
+    // {{{ Properties
+
+    /**
+     * @var int
+     */
+    var $day;
+
+    /**
+     * @var int
+     */
+    var $hour;
+
+    /**
+     * @var int
+     */
+    var $minute;
+
+    /**
+     * @var int
+     */
+    var $second;
+
+    // }}}
+    // {{{ Constructor
+
+    /**
+     * Constructor.
+     *
+     * Creates the time span object calling the set() method.
+     *
+     * @param  mixed $time   Time span expression.
+     * @param  mixed $format Format string to set it from a string or the
+     *                       second date set it from a date diff.
+     *
+     * @see    set()
+     * @access public
+     */
+    function Date_Span($time = 0, $format = null)
+    {
+        $this->set($time, $format);
+    }
+
+    // }}}
+    // {{{ set()
+
+    /**
+     * Set the time span to a new value in a 'smart' way.
+     *
+     * Sets the time span depending on the argument types, calling
+     * to the appropriate setFromXxx() method.
+     *
+     * @param  mixed $time   Time span expression.
+     * @param  mixed $format Format string to set it from a string or the
+     *                       second date set it from a date diff.
+     *
+     * @return bool  true on success.
+     *
+     * @see    setFromObject()
+     * @see    setFromArray()
+     * @see    setFromString()
+     * @see    setFromSeconds()
+     * @see    setFromDateDiff()
+     * @access public
+     */
+    function set($time = 0, $format = null)
+    {
+        if (is_a($time, 'date_span')) {
+            return $this->copy($time);
+        } elseif (is_a($time, 'date') and is_a($format, 'date')) {
+            return $this->setFromDateDiff($time, $format);
+        } elseif (is_array($time)) {
+            return $this->setFromArray($time);
+        } elseif (is_string($time)) {
+            return $this->setFromString($time, $format);
+        } elseif (is_int($time)) {
+            return $this->setFromSeconds($time);
+        } else {
+            return $this->setFromSeconds(0);
+        }
+    }
+
+    // }}}
+    // {{{ setFromArray()
+
+    /**
+     * Set the time span from an array.
+     *
+     * Set the time span from an array. Any value can be a float (but it
+     * has no sense in seconds), for example array(23.5, 20, 0) is
+     * interpreted as 23 hours, .5*60 + 20 = 50 minutes and 0 seconds.
+     *
+     * @param  array $time Items are counted from right to left. First
+     *                     item is for seconds, second for minutes, third
+     *                     for hours and fourth for days. If there are
+     *                     less items than 4, zero (0) is assumed for the
+     *                     absent values.
+     *
+     * @return bool  True on success.
+     *
+     * @access public
+     */
+    function setFromArray($time)
+    {
+        if (!is_array($time)) {
+            return false;
+        }
+        $tmp1 = new Date_Span;
+        if (!$tmp1->setFromSeconds(@array_pop($time))) {
+            return false;
+        }
+        $tmp2 = new Date_Span;
+        if (!$tmp2->setFromMinutes(@array_pop($time))) {
+            return false;
+        }
+        $tmp1->add($tmp2);
+        if (!$tmp2->setFromHours(@array_pop($time))) {
+            return false;
+        }
+        $tmp1->add($tmp2);
+        if (!$tmp2->setFromDays(@array_pop($time))) {
+            return false;
+        }
+        $tmp1->add($tmp2);
+        return $this->copy($tmp1);
+    }
+
+    // }}}
+    // {{{ setFromString()
+
+    /**
+     * Set the time span from a string based on an input format.
+     *
+     * Set the time span from a string based on an input format. This is
+     * some like a mix of format() method and sscanf() PHP function. The
+     * error checking and validation of this function is very primitive,
+     * so you should be carefull when using it with unknown $time strings.
+     * With this method you are assigning day, hour, minute and second
+     * values, and the last values are used. This means that if you use
+     * something like setFromString('10, 20', '%H, %h') your time span
+     * would be 20 hours long. Allways remember that this method set
+     * <b>all</b> the values, so if you had a $time span 30 minutes long
+     * and you make $time->setFromString('20 hours', '%H hours'), $time
+     * span would be 20 hours long (and not 20 hours and 30 minutes).
+     * Input format options:<br>
+     *  <code>%C</code> Days with time, same as "%D, %H:%M:%S".<br>
+     *  <code>%d</code> Total days as a float number
+     *                  (2 days, 12 hours = 2.5 days).<br>
+     *  <code>%D</code> Days as a decimal number.<br>
+     *  <code>%e</code> Total hours as a float number
+     *                  (1 day, 2 hours, 30 minutes = 26.5 hours).<br>
+     *  <code>%f</code> Total minutes as a float number
+     *                  (2 minutes, 30 seconds = 2.5 minutes).<br>
+     *  <code>%g</code> Total seconds as a decimal number
+     *                  (2 minutes, 30 seconds = 90 seconds).<br>
+     *  <code>%h</code> Hours as decimal number.<br>
+     *  <code>%H</code> Hours as decimal number limited to 2 digits.<br>
+     *  <code>%m</code> Minutes as a decimal number.<br>
+     *  <code>%M</code> Minutes as a decimal number limited to 2 digits.<br>
+     *  <code>%n</code> Newline character (\n).<br>
+     *  <code>%p</code> Either 'am' or 'pm' depending on the time. If 'pm'
+     *                  is detected it adds 12 hours to the resulting time
+     *                  span (without any checks). This is case
+     *                  insensitive.<br>
+     *  <code>%r</code> Time in am/pm notation, same as "%H:%M:%S %p".<br>
+     *  <code>%R</code> Time in 24-hour notation, same as "%H:%M".<br>
+     *  <code>%s</code> Seconds as a decimal number.<br>
+     *  <code>%S</code> Seconds as a decimal number limited to 2 digits.<br>
+     *  <code>%t</code> Tab character (\t).<br>
+     *  <code>%T</code> Current time equivalent, same as "%H:%M:%S".<br>
+     *  <code>%%</code> Literal '%'.<br>
+     *
+     * @param  string $time   String from where to get the time span
+     *                        information.
+     * @param  string $format Format string.
+     *
+     * @return bool   True on success.
+     *
+     * @access public
+     */
+    function setFromString($time, $format = null)
+    {
+        if (is_null($format)) {
+            $format = $GLOBALS['_DATE_SPAN_INPUT_FORMAT'];
+        }
+        // If format is a string, it parses the string format.
+        if (is_string($format)) {
+            $str = '';
+            $vars = array();
+            $pm = 'am';
+            $day = $hour = $minute = $second = 0;
+            for ($i = 0; $i < strlen($format); $i++) {
+                $char = $format{$i};
+                if ($char == '%') {
+                    $nextchar = $format{++$i};
+                    switch ($nextchar) {
+                        case 'c':
+                            $str .= '%d, %d:%d:%d';
+                            array_push(
+                                $vars, 'day', 'hour', 'minute', 'second');
+                            break;
+                        case 'C':
+                            $str .= '%d, %2d:%2d:%2d';
+                            array_push(
+                                $vars, 'day', 'hour', 'minute', 'second');
+                            break;
+                        case 'd':
+                            $str .= '%f';
+                            array_push($vars, 'day');
+                            break;
+                        case 'D':
+                            $str .= '%d';
+                            array_push($vars, 'day');
+                            break;
+                        case 'e':
+                            $str .= '%f';
+                            array_push($vars, 'hour');
+                            break;
+                        case 'f':
+                            $str .= '%f';
+                            array_push($vars, 'minute');
+                            break;
+                        case 'g':
+                            $str .= '%f';
+                            array_push($vars, 'second');
+                            break;
+                        case 'h':
+                            $str .= '%d';
+                            array_push($vars, 'hour');
+                            break;
+                        case 'H':
+                            $str .= '%2d';
+                            array_push($vars, 'hour');
+                            break;
+                        case 'm':
+                            $str .= '%d';
+                            array_push($vars, 'minute');
+                            break;
+                        case 'M':
+                            $str .= '%2d';
+                            array_push($vars, 'minute');
+                            break;
+                        case 'n':
+                            $str .= "\n";
+                            break;
+                        case 'p':
+                            $str .= '%2s';
+                            array_push($vars, 'pm');
+                            break;
+                        case 'r':
+                            $str .= '%2d:%2d:%2d %2s';
+                            array_push(
+                                $vars, 'hour', 'minute', 'second', 'pm');
+                            break;
+                        case 'R':
+                            $str .= '%2d:%2d';
+                            array_push($vars, 'hour', 'minute');
+                            break;
+                        case 's':
+                            $str .= '%d';
+                            array_push($vars, 'second');
+                            break;
+                        case 'S':
+                            $str .= '%2d';
+                            array_push($vars, 'second');
+                            break;
+                        case 't':
+                            $str .= "\t";
+                            break;
+                        case 'T':
+                            $str .= '%2d:%2d:%2d';
+                            array_push($vars, 'hour', 'minute', 'second');
+                            break;
+                        case '%':
+                            $str .= "%";
+                            break;
+                        default:
+                            $str .= $char . $nextchar;
+                    }
+                } else {
+                    $str .= $char;
+                }
+            }
+            $vals = sscanf($time, $str);
+            foreach ($vals as $i => $val) {
+                if (is_null($val)) {
+                    return false;
+                }
+                $$vars[$i] = $val;
+            }
+            if (strcasecmp($pm, 'pm') == 0) {
+                $hour += 12;
+            } elseif (strcasecmp($pm, 'am') != 0) {
+                return false;
+            }
+            $this->setFromArray(array($day, $hour, $minute, $second));
+        // If format is a integer, it uses a predefined format
+        // detection method.
+        } elseif (is_integer($format)) {
+            switch ($format) {
+                case DATE_SPAN_INPUT_FORMAT_NNSV:
+                    $time = preg_split('/\D+/', $time);
+                    switch (count($time)) {
+                        case 0:
+                            return $this->setFromArray(
+                                array(0, 0, 0, 0));
+                        case 1:
+                            return $this->setFromArray(
+                                array(0, $time[0], 0, 0));
+                        case 2:
+                            return $this->setFromArray(
+                                array(0, $time[0], $time[1], 0));
+                        case 3:
+                            return $this->setFromArray(
+                                array(0, $time[0], $time[1], $time[2]));
+                        default:
+                            return $this->setFromArray($time);
+                    }
+                    break;
+            }
+        }
+        return false;
+    }
+
+    // }}}
+    // {{{ setFromSeconds()
+
+    /**
+     * Set the time span from a total number of seconds.
+     *
+     * @param  int  $seconds Total number of seconds.
+     *
+     * @return bool True on success.
+     *
+     * @access public
+     */
+    function setFromSeconds($seconds)
+    {
+        if ($seconds < 0) {
+            return false;
+        }
+        $sec  = intval($seconds);
+        $min  = floor($sec / 60);
+        $hour = floor($min / 60);
+        $day  = intval(floor($hour / 24));
+        $this->second = $sec % 60;
+        $this->minute = $min % 60;
+        $this->hour   = $hour % 24;
+        $this->day    = $day;
+        return true;
+    }
+
+    // }}}
+    // {{{ setFromMinutes()
+
+    /**
+     * Set the time span from a total number of minutes.
+     *
+     * @param  float $minutes Total number of minutes.
+     *
+     * @return bool  True on success.
+     *
+     * @access public
+     */
+    function setFromMinutes($minutes)
+    {
+        return $this->setFromSeconds(round($minutes * 60));
+    }
+
+    // }}}
+    // {{{ setFromHours()
+
+    /**
+     * Set the time span from a total number of hours.
+     *
+     * @param  float $hours Total number of hours.
+     *
+     * @return bool  True on success.
+     *
+     * @access public
+     */
+    function setFromHours($hours)
+    {
+        return $this->setFromSeconds(round($hours * 3600));
+    }
+
+    // }}}
+    // {{{ setFromDays()
+
+    /**
+     * Set the time span from a total number of days.
+     *
+     * @param  float $days Total number of days.
+     *
+     * @return bool  True on success.
+     *
+     * @access public
+     */
+    function setFromDays($days)
+    {
+        return $this->setFromSeconds(round($days * 86400));
+    }
+
+    // }}}
+    // {{{ setFromDateDiff()
+
+    /**
+     * Set the span from the elapsed time between two dates.
+     *
+     * Set the span from the elapsed time between two dates. The time span
+     * is allways positive, so the date's order is not important.
+     *
+     * @param  object Date $date1 First Date.
+     * @param  object Date $date2 Second Date.
+     *
+     * @return bool  True on success.
+     *
+     * @access public
+     */
+    function setFromDateDiff($date1, $date2)
+    {
+        if (!is_a($date1, 'date') or !is_a($date2, 'date')) {
+            return false;
+        }
+        $date1->toUTC();
+        $date2->toUTC();
+        if ($date1->after($date2)) {
+            list($date1, $date2) = array($date2, $date1);
+        }
+        $days = Date_Calc::dateDiff(
+            $date1->getDay(), $date1->getMonth(), $date1->getYear(),
+            $date2->getDay(), $date2->getMonth(), $date2->getYear()
+        );
+        $hours = $date2->getHour() - $date1->getHour();
+        $mins  = $date2->getMinute() - $date1->getMinute();
+        $secs  = $date2->getSecond() - $date1->getSecond();
+        $this->setFromSeconds(
+            $days * 86400 + $hours * 3600 + $mins * 60 + $secs
+        );
+        return true;
+    }
+
+    // }}}
+    // {{{ copy()
+
+    /**
+     * Set the time span from another time object.
+     *
+     * @param  object Date_Span $time Source time span object.
+     *
+     * @return bool   True on success.
+     *
+     * @access public
+     */
+    function copy($time)
+    {
+        if (is_a($time, 'date_span')) {
+            $this->second = $time->second;
+            $this->minute = $time->minute;
+            $this->hour   = $time->hour;
+            $this->day    = $time->day;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // }}}
+    // {{{ format()
+
+    /**
+     * Time span pretty printing (similar to Date::format()).
+     *
+     * Formats the time span in the given format, similar to
+     * strftime() and Date::format().<br>
+     * <br>
+     * Formatting options:<br>
+     *  <code>%C</code> Days with time, same as "%D, %H:%M:%S".<br>
+     *  <code>%d</code> Total days as a float number
+     *                  (2 days, 12 hours = 2.5 days).<br>
+     *  <code>%D</code> Days as a decimal number.<br>
+     *  <code>%e</code> Total hours as a float number
+     *                  (1 day, 2 hours, 30 minutes = 26.5 hours).<br>
+     *  <code>%E</code> Total hours as a decimal number
+     *                  (1 day, 2 hours, 40 minutes = 26 hours).<br>
+     *  <code>%f</code> Total minutes as a float number
+     *                  (2 minutes, 30 seconds = 2.5 minutes).<br>
+     *  <code>%F</code> Total minutes as a decimal number
+     *                  (1 hour, 2 minutes, 40 seconds = 62 minutes).<br>
+     *  <code>%g</code> Total seconds as a decimal number
+     *                  (2 minutes, 30 seconds = 90 seconds).<br>
+     *  <code>%h</code> Hours as decimal number (0 to 23).<br>
+     *  <code>%H</code> Hours as decimal number (00 to 23).<br>
+     *  <code>%i</code> Hours as decimal number on 12-hour clock
+     *                  (1 to 12).<br>
+     *  <code>%I</code> Hours as decimal number on 12-hour clock
+     *                  (01 to 12).<br>
+     *  <code>%m</code> Minutes as a decimal number (0 to 59).<br>
+     *  <code>%M</code> Minutes as a decimal number (00 to 59).<br>
+     *  <code>%n</code> Newline character (\n).<br>
+     *  <code>%p</code> Either 'am' or 'pm' depending on the time.<br>
+     *  <code>%P</code> Either 'AM' or 'PM' depending on the time.<br>
+     *  <code>%r</code> Time in am/pm notation, same as "%I:%M:%S %p".<br>
+     *  <code>%R</code> Time in 24-hour notation, same as "%H:%M".<br>
+     *  <code>%s</code> Seconds as a decimal number (0 to 59).<br>
+     *  <code>%S</code> Seconds as a decimal number (00 to 59).<br>
+     *  <code>%t</code> Tab character (\t).<br>
+     *  <code>%T</code> Current time equivalent, same as "%H:%M:%S".<br>
+     *  <code>%%</code> Literal '%'.<br>
+     *
+     * @param  string $format The format string for returned time span.
+     *
+     * @return string The time span in specified format.
+     *
+     * @access public
+     */
+    function format($format = null)
+    {
+        if (is_null($format)) {
+            $format = $GLOBALS['_DATE_SPAN_FORMAT'];
+        }
+        $output = '';
+        for ($i = 0; $i < strlen($format); $i++) {
+            $char = $format{$i};
+            if ($char == '%') {
+                $nextchar = $format{++$i};
+                switch ($nextchar) {
+                    case 'C':
+                        $output .= sprintf(
+                            '%d, %02d:%02d:%02d',
+                            $this->day,
+                            $this->hour,
+                            $this->minute,
+                            $this->second
+                        );
+                        break;
+                    case 'd':
+                        $output .= $this->toDays();
+                        break;
+                    case 'D':
+                        $output .= $this->day;
+                        break;
+                    case 'e':
+                        $output .= $this->toHours();
+                        break;
+                    case 'E':
+                        $output .= floor($this->toHours());
+                        break;
+                    case 'f':
+                        $output .= $this->toMinutes();
+                        break;
+                    case 'F':
+                        $output .= floor($this->toMinutes());
+                        break;
+                    case 'g':
+                        $output .= $this->toSeconds();
+                        break;
+                    case 'h':
+                        $output .= $this->hour;
+                        break;
+                    case 'H':
+                        $output .= sprintf('%02d', $this->hour);
+                        break;
+                    case 'i':
+                        $hour =
+                            ($this->hour + 1) > 12 ?
+                            $this->hour - 12 :
+                            $this->hour;
+                        $output .= ($hour == 0) ? 12 : $hour;
+                        break;
+                    case 'I':
+                        $hour =
+                            ($this->hour + 1) > 12 ?
+                            $this->hour - 12 :
+                            $this->hour;
+                        $output .= sprintf('%02d', $hour==0 ? 12 : $hour);
+                        break;
+                    case 'm':
+                        $output .= $this->minute;
+                        break;
+                    case 'M':
+                        $output .= sprintf('%02d',$this->minute);
+                        break;
+                    case 'n':
+                        $output .= "\n";
+                        break;
+                    case 'p':
+                        $output .= $this->hour >= 12 ? 'pm' : 'am';
+                        break;
+                    case 'P':
+                        $output .= $this->hour >= 12 ? 'PM' : 'AM';
+                        break;
+                    case 'r':
+                        $hour =
+                            ($this->hour + 1) > 12 ?
+                            $this->hour - 12 :
+                            $this->hour;
+                        $output .= sprintf(
+                            '%02d:%02d:%02d %s',
+                            $hour==0 ?  12 : $hour,
+                            $this->minute,
+                            $this->second,
+                            $this->hour >= 12 ? 'pm' : 'am'
+                        );
+                        break;
+                    case 'R':
+                        $output .= sprintf(
+                            '%02d:%02d', $this->hour, $this->minute
+                        );
+                        break;
+                    case 's':
+                        $output .= $this->second;
+                        break;
+                    case 'S':
+                        $output .= sprintf('%02d', $this->second);
+                        break;
+                    case 't':
+                        $output .= "\t";
+                        break;
+                    case 'T':
+                        $output .= sprintf(
+                            '%02d:%02d:%02d',
+                            $this->hour, $this->minute, $this->second
+                        );
+                        break;
+                    case '%':
+                        $output .= "%";
+                        break;
+                    default:
+                        $output .= $char . $nextchar;
+                }
+            } else {
+                $output .= $char;
+            }
+        }
+        return $output;
+    }
+
+    // }}}
+    // {{{ toSeconds()
+
+    /**
+     * Convert time span to seconds.
+     *
+     * @return int Time span as an integer number of seconds.
+     *
+     * @access public
+     */
+    function toSeconds()
+    {
+        return $this->day * 86400 + $this->hour * 3600 +
+            $this->minute * 60 + $this->second;
+    }
+
+    // }}}
+    // {{{ toMinutes()
+
+    /**
+     * Convert time span to minutes.
+     *
+     * @return float Time span as a decimal number of minutes.
+     *
+     * @access public
+     */
+    function toMinutes()
+    {
+        return $this->day * 1440 + $this->hour * 60 + $this->minute +
+            $this->second / 60;
+    }
+
+    // }}}
+    // {{{ toHours()
+
+    /**
+     * Convert time span to hours.
+     *
+     * @return float Time span as a decimal number of hours.
+     *
+     * @access public
+     */
+    function toHours()
+    {
+        return $this->day * 24 + $this->hour + $this->minute / 60 +
+            $this->second / 3600;
+    }
+
+    // }}}
+    // {{{ toDays()
+
+    /**
+     * Convert time span to days.
+     *
+     * @return float Time span as a decimal number of days.
+     *
+     * @access public
+     */
+    function toDays()
+    {
+        return $this->day + $this->hour / 24 + $this->minute / 1440 +
+            $this->second / 86400;
+    }
+
+    // }}}
+    // {{{ add()
+
+    /**
+     * Adds a time span.
+     *
+     * @param  object Date_Span $time Time span to add.
+     *
+     * @access public
+     */
+    function add($time)
+    {
+        return $this->setFromSeconds(
+            $this->toSeconds() + $time->toSeconds()
+        );
+    }
+
+    // }}}
+    // {{{ substract()
+
+    /**
+     * Subtracts a time span.
+     *
+     * Subtracts a time span. If the time span to subtract is larger
+     * than the original, the result is zero (there's no sense in
+     * negative time spans).
+     *
+     * @param  object Date_Span $time Time span to subtract.
+     *
+     * @access public
+     */
+    function subtract($time)
+    {
+        $sub = $this->toSeconds() - $time->toSeconds();
+        if ($sub < 0) {
+            $this->setFromSeconds(0);
+        } else {
+            $this->setFromSeconds($sub);
+        }
+    }
+
+    // }}}
+    // {{{ equal()
+
+    /**
+     * Tells if time span is equal to $time.
+     *
+     * @param  object Date_Span $time Time span to compare to.
+     *
+     * @return bool   True if the time spans are equal.
+     *
+     * @access public
+     */
+    function equal($time)
+    {
+        return $this->toSeconds() == $time->toSeconds();
+    }
+
+    // }}}
+    // {{{ greaterEqual()
+
+    /**
+     * Tells if this time span is greater or equal than $time.
+     *
+     * @param  object Date_Span $time Time span to compare to.
+     *
+     * @return bool   True if this time span is greater or equal than $time.
+     *
+     * @access public
+     */
+    function greaterEqual($time)
+    {
+        return $this->toSeconds() >= $time->toSeconds();
+    }
+
+    // }}}
+    // {{{ lowerEqual()
+
+    /**
+     * Tells if this time span is lower or equal than $time.
+     *
+     * @param  object Date_Span $time Time span to compare to.
+     *
+     * @return bool   True if this time span is lower or equal than $time.
+     *
+     * @access public
+     */
+    function lowerEqual($time)
+    {
+        return $this->toSeconds() <= $time->toSeconds();
+    }
+
+    // }}}
+    // {{{ greater()
+
+    /**
+     * Tells if this time span is greater than $time.
+     *
+     * @param  object Date_Span $time Time span to compare to.
+     *
+     * @return bool   True if this time span is greater than $time.
+     *
+     * @access public
+     */
+    function greater($time)
+    {
+        return $this->toSeconds() > $time->toSeconds();
+    }
+
+    // }}}
+    // {{{ lower()
+
+    /**
+     * Tells if this time span is lower than $time.
+     *
+     * @param  object Date_Span $time Time span to compare to.
+     *
+     * @return bool   True if this time span is lower than $time.
+     *
+     * @access public
+     */
+    function lower($time)
+    {
+        return $this->toSeconds() < $time->toSeconds();
+    }
+
+    // }}}
+    // {{{ compare()
+
+    /**
+     * Compares two time spans.
+     *
+     * Compares two time spans. Suitable for use in sorting functions.
+     *
+     * @param  object Date_Span $time1 The first time span.
+     * @param  object Date_Span $time2 The second time span.
+     *
+     * @return int    0 if the time spans are equal, -1 if time1 is lower
+     *                than time2, 1 if time1 is greater than time2.
+     *
+     * @static
+     * @access public
+     */
+    function compare($time1, $time2)
+    {
+        if ($time1->equal($time2)) {
+            return 0;
+        } elseif ($time1->lower($time2)) {
+            return -1;
+        } else {
+            return 1;
+        }
+    }
+
+    // }}}
+    // {{{ isEmpty()
+
+    /**
+     * Tells if the time span is empty (zero length).
+     *
+     * @return bool True is it's empty.
+     */
+    function isEmpty()
+    {
+        return !$this->day && !$this->hour && !$this->minute && !$this->second;
+    }
+
+    // }}}
+    // {{{ setDefaultInputFormat()
+
+    /**
+     * Set the default input format.
+     *
+     * @param  mixed $format New default input format.
+     *
+     * @return mixed Previous default input format.
+     *
+     * @static
+     */
+    function setDefaultInputFormat($format)
+    {
+        $old = $GLOBALS['_DATE_SPAN_INPUT_FORMAT'];
+        $GLOBALS['_DATE_SPAN_INPUT_FORMAT'] = $format;
+        return $old;
+    }
+
+    // }}}
+    // {{{ getDefaultInputFormat()
+
+    /**
+     * Get the default input format.
+     *
+     * @return mixed Default input format.
+     *
+     * @static
+     */
+    function getDefaultInputFormat()
+    {
+        return $GLOBALS['_DATE_SPAN_INPUT_FORMAT'];
+    }
+
+    // }}}
+    // {{{ setDefaultFormat()
+
+    /**
+     * Set the default format.
+     *
+     * @param  mixed $format New default format.
+     *
+     * @return mixed Previous default format.
+     *
+     * @static
+     */
+    function setDefaultFormat($format)
+    {
+        $old = $GLOBALS['_DATE_SPAN_FORMAT'];
+        $GLOBALS['_DATE_SPAN_FORMAT'] = $format;
+        return $old;
+    }
+
+    // }}}
+    // {{{ getDefaultFormat()
+
+    /**
+     * Get the default format.
+     *
+     * @return mixed Default format.
+     *
+     * @static
+     */
+    function getDefaultFormat()
+    {
+        return $GLOBALS['_DATE_SPAN_FORMAT'];
+    }
+
+    // }}}
+    // {{{ __clone()
+
+    /**
+     * Returns a copy of the object (workarround for PHP5 forward compatibility).
+     *
+     * @return object Date_Span Copy of the object.
+     */
+    function __clone() {
+        $c = get_class($this);
+        $s = new $c;
+        $s->day    = $this->day;
+        $s->hour   = $this->hour;
+        $s->minute = $this->minute;
+        $s->second = $this->second;
+        return $s;
+    }
+
+    // }}}
+}
+
+// }}}
+
+/*
+ * Local variables:
+ * mode: php
+ * tab-width: 4
+ * c-basic-offset: 4
+ * c-hanging-comment-ender-p: nil
+ * End:
+ */
+?>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Calc.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Calc.php	(revision 2079)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Calc.php	(revision 2079)
@@ -0,0 +1,2117 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
+
+// {{{ Header
+
+/**
+ * Calculates, manipulates and retrieves dates
+ *
+ * It does not rely on 32-bit system time stamps, so it works dates
+ * before 1970 and after 2038.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 1999-2006 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted under the terms of the BSD License.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE
+ * COPYRIGHT OWNER OR 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.
+ *
+ * @category   Date and Time
+ * @package    Date
+ * @author     Monte Ohrt <monte@ispi.net>
+ * @author     Pierre-Alain Joye <pajoye@php.net>
+ * @author     Daniel Convissor <danielc@php.net>
+ * @copyright  1999-2006 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    CVS: $Id: Calc.php,v 1.35 2006/11/21 23:01:13 firman Exp $
+ * @link       http://pear.php.net/package/Date
+ * @since      File available since Release 1.2
+ */
+
+// }}}
+
+if (!defined('DATE_CALC_BEGIN_WEEKDAY')) {
+    /**
+     * Defines what day starts the week
+     *
+     * Monday (1) is the international standard.
+     * Redefine this to 0 if you want weeks to begin on Sunday.
+     */
+    define('DATE_CALC_BEGIN_WEEKDAY', 1);
+}
+
+if (!defined('DATE_CALC_FORMAT')) {
+    /**
+     * The default value for each method's $format parameter
+     *
+     * The default is '%Y%m%d'.  To override this default, define
+     * this constant before including Calc.php.
+     *
+     * @since Constant available since Release 1.4.4
+     */
+    define('DATE_CALC_FORMAT', '%Y%m%d');
+}
+
+// {{{ Class: Date_Calc
+
+/**
+ * Calculates, manipulates and retrieves dates
+ *
+ * It does not rely on 32-bit system time stamps, so it works dates
+ * before 1970 and after 2038.
+ *
+ * @author     Monte Ohrt <monte@ispi.net>
+ * @author     Daniel Convissor <danielc@php.net>
+ * @copyright  1999-2006 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    Release: 1.4.7
+ * @link       http://pear.php.net/package/Date
+ * @since      Class available since Release 1.2
+ */
+class Date_Calc
+{
+    // {{{ dateFormat()
+
+    /**
+     * Formats the date in the given format, much like strfmt()
+     *
+     * This function is used to alleviate the problem with 32-bit numbers for
+     * dates pre 1970 or post 2038, as strfmt() has on most systems.
+     * Most of the formatting options are compatible.
+     *
+     * Formatting options:
+     * <pre>
+     * %a   abbreviated weekday name (Sun, Mon, Tue)
+     * %A   full weekday name (Sunday, Monday, Tuesday)
+     * %b   abbreviated month name (Jan, Feb, Mar)
+     * %B   full month name (January, February, March)
+     * %d   day of month (range 00 to 31)
+     * %e   day of month, single digit (range 0 to 31)
+     * %E   number of days since unspecified epoch (integer)
+     *        (%E is useful for passing a date in a URL as
+     *        an integer value. Then simply use
+     *        daysToDate() to convert back to a date.)
+     * %j   day of year (range 001 to 366)
+     * %m   month as decimal number (range 1 to 12)
+     * %n   newline character (\n)
+     * %t   tab character (\t)
+     * %w   weekday as decimal (0 = Sunday)
+     * %U   week number of current year, first sunday as first week
+     * %y   year as decimal (range 00 to 99)
+     * %Y   year as decimal including century (range 0000 to 9999)
+     * %%   literal '%'
+     * </pre>
+     *
+     * @param int    $day     the day of the month
+     * @param int    $month   the month
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     * @param string $format  the format string
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function dateFormat($day, $month, $year, $format)
+    {
+        if (!Date_Calc::isValidDate($day, $month, $year)) {
+            $year  = Date_Calc::dateNow('%Y');
+            $month = Date_Calc::dateNow('%m');
+            $day   = Date_Calc::dateNow('%d');
+        }
+
+        $output = '';
+
+        for ($strpos = 0; $strpos < strlen($format); $strpos++) {
+            $char = substr($format, $strpos, 1);
+            if ($char == '%') {
+                $nextchar = substr($format, $strpos + 1, 1);
+                switch($nextchar) {
+                    case 'a':
+                        $output .= Date_Calc::getWeekdayAbbrname($day, $month, $year);
+                        break;
+                    case 'A':
+                        $output .= Date_Calc::getWeekdayFullname($day, $month, $year);
+                        break;
+                    case 'b':
+                        $output .= Date_Calc::getMonthAbbrname($month);
+                        break;
+                    case 'B':
+                        $output .= Date_Calc::getMonthFullname($month);
+                        break;
+                    case 'd':
+                        $output .= sprintf('%02d', $day);
+                        break;
+                    case 'e':
+                        $output .= $day;
+                        break;
+                    case 'E':
+                        $output .= Date_Calc::dateToDays($day, $month, $year);
+                        break;
+                    case 'j':
+                        $output .= Date_Calc::julianDate($day, $month, $year);
+                        break;
+                    case 'm':
+                        $output .= sprintf('%02d', $month);
+                        break;
+                    case 'n':
+                        $output .= "\n";
+                        break;
+                    case 't':
+                        $output .= "\t";
+                        break;
+                    case 'w':
+                        $output .= Date_Calc::dayOfWeek($day, $month, $year);
+                        break;
+                    case 'U':
+                        $output .= Date_Calc::weekOfYear($day, $month, $year);
+                        break;
+                    case 'y':
+                        $output .= substr($year, 2, 2);
+                        break;
+                    case 'Y':
+                        $output .= $year;
+                        break;
+                    case '%':
+                        $output .= '%';
+                        break;
+                    default:
+                        $output .= $char.$nextchar;
+                }
+                $strpos++;
+            } else {
+                $output .= $char;
+            }
+        }
+        return $output;
+    }
+
+    // }}}
+    // {{{ defaultCentury()
+
+    /**
+     * Turns a two digit year into a four digit year
+     *
+     * From '51 to '99 is in the 1900's, otherwise it's in the 2000's.
+     *
+     * @param int    $year    the 2 digit year
+     *
+     * @return string  the 4 digit year
+     *
+     * @access public
+     * @static
+     */
+    function defaultCentury($year)
+    {
+        if (strlen($year) == 1) {
+            $year = '0' . $year;
+        }
+        if ($year > 50) {
+            return '19' . $year;
+        } else {
+            return '20' . $year;
+        }
+    }
+
+    // }}}
+    // {{{ dateToDays()
+
+    /**
+     * Converts a date to number of days since a distant unspecified epoch
+     *
+     * @param int    $day     the day of the month
+     * @param int    $month   the month
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return integer  the number of days since the Date_Calc epoch
+     *
+     * @access public
+     * @static
+     */
+    function dateToDays($day, $month, $year)
+    {
+        $century = (int)substr($year, 0, 2);
+        $year = (int)substr($year, 2, 2);
+        if ($month > 2) {
+            $month -= 3;
+        } else {
+            $month += 9;
+            if ($year) {
+                $year--;
+            } else {
+                $year = 99;
+                $century --;
+            }
+        }
+
+        return (floor((146097 * $century) / 4 ) +
+                floor((1461 * $year) / 4 ) +
+                floor((153 * $month + 2) / 5 ) +
+                $day + 1721119);
+    }
+
+    // }}}
+    // {{{ daysToDate()
+
+    /**
+     * Converts number of days to a distant unspecified epoch
+     *
+     * @param int    $days    the number of days since the Date_Calc epoch
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function daysToDate($days, $format = DATE_CALC_FORMAT)
+    {
+        $days   -= 1721119;
+        $century = floor((4 * $days - 1) / 146097);
+        $days    = floor(4 * $days - 1 - 146097 * $century);
+        $day     = floor($days / 4);
+
+        $year    = floor((4 * $day +  3) / 1461);
+        $day     = floor(4 * $day +  3 - 1461 * $year);
+        $day     = floor(($day +  4) / 4);
+
+        $month   = floor((5 * $day - 3) / 153);
+        $day     = floor(5 * $day - 3 - 153 * $month);
+        $day     = floor(($day +  5) /  5);
+
+        if ($month < 10) {
+            $month +=3;
+        } else {
+            $month -=9;
+            if ($year++ == 99) {
+                $year = 0;
+                $century++;
+            }
+        }
+
+        $century = sprintf('%02d', $century);
+        $year    = sprintf('%02d', $year);
+        return Date_Calc::dateFormat($day, $month, $century . $year, $format);
+    }
+
+    // }}}
+    // {{{ gregorianToISO()
+
+    /**
+     * Converts from Gregorian Year-Month-Day to ISO Year-WeekNumber-WeekDay
+     *
+     * Uses ISO 8601 definitions.  Algorithm by Rick McCarty, 1999 at
+     * http://personal.ecu.edu/mccartyr/ISOwdALG.txt .
+     * Transcribed to PHP by Jesus M. Castagnetto.
+     *
+     * @param int    $day     the day of the month
+     * @param int    $month   the month
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return string  the date in ISO Year-WeekNumber-WeekDay format
+     *
+     * @access public
+     * @static
+     */
+    function gregorianToISO($day, $month, $year)
+    {
+        $mnth = array (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
+        $y_isleap = Date_Calc::isLeapYear($year);
+        $y_1_isleap = Date_Calc::isLeapYear($year - 1);
+        $day_of_year_number = $day + $mnth[$month - 1];
+        if ($y_isleap && $month > 2) {
+            $day_of_year_number++;
+        }
+        // find Jan 1 weekday (monday = 1, sunday = 7)
+        $yy = ($year - 1) % 100;
+        $c = ($year - 1) - $yy;
+        $g = $yy + intval($yy / 4);
+        $jan1_weekday = 1 + intval((((($c / 100) % 4) * 5) + $g) % 7);
+        // weekday for year-month-day
+        $h = $day_of_year_number + ($jan1_weekday - 1);
+        $weekday = 1 + intval(($h - 1) % 7);
+        // find if Y M D falls in YearNumber Y-1, WeekNumber 52 or
+        if ($day_of_year_number <= (8 - $jan1_weekday) && $jan1_weekday > 4){
+            $yearnumber = $year - 1;
+            if ($jan1_weekday == 5 || ($jan1_weekday == 6 && $y_1_isleap)) {
+                $weeknumber = 53;
+            } else {
+                $weeknumber = 52;
+            }
+        } else {
+            $yearnumber = $year;
+        }
+        // find if Y M D falls in YearNumber Y+1, WeekNumber 1
+        if ($yearnumber == $year) {
+            if ($y_isleap) {
+                $i = 366;
+            } else {
+                $i = 365;
+            }
+            if (($i - $day_of_year_number) < (4 - $weekday)) {
+                $yearnumber++;
+                $weeknumber = 1;
+            }
+        }
+        // find if Y M D falls in YearNumber Y, WeekNumber 1 through 53
+        if ($yearnumber == $year) {
+            $j = $day_of_year_number + (7 - $weekday) + ($jan1_weekday - 1);
+            $weeknumber = intval($j / 7);
+            if ($jan1_weekday > 4) {
+                $weeknumber--;
+            }
+        }
+        // put it all together
+        if ($weeknumber < 10) {
+            $weeknumber = '0'.$weeknumber;
+        }
+        return $yearnumber . '-' . $weeknumber . '-' . $weekday;
+    }
+
+    // }}}
+    // {{{ dateSeason()
+
+    /**
+     * Determines julian date of the given season
+     *
+     * Adapted from previous work in Java by James Mark Hamilton.
+     *
+     * @param string $season  the season to get the date for: VERNALEQUINOX,
+     *                         SUMMERSOLSTICE, AUTUMNALEQUINOX,
+     *                         or WINTERSOLSTICE
+     * @param string $year    the year in four digit format.  Must be between
+     *                         -1000BC and 3000AD.
+     *
+     * @return float  the julian date the season starts on
+     *
+     * @author James Mark Hamilton <mhamilton@qwest.net>
+     * @author Robert Butler <rob@maxwellcreek.org>
+     * @access public
+     * @static
+     */
+    function dateSeason($season, $year = 0)
+    {
+        if ($year == '') {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (($year >= -1000) && ($year <= 1000)) {
+            $y = $year / 1000.0;
+            switch ($season) {
+                case 'VERNALEQUINOX':
+                    $juliandate = (((((((-0.00071 * $y) - 0.00111) * $y) + 0.06134) * $y) + 365242.1374) * $y) + 1721139.29189;
+                    break;
+                case 'SUMMERSOLSTICE':
+                    $juliandate = (((((((0.00025 * $y) + 0.00907) * $y) - 0.05323) * $y) + 365241.72562) * $y) + 1721233.25401;
+                    break;
+                case 'AUTUMNALEQUINOX':
+                    $juliandate = (((((((0.00074 * $y) - 0.00297) * $y) - 0.11677) * $y) + 365242.49558) * $y) + 1721325.70455;
+                    break;
+                case 'WINTERSOLSTICE':
+                default:
+                    $juliandate = (((((((-0.00006 * $y) - 0.00933) * $y) - 0.00769) * $y) + 365242.88257) * $y) + 1721414.39987;
+            }
+        } elseif (($year > 1000) && ($year <= 3000)) {
+            $y = ($year - 2000) / 1000;
+            switch ($season) {
+                case 'VERNALEQUINOX':
+                    $juliandate = (((((((-0.00057 * $y) - 0.00411) * $y) + 0.05169) * $y) + 365242.37404) * $y) + 2451623.80984;
+                    break;
+                case 'SUMMERSOLSTICE':
+                    $juliandate = (((((((-0.0003 * $y) + 0.00888) * $y) + 0.00325) * $y) + 365241.62603) * $y) + 2451716.56767;
+                    break;
+                case 'AUTUMNALEQUINOX':
+                    $juliandate = (((((((0.00078 * $y) + 0.00337) * $y) - 0.11575) * $y) + 365242.01767) * $y) + 2451810.21715;
+                    break;
+                case 'WINTERSOLSTICE':
+                default:
+                    $juliandate = (((((((0.00032 * $y) - 0.00823) * $y) - 0.06223) * $y) + 365242.74049) * $y) + 2451900.05952;
+            }
+        }
+        return $juliandate;
+    }
+
+    // }}}
+    // {{{ dateNow()
+
+    /**
+     * Returns the current local date
+     *
+     * NOTE: This function retrieves the local date using strftime(),
+     * which may or may not be 32-bit safe on your system.
+     *
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the current date in the specified format
+     *
+     * @access public
+     * @static
+     */
+    function dateNow($format = DATE_CALC_FORMAT)
+    {
+        return strftime($format, time());
+    }
+
+    // }}}
+    // {{{ getYear()
+
+    /**
+     * Returns the current local year in format CCYY
+     *
+     * @return string  the current year in four digit format
+     *
+     * @access public
+     * @static
+     */
+    function getYear()
+    {
+        return Date_Calc::dateNow('%Y');
+    }
+
+    // }}}
+    // {{{ getMonth()
+
+    /**
+     * Returns the current local month in format MM
+     *
+     * @return string  the current month in two digit format
+     *
+     * @access public
+     * @static
+     */
+    function getMonth()
+    {
+        return Date_Calc::dateNow('%m');
+    }
+
+    // }}}
+    // {{{ getDay()
+
+    /**
+     * Returns the current local day in format DD
+     *
+     * @return string  the current day of the month in two digit format
+     *
+     * @access public
+     * @static
+     */
+    function getDay()
+    {
+        return Date_Calc::dateNow('%d');
+    }
+
+    // }}}
+    // {{{ julianDate()
+
+    /**
+     * Returns number of days since 31 December of year before given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return int  the julian date for the date
+     *
+     * @access public
+     * @static
+     */
+    function julianDate($day = 0, $month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $days = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
+        $julian = ($days[$month - 1] + $day);
+        if ($month > 2 && Date_Calc::isLeapYear($year)) {
+            $julian++;
+        }
+        return $julian;
+    }
+
+    // }}}
+    // {{{ getWeekdayFullname()
+
+    /**
+     * Returns the full weekday name for the given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return string  the full name of the day of the week
+     *
+     * @access public
+     * @static
+     */
+    function getWeekdayFullname($day = 0, $month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $weekday_names = Date_Calc::getWeekDays();
+        $weekday = Date_Calc::dayOfWeek($day, $month, $year);
+        return $weekday_names[$weekday];
+    }
+
+    // }}}
+    // {{{ getWeekdayAbbrname()
+
+    /**
+     * Returns the abbreviated weekday name for the given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param int    $length  the length of abbreviation
+     *
+     * @return string  the abbreviated name of the day of the week
+     *
+     * @access public
+     * @static
+     * @see Date_Calc::getWeekdayFullname()
+     */
+    function getWeekdayAbbrname($day = 0, $month = 0, $year = 0, $length = 3)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        return substr(Date_Calc::getWeekdayFullname($day, $month, $year),
+                      0, $length);
+    }
+
+    // }}}
+    // {{{ getMonthFullname()
+
+    /**
+     * Returns the full month name for the given month
+     *
+     * @param int    $month   the month
+     *
+     * @return string  the full name of the month
+     *
+     * @access public
+     * @static
+     */
+    function getMonthFullname($month)
+    {
+        $month = (int)$month;
+        if (empty($month)) {
+            $month = (int)Date_Calc::dateNow('%m');
+        }
+        $month_names = Date_Calc::getMonthNames();
+        return $month_names[$month];
+    }
+
+    // }}}
+    // {{{ getMonthAbbrname()
+
+    /**
+     * Returns the abbreviated month name for the given month
+     *
+     * @param int    $month   the month
+     * @param int    $length  the length of abbreviation
+     *
+     * @return string  the abbreviated name of the month
+     *
+     * @access public
+     * @static
+     * @see Date_Calc::getMonthFullname
+     */
+    function getMonthAbbrname($month, $length = 3)
+    {
+        $month = (int)$month;
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        return substr(Date_Calc::getMonthFullname($month), 0, $length);
+    }
+
+    // }}}
+    // {{{ getMonthFromFullname()
+
+    /**
+     * Returns the numeric month from the month name or an abreviation
+     *
+     * Both August and Aug would return 8.
+     *
+     * @param string $month  the name of the month to examine.
+     *                        Case insensitive.
+     *
+     * @return integer  the month's number
+     *
+     * @access public
+     * @static
+     */
+    function getMonthFromFullName($month)
+    {
+        $month = strtolower($month);
+        $months = Date_Calc::getMonthNames();
+        while(list($id, $name) = each($months)) {
+            if (ereg($month, strtolower($name))) {
+                return $id;
+            }
+        }
+        return 0;
+    }
+
+    // }}}
+    // {{{ getMonthNames()
+
+    /**
+     * Returns an array of month names
+     *
+     * Used to take advantage of the setlocale function to return
+     * language specific month names.
+     *
+     * TODO: cache values to some global array to avoid preformace
+     * hits when called more than once.
+     *
+     * @returns array  an array of month names
+     *
+     * @access public
+     * @static
+     */
+    function getMonthNames()
+    {
+        $months = array();
+        for ($i = 1; $i < 13; $i++) {
+            $months[$i] = strftime('%B', mktime(0, 0, 0, $i, 1, 2001));
+        }
+        return $months;
+    }
+
+    // }}}
+    // {{{ getWeekDays()
+
+    /**
+     * Returns an array of week days
+     *
+     * Used to take advantage of the setlocale function to
+     * return language specific week days.
+     *
+     * TODO: cache values to some global array to avoid preformace
+     * hits when called more than once.
+     *
+     * @returns array  an array of week day names
+     *
+     * @access public
+     * @static
+     */
+    function getWeekDays()
+    {
+        $weekdays = array();
+        for ($i = 0; $i < 7; $i++) {
+            $weekdays[$i] = strftime('%A', mktime(0, 0, 0, 1, $i, 2001));
+        }
+        return $weekdays;
+    }
+
+    // }}}
+    // {{{ dayOfWeek()
+
+    /**
+     * Returns day of week for given date (0 = Sunday)
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return int  the number of the day in the week
+     *
+     * @access public
+     * @static
+     */
+    function dayOfWeek($day = 0, $month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        if ($month > 2) {
+            $month -= 2;
+        } else {
+            $month += 10;
+            $year--;
+        }
+
+        $day = (floor((13 * $month - 1) / 5) +
+                $day + ($year % 100) +
+                floor(($year % 100) / 4) +
+                floor(($year / 100) / 4) - 2 *
+                floor($year / 100) + 77);
+
+        $weekday_number = $day - 7 * floor($day / 7);
+        return $weekday_number;
+    }
+
+    // }}}
+    // {{{ weekOfYear()
+
+    /**
+     * Returns week of the year, first Sunday is first day of first week
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return int  the number of the week in the year
+     *
+     * @access public
+     * @static
+     */
+    function weekOfYear($day = 0, $month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $iso    = Date_Calc::gregorianToISO($day, $month, $year);
+        $parts  = explode('-', $iso);
+        $week_number = intval($parts[1]);
+        return $week_number;
+    }
+
+    // }}}
+    // {{{ quarterOfYear()
+
+    /**
+     * Returns quarter of the year for given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return int  the number of the quarter in the year
+     *
+     * @access public
+     * @static
+     */
+    function quarterOfYear($day = 0, $month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $year_quarter = intval(($month - 1) / 3 + 1);
+        return $year_quarter;
+    }
+
+    // }}}
+    // {{{ daysInMonth()
+
+    /**
+     * Find the number of days in the given month
+     *
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return int  the number of days the month has
+     *
+     * @access public
+     * @static
+     */
+    function daysInMonth($month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+
+        if ($year == 1582 && $month == 10) {
+            return 21;  // October 1582 only had 1st-4th and 15th-31st
+        }
+
+        if ($month == 2) {
+            if (Date_Calc::isLeapYear($year)) {
+                return 29;
+             } else {
+                return 28;
+            }
+        } elseif ($month == 4 or $month == 6 or $month == 9 or $month == 11) {
+            return 30;
+        } else {
+            return 31;
+        }
+    }
+
+    // }}}
+    // {{{ weeksInMonth()
+
+    /**
+     * Returns the number of rows on a calendar month
+     *
+     * Useful for determining the number of rows when displaying a typical
+     * month calendar.
+     *
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return int  the number of weeks the month has
+     *
+     * @access public
+     * @static
+     */
+    function weeksInMonth($month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        $FDOM = Date_Calc::firstOfMonthWeekday($month, $year);
+        if (DATE_CALC_BEGIN_WEEKDAY==1 && $FDOM==0) {
+            $first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY;
+            $weeks = 1;
+        } elseif (DATE_CALC_BEGIN_WEEKDAY==0 && $FDOM == 6) {
+            $first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY;
+            $weeks = 1;
+        } else {
+            $first_week_days = DATE_CALC_BEGIN_WEEKDAY - $FDOM;
+            $weeks = 0;
+        }
+        $first_week_days %= 7;
+        return ceil((Date_Calc::daysInMonth($month, $year)
+                     - $first_week_days) / 7) + $weeks;
+    }
+
+    // }}}
+    // {{{ getCalendarWeek()
+
+    /**
+     * Return an array with days in week
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return array $week[$weekday]
+     *
+     * @access public
+     * @static
+     */
+    function getCalendarWeek($day = 0, $month = 0, $year = 0,
+                             $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+
+        $week_array = array();
+
+        // date for the column of week
+
+        $curr_day = Date_Calc::beginOfWeek($day, $month, $year,'%E');
+
+        for ($counter = 0; $counter <= 6; $counter++) {
+            $week_array[$counter] = Date_Calc::daysToDate($curr_day, $format);
+            $curr_day++;
+        }
+        return $week_array;
+    }
+
+    // }}}
+    // {{{ getCalendarMonth()
+
+    /**
+     * Return a set of arrays to construct a calendar month for the given date
+     *
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return array $month[$row][$col]
+     *
+     * @access public
+     * @static
+     */
+    function getCalendarMonth($month = 0, $year = 0,
+                              $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+
+        $month_array = array();
+
+        // date for the first row, first column of calendar month
+        if (DATE_CALC_BEGIN_WEEKDAY == 1) {
+            if (Date_Calc::firstOfMonthWeekday($month, $year) == 0) {
+                $curr_day = Date_Calc::dateToDays('01', $month, $year) - 6;
+            } else {
+                $curr_day = Date_Calc::dateToDays('01', $month, $year)
+                    - Date_Calc::firstOfMonthWeekday($month, $year) + 1;
+            }
+        } else {
+            $curr_day = (Date_Calc::dateToDays('01', $month, $year)
+                - Date_Calc::firstOfMonthWeekday($month, $year));
+        }
+
+        // number of days in this month
+        $daysInMonth = Date_Calc::daysInMonth($month, $year);
+
+        $weeksInMonth = Date_Calc::weeksInMonth($month, $year);
+        for ($row_counter = 0; $row_counter < $weeksInMonth; $row_counter++) {
+            for ($column_counter = 0; $column_counter <= 6; $column_counter++) {
+                $month_array[$row_counter][$column_counter] =
+                        Date_Calc::daysToDate($curr_day , $format);
+                $curr_day++;
+            }
+        }
+
+        return $month_array;
+    }
+
+    // }}}
+    // {{{ getCalendarYear()
+
+    /**
+     * Return a set of arrays to construct a calendar year for the given date
+     *
+     * @param int    $year    the year in four digit format, default current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return array $year[$month][$row][$col]
+     *
+     * @access public
+     * @static
+     */
+    function getCalendarYear($year = 0, $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+
+        $year_array = array();
+
+        for ($curr_month = 0; $curr_month <= 11; $curr_month++) {
+            $year_array[$curr_month] =
+                    Date_Calc::getCalendarMonth($curr_month + 1,
+                                                $year, $format);
+        }
+
+        return $year_array;
+    }
+
+    // }}}
+    // {{{ prevDay()
+
+    /**
+     * Returns date of day before given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function prevDay($day = 0, $month = 0, $year = 0,
+                     $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $days = Date_Calc::dateToDays($day, $month, $year);
+        return Date_Calc::daysToDate($days - 1, $format);
+    }
+
+    // }}}
+    // {{{ nextDay()
+
+    /**
+     * Returns date of day after given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function nextDay($day = 0, $month = 0, $year = 0,
+                     $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $days = Date_Calc::dateToDays($day, $month, $year);
+        return Date_Calc::daysToDate($days + 1, $format);
+    }
+
+    // }}}
+    // {{{ prevWeekday()
+
+    /**
+     * Returns date of the previous weekday, skipping from Monday to Friday
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function prevWeekday($day = 0, $month = 0, $year = 0,
+                         $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $days = Date_Calc::dateToDays($day, $month, $year);
+        if (Date_Calc::dayOfWeek($day, $month, $year) == 1) {
+            $days -= 3;
+        } elseif (Date_Calc::dayOfWeek($day, $month, $year) == 0) {
+            $days -= 2;
+        } else {
+            $days -= 1;
+        }
+        return Date_Calc::daysToDate($days, $format);
+    }
+
+    // }}}
+    // {{{ nextWeekday()
+
+    /**
+     * Returns date of the next weekday of given date, skipping from
+     * Friday to Monday
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function nextWeekday($day = 0, $month = 0, $year = 0,
+                         $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $days = Date_Calc::dateToDays($day, $month, $year);
+        if (Date_Calc::dayOfWeek($day, $month, $year) == 5) {
+            $days += 3;
+        } elseif (Date_Calc::dayOfWeek($day, $month, $year) == 6) {
+            $days += 2;
+        } else {
+            $days += 1;
+        }
+        return Date_Calc::daysToDate($days, $format);
+    }
+
+    // }}}
+    // {{{ prevDayOfWeek()
+
+    /**
+     * Returns date of the previous specific day of the week
+     * from the given date
+     *
+     * @param int day of week, 0=Sunday
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param bool   $onOrBefore  if true and days are same, returns current day
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function prevDayOfWeek($dow, $day = 0, $month = 0, $year = 0,
+                           $format = DATE_CALC_FORMAT, $onOrBefore = false)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $days = Date_Calc::dateToDays($day, $month, $year);
+        $curr_weekday = Date_Calc::dayOfWeek($day, $month, $year);
+        if ($curr_weekday == $dow) {
+            if (!$onOrBefore) {
+                $days -= 7;
+            }
+        } elseif ($curr_weekday < $dow) {
+            $days -= 7 - ($dow - $curr_weekday);
+        } else {
+            $days -= $curr_weekday - $dow;
+        }
+        return Date_Calc::daysToDate($days, $format);
+    }
+
+    // }}}
+    // {{{ nextDayOfWeek()
+
+    /**
+     * Returns date of the next specific day of the week
+     * from the given date
+     *
+     * @param int    $dow     the day of the week (0 = Sunday)
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param bool   $onOrAfter  if true and days are same, returns current day
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function nextDayOfWeek($dow, $day = 0, $month = 0, $year = 0,
+                           $format = DATE_CALC_FORMAT, $onOrAfter = false)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+
+        $days = Date_Calc::dateToDays($day, $month, $year);
+        $curr_weekday = Date_Calc::dayOfWeek($day, $month, $year);
+
+        if ($curr_weekday == $dow) {
+            if (!$onOrAfter) {
+                $days += 7;
+            }
+        } elseif ($curr_weekday > $dow) {
+            $days += 7 - ($curr_weekday - $dow);
+        } else {
+            $days += $dow - $curr_weekday;
+        }
+
+        return Date_Calc::daysToDate($days, $format);
+    }
+
+    // }}}
+    // {{{ prevDayOfWeekOnOrBefore()
+
+    /**
+     * Returns date of the previous specific day of the week
+     * on or before the given date
+     *
+     * @param int    $dow     the day of the week (0 = Sunday)
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function prevDayOfWeekOnOrBefore($dow, $day = 0, $month = 0, $year = 0,
+                                     $format = DATE_CALC_FORMAT)
+    {
+        return Date_Calc::prevDayOfWeek($dow, $day, $month, $year, $format,
+                                        true);
+    }
+
+    // }}}
+    // {{{ nextDayOfWeekOnOrAfter()
+
+    /**
+     * Returns date of the next specific day of the week
+     * on or after the given date
+     *
+     * @param int    $dow     the day of the week (0 = Sunday)
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function nextDayOfWeekOnOrAfter($dow, $day = 0, $month = 0, $year = 0,
+                                    $format = DATE_CALC_FORMAT)
+    {
+        return Date_Calc::nextDayOfWeek($dow, $day, $month, $year, $format,
+                                        true);
+    }
+
+    // }}}
+    // {{{ beginOfWeek()
+
+    /**
+     * Find the month day of the beginning of week for given date,
+     * using DATE_CALC_BEGIN_WEEKDAY
+     *
+     * Can return weekday of prev month.
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function beginOfWeek($day = 0, $month = 0, $year = 0,
+                         $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $this_weekday = Date_Calc::dayOfWeek($day, $month, $year);
+        $interval = (7 - DATE_CALC_BEGIN_WEEKDAY + $this_weekday) % 7;
+        return Date_Calc::daysToDate(Date_Calc::dateToDays($day, $month, $year)
+                                     - $interval, $format);
+    }
+
+    // }}}
+    // {{{ endOfWeek()
+
+    /**
+     * Find the month day of the end of week for given date,
+     * using DATE_CALC_BEGIN_WEEKDAY
+     *
+     * Can return weekday of following month.
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function endOfWeek($day = 0, $month = 0, $year = 0,
+                       $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        $this_weekday = Date_Calc::dayOfWeek($day, $month, $year);
+        $interval = (6 + DATE_CALC_BEGIN_WEEKDAY - $this_weekday) % 7;
+        return Date_Calc::daysToDate(Date_Calc::dateToDays($day, $month, $year)
+                                     + $interval, $format);
+    }
+
+    // }}}
+    // {{{ beginOfPrevWeek()
+
+    /**
+     * Find the month day of the beginning of week before given date,
+     * using DATE_CALC_BEGIN_WEEKDAY
+     *
+     * Can return weekday of prev month.
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function beginOfPrevWeek($day = 0, $month = 0, $year = 0,
+                             $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+
+        $date = Date_Calc::daysToDate(Date_Calc::dateToDays($day-7,
+                                                            $month,
+                                                            $year),
+                                      '%Y%m%d');
+
+        $prev_week_year  = substr($date, 0, 4);
+        $prev_week_month = substr($date, 4, 2);
+        $prev_week_day   = substr($date, 6, 2);
+
+        return Date_Calc::beginOfWeek($prev_week_day, $prev_week_month,
+                                      $prev_week_year, $format);
+    }
+
+    // }}}
+    // {{{ beginOfNextWeek()
+
+    /**
+     * Find the month day of the beginning of week after given date,
+     * using DATE_CALC_BEGIN_WEEKDAY
+     *
+     * Can return weekday of prev month.
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function beginOfNextWeek($day = 0, $month = 0, $year = 0,
+                             $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+
+        $date = Date_Calc::daysToDate(Date_Calc::dateToDays($day + 7,
+                                                            $month,
+                                                            $year),
+                                      '%Y%m%d');
+
+        $next_week_year  = substr($date, 0, 4);
+        $next_week_month = substr($date, 4, 2);
+        $next_week_day   = substr($date, 6, 2);
+
+        return Date_Calc::beginOfWeek($next_week_day, $next_week_month,
+                                      $next_week_year, $format);
+    }
+
+    // }}}
+    // {{{ beginOfMonth()
+
+    /**
+     * Return date of first day of month of given date
+     *
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     * @see Date_Calc::beginOfMonthBySpan()
+     * @deprecated Method deprecated in Release 1.4.4
+     */
+    function beginOfMonth($month = 0, $year = 0, $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        return Date_Calc::dateFormat('01', $month, $year, $format);
+    }
+
+    // }}}
+    // {{{ beginOfPrevMonth()
+
+    /**
+     * Returns date of the first day of previous month of given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     * @see Date_Calc::beginOfMonthBySpan()
+     * @deprecated Method deprecated in Release 1.4.4
+     */
+    function beginOfPrevMonth($day = 0, $month = 0, $year = 0,
+                              $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        if ($month > 1) {
+            $month--;
+            $day = 1;
+        } else {
+            $year--;
+            $month = 12;
+            $day   = 1;
+        }
+        return Date_Calc::dateFormat($day, $month, $year, $format);
+    }
+
+    // }}}
+    // {{{ endOfPrevMonth()
+
+    /**
+     * Returns date of the last day of previous month for given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     * @see Date_Calc::endOfMonthBySpan()
+     * @deprecated Method deprecated in Release 1.4.4
+     */
+    function endOfPrevMonth($day = 0, $month = 0, $year = 0,
+                            $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        if ($month > 1) {
+            $month--;
+        } else {
+            $year--;
+            $month = 12;
+        }
+        $day = Date_Calc::daysInMonth($month, $year);
+        return Date_Calc::dateFormat($day, $month, $year, $format);
+    }
+
+    // }}}
+    // {{{ beginOfNextMonth()
+
+    /**
+     * Returns date of begin of next month of given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     * @see Date_Calc::beginOfMonthBySpan()
+     * @deprecated Method deprecated in Release 1.4.4
+     */
+    function beginOfNextMonth($day = 0, $month = 0, $year = 0,
+                              $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        if ($month < 12) {
+            $month++;
+            $day = 1;
+        } else {
+            $year++;
+            $month = 1;
+            $day = 1;
+        }
+        return Date_Calc::dateFormat($day, $month, $year, $format);
+    }
+
+    // }}}
+    // {{{ endOfNextMonth()
+
+    /**
+     * Returns date of the last day of next month of given date
+     *
+     * @param int    $day     the day of the month, default is current local day
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     * @see Date_Calc::endOfMonthBySpan()
+     * @deprecated Method deprecated in Release 1.4.4
+     */
+    function endOfNextMonth($day = 0, $month = 0, $year = 0,
+                            $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if (empty($day)) {
+            $day = Date_Calc::dateNow('%d');
+        }
+        if ($month < 12) {
+            $month++;
+        } else {
+            $year++;
+            $month = 1;
+        }
+        $day = Date_Calc::daysInMonth($month, $year);
+        return Date_Calc::dateFormat($day, $month, $year, $format);
+    }
+
+    // }}}
+    // {{{ beginOfMonthBySpan()
+
+    /**
+     * Returns date of the first day of the month in the number of months
+     * from the given date
+     *
+     * @param int    $months  the number of months from the date provided.
+     *                         Positive numbers go into the future.
+     *                         Negative numbers go into the past.
+     *                         0 is the month presented in $month.
+     * @param string $month   the month, default is current local month
+     * @param string $year    the year in four digit format, default is the
+     *                         current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     * @since  Method available since Release 1.4.4
+     */
+    function beginOfMonthBySpan($months = 0, $month = 0, $year = 0,
+                                $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if ($months > 0) {
+            // future month
+            $tmp_mo = $month + $months;
+            $month  = $tmp_mo % 12;
+            if ($month == 0) {
+                $month = 12;
+                $year = $year + floor(($tmp_mo - 1) / 12);
+            } else {
+                $year = $year + floor($tmp_mo / 12);
+            }
+        } else {
+            // past or present month
+            $tmp_mo = $month + $months;
+            if ($tmp_mo > 0) {
+                // same year
+                $month = $tmp_mo;
+            } elseif ($tmp_mo == 0) {
+                // prior dec
+                $month = 12;
+                $year--;
+            } else {
+                // some time in a prior year
+                $month = 12 + ($tmp_mo % 12);
+                $year  = $year + floor($tmp_mo / 12);
+            }
+        }
+        return Date_Calc::dateFormat(1, $month, $year, $format);
+    }
+
+    // }}}
+    // {{{ endOfMonthBySpan()
+
+    /**
+     * Returns date of the last day of the month in the number of months
+     * from the given date
+     *
+     * @param int    $months  the number of months from the date provided.
+     *                         Positive numbers go into the future.
+     *                         Negative numbers go into the past.
+     *                         0 is the month presented in $month.
+     * @param string $month   the month, default is current local month
+     * @param string $year    the year in four digit format, default is the
+     *                         current local year
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     * @since  Method available since Release 1.4.4
+     */
+    function endOfMonthBySpan($months = 0, $month = 0, $year = 0,
+                              $format = DATE_CALC_FORMAT)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        if ($months > 0) {
+            // future month
+            $tmp_mo = $month + $months;
+            $month  = $tmp_mo % 12;
+            if ($month == 0) {
+                $month = 12;
+                $year = $year + floor(($tmp_mo - 1) / 12);
+            } else {
+                $year = $year + floor($tmp_mo / 12);
+            }
+        } else {
+            // past or present month
+            $tmp_mo = $month + $months;
+            if ($tmp_mo > 0) {
+                // same year
+                $month = $tmp_mo;
+            } elseif ($tmp_mo == 0) {
+                // prior dec
+                $month = 12;
+                $year--;
+            } else {
+                // some time in a prior year
+                $month = 12 + ($tmp_mo % 12);
+                $year  = $year + floor($tmp_mo / 12);
+            }
+        }
+        return Date_Calc::dateFormat(Date_Calc::daysInMonth($month, $year),
+                                     $month, $year, $format);
+    }
+
+    // }}}
+    // {{{ firstOfMonthWeekday()
+
+    /**
+     * Find the day of the week for the first of the month of given date
+     *
+     * @param int    $month   the month, default is current local month
+     * @param int    $year    the year in four digit format, default is current local year
+     *
+     * @return int number of weekday for the first day, 0=Sunday
+     *
+     * @access public
+     * @static
+     */
+    function firstOfMonthWeekday($month = 0, $year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (empty($month)) {
+            $month = Date_Calc::dateNow('%m');
+        }
+        return Date_Calc::dayOfWeek('01', $month, $year);
+    }
+
+    // }}}
+    // {{{ NWeekdayOfMonth()
+
+    /**
+     * Calculates the date of the Nth weekday of the month,
+     * such as the second Saturday of January 2000
+     *
+     * @param int    $week    the number of the week to get
+     *                         (1 = first, etc.  Also can be 'last'.)
+     * @param int    $dow     the day of the week (0 = Sunday)
+     * @param int    $month   the month
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     * @param string $format  the string indicating how to format the output
+     *
+     * @return string  the date in the desired format
+     *
+     * @access public
+     * @static
+     */
+    function NWeekdayOfMonth($week, $dow, $month, $year,
+                             $format = DATE_CALC_FORMAT)
+    {
+        if (is_numeric($week)) {
+            $DOW1day = ($week - 1) * 7 + 1;
+            $DOW1    = Date_Calc::dayOfWeek($DOW1day, $month, $year);
+            $wdate   = ($week - 1) * 7 + 1 + (7 + $dow - $DOW1) % 7;
+            if ($wdate > Date_Calc::daysInMonth($month, $year)) {
+                return -1;
+            } else {
+                return Date_Calc::dateFormat($wdate, $month, $year, $format);
+            }
+        } elseif ($week == 'last' && $dow < 7) {
+            $lastday = Date_Calc::daysInMonth($month, $year);
+            $lastdow = Date_Calc::dayOfWeek($lastday, $month, $year);
+            $diff    = $dow - $lastdow;
+            if ($diff > 0) {
+                return Date_Calc::dateFormat($lastday - (7 - $diff), $month,
+                                             $year, $format);
+            } else {
+                return Date_Calc::dateFormat($lastday + $diff, $month,
+                                             $year, $format);
+            }
+        } else {
+            return -1;
+        }
+    }
+
+    // }}}
+    // {{{ isValidDate()
+
+    /**
+     * Returns true for valid date, false for invalid date
+     *
+     * @param int    $day     the day of the month
+     * @param int    $month   the month
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return boolean
+     *
+     * @access public
+     * @static
+     */
+    function isValidDate($day, $month, $year)
+    {
+        if ($year < 0 || $year > 9999) {
+            return false;
+        }
+        if (!checkdate($month, $day, $year)) {
+            return false;
+        }
+        return true;
+    }
+
+    // }}}
+    // {{{ isLeapYear()
+
+    /**
+     * Returns true for a leap year, else false
+     *
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return boolean
+     *
+     * @access public
+     * @static
+     */
+    function isLeapYear($year = 0)
+    {
+        if (empty($year)) {
+            $year = Date_Calc::dateNow('%Y');
+        }
+        if (preg_match('/\D/', $year)) {
+            return false;
+        }
+        if ($year < 1000) {
+            return false;
+        }
+        if ($year < 1582) {
+            // pre Gregorio XIII - 1582
+            return ($year % 4 == 0);
+        } else {
+            // post Gregorio XIII - 1582
+            return (($year % 4 == 0) && ($year % 100 != 0)) || ($year % 400 == 0);
+        }
+    }
+
+    // }}}
+    // {{{ isFutureDate()
+
+    /**
+     * Determines if given date is a future date from now
+     *
+     * @param int    $day     the day of the month
+     * @param int    $month   the month
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return boolean
+     *
+     * @access public
+     * @static
+     */
+    function isFutureDate($day, $month, $year)
+    {
+        $this_year  = Date_Calc::dateNow('%Y');
+        $this_month = Date_Calc::dateNow('%m');
+        $this_day   = Date_Calc::dateNow('%d');
+
+        if ($year > $this_year) {
+            return true;
+        } elseif ($year == $this_year) {
+            if ($month > $this_month) {
+                return true;
+            } elseif ($month == $this_month) {
+                if ($day > $this_day) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    // }}}
+    // {{{ isPastDate()
+
+    /**
+     * Determines if given date is a past date from now
+     *
+     * @param int    $day     the day of the month
+     * @param int    $month   the month
+     * @param int    $year    the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return boolean
+     *
+     * @access public
+     * @static
+     */
+    function isPastDate($day, $month, $year)
+    {
+        $this_year  = Date_Calc::dateNow('%Y');
+        $this_month = Date_Calc::dateNow('%m');
+        $this_day   = Date_Calc::dateNow('%d');
+
+        if ($year < $this_year) {
+            return true;
+        } elseif ($year == $this_year) {
+            if ($month < $this_month) {
+                return true;
+            } elseif ($month == $this_month) {
+                if ($day < $this_day) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    // }}}
+    // {{{ dateDiff()
+
+    /**
+     * Returns number of days between two given dates
+     *
+     * @param int    $day1    the day of the month
+     * @param int    $month1  the month
+     * @param int    $year1   the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     * @param int    $day2    the day of the month
+     * @param int    $month2  the month
+     * @param int    $year2   the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return int  the absolute number of days between the two dates.
+     *               If an error occurs, -1 is returned.
+     *
+     * @access public
+     * @static
+     */
+    function dateDiff($day1, $month1, $year1, $day2, $month2, $year2)
+    {
+        if (!Date_Calc::isValidDate($day1, $month1, $year1)) {
+            return -1;
+        }
+        if (!Date_Calc::isValidDate($day2, $month2, $year2)) {
+            return -1;
+        }
+        return abs(Date_Calc::dateToDays($day1, $month1, $year1)
+                   - Date_Calc::dateToDays($day2, $month2, $year2));
+    }
+
+    // }}}
+    // {{{ compareDates()
+
+    /**
+     * Compares two dates
+     *
+     * @param int    $day1    the day of the month
+     * @param int    $month1  the month
+     * @param int    $year1   the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     * @param int    $day2    the day of the month
+     * @param int    $month2  the month
+     * @param int    $year2   the year.  Use the complete year instead of the
+     *                         abbreviated version.  E.g. use 2005, not 05.
+     *                         Do not add leading 0's for years prior to 1000.
+     *
+     * @return int  0 if the dates are equal. 1 if date 1 is later, -1 if
+     *               date 1 is earlier.
+     *
+     * @access public
+     * @static
+     */
+    function compareDates($day1, $month1, $year1, $day2, $month2, $year2)
+    {
+        $ndays1 = Date_Calc::dateToDays($day1, $month1, $year1);
+        $ndays2 = Date_Calc::dateToDays($day2, $month2, $year2);
+        if ($ndays1 == $ndays2) {
+            return 0;
+        }
+        return ($ndays1 > $ndays2) ? 1 : -1;
+    }
+
+    // }}}
+}
+
+// }}}
+
+/*
+ * Local variables:
+ * mode: php
+ * tab-width: 4
+ * c-basic-offset: 4
+ * c-hanging-comment-ender-p: nil
+ * End:
+ */
+?>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Human.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Human.php	(revision 2079)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/pear/Date/Human.php	(revision 2079)
@@ -0,0 +1,242 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
+
+// {{{ Header
+
+/**
+ * Class to convert date strings between Gregorian and Human calendar formats
+ *
+ * The Human Calendar format has been proposed by Scott Flansburg and can be
+ * explained as follows:
+ *  The year is made up of 13 months
+ *  Each month has 28 days
+ *  Counting of months starts from 0 (zero) so the months will run from 0 to 12
+ *  New Years day (00) is a monthless day
+ *  Note: Leap Years are not yet accounted for in the Human Calendar system
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 1997-2006 Allan Kent
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted under the terms of the BSD License.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE
+ * COPYRIGHT OWNER OR 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.
+ *
+ * @category   Date and Time
+ * @package    Date
+ * @author     Allan Kent <allan@lodestone.co.za>
+ * @copyright  1997-2006 Allan Kent
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    CVS: $Id: Human.php,v 1.6 2006/11/21 17:38:15 firman Exp $
+ * @link       http://pear.php.net/package/Date
+ * @since      File available since Release 1.3
+ */
+
+// }}}
+// {{{ Class: Date_Human
+
+/**
+ * Class to convert date strings between Gregorian and Human calendar formats
+ *
+ * The Human Calendar format has been proposed by Scott Flansburg and can be
+ * explained as follows:
+ *  The year is made up of 13 months
+ *  Each month has 28 days
+ *  Counting of months starts from 0 (zero) so the months will run from 0 to 12
+ *  New Years day (00) is a monthless day
+ *  Note: Leap Years are not yet accounted for in the Human Calendar system
+ *
+ * @author     Allan Kent <allan@lodestone.co.za>
+ * @copyright  1997-2005 Allan Kent
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ *             BSD License
+ * @version    Release: 1.4.7
+ * @link       http://pear.php.net/package/Date
+ * @since      Class available since Release 1.3
+ */
+class Date_Human
+{
+    // {{{ gregorianToHuman()
+
+    /**
+     * Returns an associative array containing the converted date information
+     * in 'Human Calendar' format.
+     *
+     * @param int day in DD format, default current local day
+     * @param int month in MM format, default current local month
+     * @param int year in CCYY format, default to current local year
+     *
+     * @access public
+     *
+     * @return associative array(
+     *               hdom,       // Human Day Of Month, starting at 1
+     *               hdow,       // Human Day Of Week, starting at 1
+     *               hwom,       // Human Week of Month, starting at 1
+     *               hwoy,       // Human Week of Year, starting at 1
+     *               hmoy,       // Human Month of Year, starting at 0
+     *               )
+     *
+     * If the day is New Years Day, the function will return
+     * "hdom" =>  0
+     * "hdow" =>  0
+     * "hwom" =>  0
+     * "hwoy" =>  0
+     * "hmoy" => -1
+     *  Since 0 is a valid month number under the Human Calendar, I have left
+     *  the month as -1 for New Years Day.
+     */
+    function gregorianToHuman($day=0, $month=0, $year=0)
+    {
+        /*
+         * Check to see if any of the arguments are empty
+         * If they are then populate the $dateinfo array
+         * Then check to see which arguments are empty and fill
+         * those with the current date info
+         */
+        if ((empty($day) || (empty($month)) || empty($year))) {
+            $dateinfo = getdate(time());
+        }
+        if (empty($day)) {
+            $day = $dateinfo["mday"];
+        }
+        if (empty($month)) {
+            $month = $dateinfo["mon"];
+        }
+        if (empty($year)) {
+            $year = $dateinfo["year"];
+        }
+        /*
+         * We need to know how many days into the year we are
+         */
+        $dateinfo = getdate(mktime(0, 0, 0, $month, $day, $year));
+        $dayofyear = $dateinfo["yday"];
+        /*
+         * Human Calendar starts at 0 for months and the first day of the year
+         * is designated 00, so we need to start our day of the year at 0 for
+         * these calculations.
+         * Also, the day of the month is calculated with a modulus of 28.
+         * Because a day is 28 days, the last day of the month would have a
+         * remainder of 0 and not 28 as it should be.  Decrementing $dayofyear
+         * gets around this.
+         */
+        $dayofyear--;
+        /*
+         * 28 days in a month...
+         */
+        $humanMonthOfYear = floor($dayofyear / 28);
+        /*
+         * If we are in the first month then the day of the month is $dayofyear
+         * else we need to find the modulus of 28.
+         */
+        if ($humanMonthOfYear == 0) {
+            $humanDayOfMonth = $dayofyear;
+        } else {
+            $humanDayOfMonth = ($dayofyear) % 28;
+        }
+        /*
+         * Day of the week is modulus 7
+         */
+        $humanDayOfWeek = $dayofyear % 7;
+        /*
+         * We can now increment $dayofyear back to it's correct value for
+         * the remainder of the calculations
+         */
+        $dayofyear++;
+        /*
+         * $humanDayOfMonth needs to be incremented now - recall that we fudged
+         * it a bit by decrementing $dayofyear earlier
+         * Same goes for $humanDayOfWeek
+         */
+        $humanDayOfMonth++;
+        $humanDayOfWeek++;
+        /*
+         * Week of the month is day of the month divided by 7, rounded up
+         * Same for week of the year, but use $dayofyear instead $humanDayOfMonth
+         */
+        $humanWeekOfMonth = ceil($humanDayOfMonth / 7);
+        $humanWeekOfYear = ceil($dayofyear / 7);
+        /*
+         * Return an associative array of the values
+         */
+        return array(
+                     "hdom" => $humanDayOfMonth,
+                     "hdow" => $humanDayOfWeek,
+                     "hwom" => $humanWeekOfMonth,
+                     "hwoy" => $humanWeekOfYear,
+                     "hmoy" => $humanMonthOfYear );
+    }
+
+    // }}}
+    // {{{ humanToGregorian()
+
+    /**
+     * Returns unix timestamp for a given Human Calendar date
+     *
+     * @param int day in DD format
+     * @param int month in MM format
+     * @param int year in CCYY format, default to current local year
+     *
+     * @access public
+     *
+     * @return int unix timestamp of date
+     */
+    function humanToGregorian($day, $month, $year=0)
+    {
+        /*
+         * Check to see if the year has been passed through.
+         * If not get current year
+         */
+        if (empty($year)) {
+            $dateinfo = getdate(time());
+            $year = $dateinfo["year"];
+        }
+        /*
+         * We need to get the day of the year that we are currently at so that
+         * we can work out the Gregorian Month and day
+         */
+        $DayOfYear = $month * 28;
+        $DayOfYear += $day;
+        /*
+         * Human Calendar starts at 0, so we need to increment $DayOfYear
+         * to take into account the day 00
+         */
+        $DayOfYear++;
+        /*
+         * the mktime() function will correctly calculate the date for out of
+         * range values, so putting $DayOfYear instead of the day of the month
+         * will work fine.
+         */
+        $GregorianTimeStamp = mktime(0, 0, 0, 1, $DayOfYear, $year);
+        return $GregorianTimeStamp;
+    }
+
+    // }}}
+}
+
+// }}}
+
+/*
+ * Local variables:
+ * mode: php
+ * tab-width: 4
+ * c-basic-offset: 4
+ * c-hanging-comment-ender-p: nil
+ * End:
+ */
+?>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/HTTP/Encoder.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/HTTP/Encoder.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/HTTP/Encoder.php	(revision 1957)
@@ -0,0 +1,326 @@
+<?php
+/**
+ * Class HTTP_Encoder  
+ * @package Minify
+ * @subpackage HTTP
+ */
+ 
+/**
+ * Encode and send gzipped/deflated content
+ *
+ * The "Vary: Accept-Encoding" header is sent. If the client allows encoding, 
+ * Content-Encoding and Content-Length are added.
+ *
+ * <code>
+ * // Send a CSS file, compressed if possible
+ * $he = new HTTP_Encoder(array(
+ *     'content' => file_get_contents($cssFile)
+ *     ,'type' => 'text/css'
+ * ));
+ * $he->encode();
+ * $he->sendAll();
+ * </code>
+ *
+ * <code>
+ * // Shortcut to encoding output
+ * header('Content-Type: text/css'); // needed if not HTML
+ * HTTP_Encoder::output($css);
+ * </code>
+ * 
+ * <code>
+ * // Just sniff for the accepted encoding
+ * $encoding = HTTP_Encoder::getAcceptedEncoding();
+ * </code>
+ *
+ * For more control over headers, use getHeaders() and getData() and send your
+ * own output.
+ * 
+ * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, 
+ * and gzcompress functions for gzip, deflate, and compress-encoding
+ * respectively.
+ * 
+ * @package Minify
+ * @subpackage HTTP
+ * @author Stephen Clay <steve@mrclay.org>
+ */
+class HTTP_Encoder {
+
+    /**
+     * Should the encoder allow HTTP encoding to IE6? 
+     * 
+     * If you have many IE6 users and the bandwidth savings is worth troubling 
+     * some of them, set this to true.
+     * 
+     * By default, encoding is only offered to IE7+. When this is true,
+     * getAcceptedEncoding() will return an encoding for IE6 if its user agent
+     * string contains "SV1". This has been documented in many places as "safe",
+     * but there seem to be remaining, intermittent encoding bugs in patched 
+     * IE6 on the wild web.
+     * 
+     * @var bool
+     */
+    public static $encodeToIe6 = false;
+    
+    
+    /**
+     * Default compression level for zlib operations
+     * 
+     * This level is used if encode() is not given a $compressionLevel
+     * 
+     * @var int
+     */
+    public static $compressionLevel = 6;
+    
+
+    /**
+     * Get an HTTP Encoder object
+     * 
+     * @param array $spec options
+     * 
+     * 'content': (string required) content to be encoded
+     * 
+     * 'type': (string) if set, the Content-Type header will have this value.
+     * 
+     * 'method: (string) only set this if you are forcing a particular encoding
+     * method. If not set, the best method will be chosen by getAcceptedEncoding()
+     * The available methods are 'gzip', 'deflate', 'compress', and '' (no
+     * encoding)
+     * 
+     * @return null
+     */
+    public function __construct($spec) 
+    {
+        $this->_content = $spec['content'];
+        $this->_headers['Content-Length'] = (string)strlen($this->_content);
+        if (isset($spec['type'])) {
+            $this->_headers['Content-Type'] = $spec['type'];
+        }
+        if (isset($spec['method'])
+            && in_array($spec['method'], array('gzip', 'deflate', 'compress', '')))
+        {
+            $this->_encodeMethod = array($spec['method'], $spec['method']);
+        } else {
+            $this->_encodeMethod = self::getAcceptedEncoding();
+        }
+    }
+
+    /**
+     * Get content in current form
+     * 
+     * Call after encode() for encoded content.
+     * 
+     * return string
+     */
+    public function getContent() 
+    {
+        return $this->_content;
+    }
+    
+    /**
+     * Get array of output headers to be sent
+     * 
+     * E.g.
+     * <code>
+     * array(
+     *     'Content-Length' => '615'
+     *     ,'Content-Encoding' => 'x-gzip'
+     *     ,'Vary' => 'Accept-Encoding'
+     * )
+     * </code>
+     *
+     * @return array 
+     */
+    public function getHeaders()
+    {
+        return $this->_headers;
+    }
+
+    /**
+     * Send output headers
+     * 
+     * You must call this before headers are sent and it probably cannot be
+     * used in conjunction with zlib output buffering / mod_gzip. Errors are
+     * not handled purposefully.
+     * 
+     * @see getHeaders()
+     * 
+     * @return null
+     */
+    public function sendHeaders()
+    {
+        foreach ($this->_headers as $name => $val) {
+            header($name . ': ' . $val);
+        }
+    }
+    
+    /**
+     * Send output headers and content
+     * 
+     * A shortcut for sendHeaders() and echo getContent()
+     *
+     * You must call this before headers are sent and it probably cannot be
+     * used in conjunction with zlib output buffering / mod_gzip. Errors are
+     * not handled purposefully.
+     * 
+     * @return null
+     */
+    public function sendAll()
+    {
+        $this->sendHeaders();
+        echo $this->_content;
+    }
+
+    /**
+     * Determine the client's best encoding method from the HTTP Accept-Encoding 
+     * header.
+     * 
+     * If no Accept-Encoding header is set, or the browser is IE before v6 SP2,
+     * this will return ('', ''), the "identity" encoding.
+     * 
+     * A syntax-aware scan is done of the Accept-Encoding, so the method must
+     * be non 0. The methods are favored in order of gzip, deflate, then 
+     * compress. Deflate is always smallest and generally faster, but is 
+     * rarely sent by servers, so client support could be buggier.
+     * 
+     * @param bool $allowCompress allow the older compress encoding
+     * 
+     * @param bool $allowDeflate allow the more recent deflate encoding
+     * 
+     * @return array two values, 1st is the actual encoding method, 2nd is the
+     * alias of that method to use in the Content-Encoding header (some browsers
+     * call gzip "x-gzip" etc.)
+     */
+    public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true)
+    {
+        // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+        
+        if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
+            || self::_isBuggyIe())
+        {
+            return array('', '');
+        }
+        $ae = $_SERVER['HTTP_ACCEPT_ENCODING'];
+        // gzip checks (quick)
+        if (0 === strpos($ae, 'gzip,')             // most browsers
+            || 0 === strpos($ae, 'deflate, gzip,') // opera
+        ) {
+            return array('gzip', 'gzip');
+        }
+        // gzip checks (slow)
+        if (preg_match(
+                '@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
+                ,$ae
+                ,$m)) {
+            return array('gzip', $m[1]);
+        }
+        if ($allowDeflate) {
+            // deflate checks    
+            $aeRev = strrev($ae);
+            if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit
+                || 0 === strpos($aeRev, 'etalfed,') // gecko
+                || 0 === strpos($ae, 'deflate,') // opera
+                // slow parsing
+                || preg_match(
+                    '@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) {
+                return array('deflate', 'deflate');
+            }
+        }
+        if ($allowCompress && preg_match(
+                '@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
+                ,$ae
+                ,$m)) {
+            return array('compress', $m[1]);
+        }
+        return array('', '');
+    }
+
+    /**
+     * Encode (compress) the content
+     * 
+     * If the encode method is '' (none) or compression level is 0, or the 'zlib'
+     * extension isn't loaded, we return false.
+     * 
+     * Then the appropriate gz_* function is called to compress the content. If
+     * this fails, false is returned.
+     * 
+     * The header "Vary: Accept-Encoding" is added. If encoding is successful, 
+     * the Content-Length header is updated, and Content-Encoding is also added.
+     * 
+     * @param int $compressionLevel given to zlib functions. If not given, the
+     * class default will be used.
+     * 
+     * @return bool success true if the content was actually compressed
+     */
+    public function encode($compressionLevel = null)
+    {
+        $this->_headers['Vary'] = 'Accept-Encoding';
+        if (null === $compressionLevel) {
+            $compressionLevel = self::$compressionLevel;
+        }
+        if ('' === $this->_encodeMethod[0]
+            || ($compressionLevel == 0)
+            || !extension_loaded('zlib'))
+        {
+            return false;
+        }
+        if ($this->_encodeMethod[0] === 'deflate') {
+            $encoded = gzdeflate($this->_content, $compressionLevel);
+        } elseif ($this->_encodeMethod[0] === 'gzip') {
+            $encoded = gzencode($this->_content, $compressionLevel);
+        } else {
+            $encoded = gzcompress($this->_content, $compressionLevel);
+        }
+        if (false === $encoded) {
+            return false;
+        }
+        $this->_headers['Content-Length'] = strlen($encoded);
+        $this->_headers['Content-Encoding'] = $this->_encodeMethod[1];
+        $this->_content = $encoded;
+        return true;
+    }
+    
+    /**
+     * Encode and send appropriate headers and content
+     *
+     * This is a convenience method for common use of the class
+     * 
+     * @param string $content
+     * 
+     * @param int $compressionLevel given to zlib functions. If not given, the
+     * class default will be used.
+     * 
+     * @return bool success true if the content was actually compressed
+     */
+    public static function output($content, $compressionLevel = null)
+    {
+        if (null === $compressionLevel) {
+            $compressionLevel = self::$compressionLevel;
+        }
+        $he = new HTTP_Encoder(array('content' => $content));
+        $ret = $he->encode($compressionLevel);
+        $he->sendAll();
+        return $ret;
+    }
+    
+    protected $_content = '';
+    protected $_headers = array();
+    protected $_encodeMethod = array('', '');
+
+    /**
+     * Is the browser an IE version earlier than 6 SP2?  
+     */
+    protected static function _isBuggyIe()
+    {
+        $ua = $_SERVER['HTTP_USER_AGENT'];
+        // quick escape for non-IEs
+        if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ')
+            || false !== strpos($ua, 'Opera')) {
+            return false;
+        }
+        // no regex = faaast
+        $version = (float)substr($ua, 30); 
+        return self::$encodeToIe6
+            ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1')))
+            : ($version < 7);
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/HTTP/ConditionalGet.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/HTTP/ConditionalGet.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/HTTP/ConditionalGet.php	(revision 1957)
@@ -0,0 +1,348 @@
+<?php
+/**
+ * Class HTTP_ConditionalGet  
+ * @package Minify
+ * @subpackage HTTP
+ */
+
+/**
+ * Implement conditional GET via a timestamp or hash of content
+ *
+ * E.g. Content from DB with update time:
+ * <code>
+ * list($updateTime, $content) = getDbUpdateAndContent();
+ * $cg = new HTTP_ConditionalGet(array(
+ *     'lastModifiedTime' => $updateTime
+ *     ,'isPublic' => true
+ * ));
+ * $cg->sendHeaders();
+ * if ($cg->cacheIsValid) {
+ *     exit();
+ * }
+ * echo $content;
+ * </code>
+ * 
+ * E.g. Shortcut for the above
+ * <code>
+ * HTTP_ConditionalGet::check($updateTime, true); // exits if client has cache
+ * echo $content;
+ * </code>
+ *
+ * E.g. Content from DB with no update time:
+ * <code>
+ * $content = getContentFromDB();
+ * $cg = new HTTP_ConditionalGet(array(
+ *     'contentHash' => md5($content)
+ * ));
+ * $cg->sendHeaders();
+ * if ($cg->cacheIsValid) {
+ *     exit();
+ * }
+ * echo $content;
+ * </code>
+ * 
+ * E.g. Static content with some static includes:
+ * <code>
+ * // before content
+ * $cg = new HTTP_ConditionalGet(array(
+ *     'lastUpdateTime' => max(
+ *         filemtime(__FILE__)
+ *         ,filemtime('/path/to/header.inc')
+ *         ,filemtime('/path/to/footer.inc')
+ *     )
+ * ));
+ * $cg->sendHeaders();
+ * if ($cg->cacheIsValid) {
+ *     exit();
+ * }
+ * </code>
+ * @package Minify
+ * @subpackage HTTP
+ * @author Stephen Clay <steve@mrclay.org>
+ */
+class HTTP_ConditionalGet {
+
+    /**
+     * Does the client have a valid copy of the requested resource?
+     * 
+     * You'll want to check this after instantiating the object. If true, do
+     * not send content, just call sendHeaders() if you haven't already.
+     *
+     * @var bool
+     */
+    public $cacheIsValid = null;
+
+    /**
+     * @param array $spec options
+     * 
+     * 'isPublic': (bool) if true, the Cache-Control header will contain 
+     * "public", allowing proxies to cache the content. Otherwise "private" will 
+     * be sent, allowing only browser caching. (default false)
+     * 
+     * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers
+     * will be sent with content. This is recommended.
+     *
+     * 'encoding': (string) if set, the header "Vary: Accept-Encoding" will
+     * always be sent and a truncated version of the encoding will be appended
+     * to the ETag. E.g. "pub123456;gz". This will also trigger a more lenient 
+     * checking of the client's If-None-Match header, as the encoding portion of
+     * the ETag will be stripped before comparison.
+     * 
+     * 'contentHash': (string) if given, only the ETag header can be sent with
+     * content (only HTTP1.1 clients can conditionally GET). The given string 
+     * should be short with no quote characters and always change when the 
+     * resource changes (recommend md5()). This is not needed/used if 
+     * lastModifiedTime is given.
+     * 
+     * 'eTag': (string) if given, this will be used as the ETag header rather
+     * than values based on lastModifiedTime or contentHash. Also the encoding
+     * string will not be appended to the given value as described above.
+     * 
+     * 'invalidate': (bool) if true, the client cache will be considered invalid
+     * without testing. Effectively this disables conditional GET. 
+     * (default false)
+     * 
+     * 'maxAge': (int) if given, this will set the Cache-Control max-age in 
+     * seconds, and also set the Expires header to the equivalent GMT date. 
+     * After the max-age period has passed, the browser will again send a 
+     * conditional GET to revalidate its cache.
+     * 
+     * @return null
+     */
+    public function __construct($spec)
+    {
+        $scope = (isset($spec['isPublic']) && $spec['isPublic'])
+            ? 'public'
+            : 'private';
+        $maxAge = 0;
+        // backwards compatibility (can be removed later)
+        if (isset($spec['setExpires']) 
+            && is_numeric($spec['setExpires'])
+            && ! isset($spec['maxAge'])) {
+            $spec['maxAge'] = $spec['setExpires'] - $_SERVER['REQUEST_TIME'];
+        }
+        if (isset($spec['maxAge'])) {
+            $maxAge = $spec['maxAge'];
+            $this->_headers['Expires'] = self::gmtDate(
+                $_SERVER['REQUEST_TIME'] + $spec['maxAge'] 
+            );
+        }
+        $etagAppend = '';
+        if (isset($spec['encoding'])) {
+            $this->_stripEtag = true;
+            $this->_headers['Vary'] = 'Accept-Encoding';
+            if ('' !== $spec['encoding']) {
+                if (0 === strpos($spec['encoding'], 'x-')) {
+                    $spec['encoding'] = substr($spec['encoding'], 2);
+                }
+                $etagAppend = ';' . substr($spec['encoding'], 0, 2);
+            }
+        }
+        if (isset($spec['lastModifiedTime'])) {
+            $this->_setLastModified($spec['lastModifiedTime']);
+            if (isset($spec['eTag'])) { // Use it
+                $this->_setEtag($spec['eTag'], $scope);
+            } else { // base both headers on time
+                $this->_setEtag($spec['lastModifiedTime'] . $etagAppend, $scope);
+            }
+        } elseif (isset($spec['eTag'])) { // Use it
+            $this->_setEtag($spec['eTag'], $scope);
+        } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag
+            $this->_setEtag($spec['contentHash'] . $etagAppend, $scope);
+        }
+        $this->_headers['Cache-Control'] = "max-age={$maxAge}, {$scope}";
+        // invalidate cache if disabled, otherwise check
+        $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate'])
+            ? false
+            : $this->_isCacheValid();
+    }
+    
+    /**
+     * Get array of output headers to be sent
+     * 
+     * In the case of 304 responses, this array will only contain the response
+     * code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified')
+     * 
+     * Otherwise something like: 
+     * <code>
+     * array(
+     *     'Cache-Control' => 'max-age=0, public'
+     *     ,'ETag' => '"foobar"'
+     * )
+     * </code>
+     *
+     * @return array 
+     */
+    public function getHeaders()
+    {
+        return $this->_headers;
+    }
+
+    /**
+     * Set the Content-Length header in bytes
+     * 
+     * With most PHP configs, as long as you don't flush() output, this method
+     * is not needed and PHP will buffer all output and set Content-Length for 
+     * you. Otherwise you'll want to call this to let the client know up front.
+     * 
+     * @param int $bytes
+     * 
+     * @return int copy of input $bytes
+     */
+    public function setContentLength($bytes)
+    {
+        return $this->_headers['Content-Length'] = $bytes;
+    }
+
+    /**
+     * Send headers
+     * 
+     * @see getHeaders()
+     * 
+     * Note this doesn't "clear" the headers. Calling sendHeaders() will
+     * call header() again (but probably have not effect) and getHeaders() will
+     * still return the headers.
+     *
+     * @return null
+     */
+    public function sendHeaders()
+    {
+        $headers = $this->_headers;
+        if (array_key_exists('_responseCode', $headers)) {
+            header($headers['_responseCode']);
+            unset($headers['_responseCode']);
+        }
+        foreach ($headers as $name => $val) {
+            header($name . ': ' . $val);
+        }
+    }
+    
+    /**
+     * Exit if the client's cache is valid for this resource
+     *
+     * This is a convenience method for common use of the class
+     *
+     * @param int $lastModifiedTime if given, both ETag AND Last-Modified headers
+     * will be sent with content. This is recommended.
+     *
+     * @param bool $isPublic (default false) if true, the Cache-Control header 
+     * will contain "public", allowing proxies to cache the content. Otherwise 
+     * "private" will be sent, allowing only browser caching.
+     *
+     * @param array $options (default empty) additional options for constructor
+     *
+     * @return null     
+     */
+    public static function check($lastModifiedTime = null, $isPublic = false, $options = array())
+    {
+        if (null !== $lastModifiedTime) {
+            $options['lastModifiedTime'] = (int)$lastModifiedTime;
+        }
+        $options['isPublic'] = (bool)$isPublic;
+        $cg = new HTTP_ConditionalGet($options);
+        $cg->sendHeaders();
+        if ($cg->cacheIsValid) {
+            exit();
+        }
+    }
+    
+    
+    /**
+     * Get a GMT formatted date for use in HTTP headers
+     * 
+     * <code>
+     * header('Expires: ' . HTTP_ConditionalGet::gmtdate($time));
+     * </code>  
+     *
+     * @param int $time unix timestamp
+     * 
+     * @return string
+     */
+    public static function gmtDate($time)
+    {
+        return gmdate('D, d M Y H:i:s \G\M\T', $time);
+    }
+    
+    protected $_headers = array();
+    protected $_lmTime = null;
+    protected $_etag = null;
+    protected $_stripEtag = false;
+    
+    protected function _setEtag($hash, $scope)
+    {
+        $this->_etag = '"' . substr($scope, 0, 3) . $hash . '"';
+        $this->_headers['ETag'] = $this->_etag;
+    }
+
+    protected function _setLastModified($time)
+    {
+        $this->_lmTime = (int)$time;
+        $this->_headers['Last-Modified'] = self::gmtDate($time);
+    }
+
+    /**
+     * Determine validity of client cache and queue 304 header if valid
+     */
+    protected function _isCacheValid()
+    {
+        if (null === $this->_etag) {
+            // lmTime is copied to ETag, so this condition implies that the
+            // server sent neither ETag nor Last-Modified, so the client can't 
+            // possibly has a valid cache.
+            return false;
+        }
+        $isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified());
+        if ($isValid) {
+            $this->_headers['_responseCode'] = 'HTTP/1.0 304 Not Modified';
+        }
+        return $isValid;
+    }
+
+    protected function resourceMatchedEtag()
+    {
+        if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
+            return false;
+        }
+        $clientEtagList = get_magic_quotes_gpc()
+            ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])
+            : $_SERVER['HTTP_IF_NONE_MATCH'];
+        $clientEtags = explode(',', $clientEtagList);
+        
+        $compareTo = $this->normalizeEtag($this->_etag);
+        foreach ($clientEtags as $clientEtag) {
+            if ($this->normalizeEtag($clientEtag) === $compareTo) {
+                // respond with the client's matched ETag, even if it's not what
+                // we would've sent by default
+                $this->_headers['ETag'] = trim($clientEtag);
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    protected function normalizeEtag($etag) {
+        $etag = trim($etag);
+        return $this->_stripEtag
+            ? preg_replace('/;\\w\\w"$/', '"', $etag)
+            : $etag;
+    }
+
+    protected function resourceNotModified()
+    {
+        if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+            return false;
+        }
+        $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+        if (false !== ($semicolon = strrpos($ifModifiedSince, ';'))) {
+            // IE has tacked on extra data to this header, strip it
+            $ifModifiedSince = substr($ifModifiedSince, 0, $semicolon);
+        }
+        if ($ifModifiedSince == self::gmtDate($this->_lmTime)) {
+            // Apache 2.2's behavior. If there was no ETag match, send the 
+            // non-encoded version of the ETag value.
+            $this->_headers['ETag'] = $this->normalizeEtag($this->_etag);
+            return true;
+        }
+        return false;
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/LICENSE.txt
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/LICENSE.txt	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/LICENSE.txt	(revision 1957)
@@ -0,0 +1,26 @@
+Copyright (c) 2008 Ryan Grove <ryan@wonko.com>
+Copyright (c) 2008 Steve Clay <steve@mrclay.org>
+All rights reserved.
+
+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.
+  * Neither the name of this project nor the names of its contributors may be
+    used to endorse or promote products derived from this software without
+    specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR 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.
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Solar/Dir.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Solar/Dir.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Solar/Dir.php	(revision 1957)
@@ -0,0 +1,199 @@
+<?php
+/**
+ * 
+ * Utility class for static directory methods.
+ * 
+ * @category Solar
+ * 
+ * @package Solar
+ * 
+ * @author Paul M. Jones <pmjones@solarphp.com>
+ * 
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * 
+ * @version $Id: Dir.php 2926 2007-11-09 16:25:44Z pmjones $
+ * 
+ */
+class Solar_Dir {
+    
+    /**
+     * 
+     * The OS-specific temporary directory location.
+     * 
+     * @var string
+     * 
+     */
+    protected static $_tmp;
+    
+    /**
+     * 
+     * Hack for [[php::is_dir() | ]] that checks the include_path.
+     * 
+     * Use this to see if a directory exists anywhere in the include_path.
+     * 
+     * {{code: php
+     *     $dir = Solar_Dir::exists('path/to/dir')
+     *     if ($dir) {
+     *         $files = scandir($dir);
+     *     } else {
+     *         echo "Not found in the include-path.";
+     *     }
+     * }}
+     * 
+     * @param string $dir Check for this directory in the include_path.
+     * 
+     * @return mixed If the directory exists in the include_path, returns the
+     * absolute path; if not, returns boolean false.
+     * 
+     */
+    public static function exists($dir)
+    {
+        // no file requested?
+        $dir = trim($dir);
+        if (! $dir) {
+            return false;
+        }
+        
+        // using an absolute path for the file?
+        // dual check for Unix '/' and Windows '\',
+        // or Windows drive letter and a ':'.
+        $abs = ($dir[0] == '/' || $dir[0] == '\\' || $dir[1] == ':');
+        if ($abs && is_dir($dir)) {
+            return $dir;
+        }
+        
+        // using a relative path on the file
+        $path = explode(PATH_SEPARATOR, ini_get('include_path'));
+        foreach ($path as $base) {
+            // strip Unix '/' and Windows '\'
+            $target = rtrim($base, '\\/') . DIRECTORY_SEPARATOR . $dir;
+            if (is_dir($target)) {
+                return $target;
+            }
+        }
+        
+        // never found it
+        return false;
+    }
+    
+    /**
+     * 
+     * "Fixes" a directory string for the operating system.
+     * 
+     * Use slashes anywhere you need a directory separator. Then run the
+     * string through fixdir() and the slashes will be converted to the
+     * proper separator (for example '\' on Windows).
+     * 
+     * Always adds a final trailing separator.
+     * 
+     * @param string $dir The directory string to 'fix'.
+     * 
+     * @return string The "fixed" directory string.
+     * 
+     */
+    public static function fix($dir)
+    {
+        $dir = str_replace('/', DIRECTORY_SEPARATOR, $dir);
+        return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+    }
+    
+    /**
+     * 
+     * Convenience method for dirname() and higher-level directories.
+     * 
+     * @param string $file Get the dirname() of this file.
+     * 
+     * @param int $up Move up in the directory structure this many 
+     * times, default 0.
+     * 
+     * @return string The dirname() of the file.
+     * 
+     */
+    public static function name($file, $up = 0)
+    {
+        $dir = dirname($file);
+        while ($up --) {
+            $dir = dirname($dir);
+        }
+        return $dir;
+    }
+    
+    /**
+     * 
+     * Returns the OS-specific directory for temporary files.
+     * 
+     * @param string $sub Add this subdirectory to the returned temporary
+     * directory name.
+     * 
+     * @return string The temporary directory path.
+     * 
+     */
+    public static function tmp($sub = '')
+    {
+        // find the tmp dir if needed
+        if (! Solar_Dir::$_tmp) {
+            
+            // use the system if we can
+            if (function_exists('sys_get_temp_dir')) {
+                $tmp = sys_get_temp_dir();
+            } else {
+                $tmp = Solar_Dir::_tmp();
+            }
+            
+            // remove trailing separator and save
+            Solar_Dir::$_tmp = rtrim($tmp, DIRECTORY_SEPARATOR);
+        }
+        
+        // do we have a subdirectory request?
+        $sub = trim($sub);
+        if ($sub) {
+            // remove leading and trailing separators, and force exactly
+            // one trailing separator
+            $sub = trim($sub, DIRECTORY_SEPARATOR)
+                 . DIRECTORY_SEPARATOR;
+        }
+        
+        return Solar_Dir::$_tmp . DIRECTORY_SEPARATOR . $sub;
+    }
+    
+    /**
+     * 
+     * Returns the OS-specific temporary directory location.
+     * 
+     * @return string The temp directory path.
+     * 
+     */
+    protected static function _tmp()
+    {
+        // non-Windows system?
+        if (strtolower(substr(PHP_OS, 0, 3)) != 'win') {
+            $tmp = empty($_ENV['TMPDIR']) ? getenv('TMPDIR') : $_ENV['TMPDIR'];
+            if ($tmp) {
+                return $tmp;
+            } else {
+                return '/tmp';
+            }
+        }
+        
+        // Windows 'TEMP'
+        $tmp = empty($_ENV['TEMP']) ? getenv('TEMP') : $_ENV['TEMP'];
+        if ($tmp) {
+            return $tmp;
+        }
+    
+        // Windows 'TMP'
+        $tmp = empty($_ENV['TMP']) ? getenv('TMP') : $_ENV['TMP'];
+        if ($tmp) {
+            return $tmp;
+        }
+    
+        // Windows 'windir'
+        $tmp = empty($_ENV['windir']) ? getenv('windir') : $_ENV['windir'];
+        if ($tmp) {
+            return $tmp;
+        }
+    
+        // final fallback for Windows
+        return getenv('SystemRoot') . '\\temp';
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify.php	(revision 1957)
@@ -0,0 +1,532 @@
+<?php
+/**
+ * Class Minify  
+ * @package Minify
+ */
+
+/**
+ * Minify_Source
+ */
+#require_once 'Minify/Source.php';
+ 
+/**
+ * Minify - Combines, minifies, and caches JavaScript and CSS files on demand.
+ *
+ * See README for usage instructions (for now).
+ *
+ * This library was inspired by {@link mailto:flashkot@mail.ru jscsscomp by Maxim Martynyuk}
+ * and by the article {@link http://www.hunlock.com/blogs/Supercharged_Javascript "Supercharged JavaScript" by Patrick Hunlock}.
+ *
+ * Requires PHP 5.1.0.
+ * Tested on PHP 5.1.6.
+ *
+ * @package Minify
+ * @author Ryan Grove <ryan@wonko.com>
+ * @author Stephen Clay <steve@mrclay.org>
+ * @copyright 2008 Ryan Grove, Stephen Clay. All rights reserved.
+ * @license http://opensource.org/licenses/bsd-license.php  New BSD License
+ * @link http://code.google.com/p/minify/
+ */
+class Minify {
+    
+    const VERSION = '2.1.3';
+    const TYPE_CSS = 'text/css';
+    const TYPE_HTML = 'text/html';
+    // there is some debate over the ideal JS Content-Type, but this is the
+    // Apache default and what Yahoo! uses..
+    const TYPE_JS = 'application/x-javascript';
+    
+    /**
+     * How many hours behind are the file modification times of uploaded files?
+     * 
+     * If you upload files from Windows to a non-Windows server, Windows may report
+     * incorrect mtimes for the files. Immediately after modifying and uploading a 
+     * file, use the touch command to update the mtime on the server. If the mtime 
+     * jumps ahead by a number of hours, set this variable to that number. If the mtime 
+     * moves back, this should not be needed.
+     *
+     * @var int $uploaderHoursBehind
+     */
+    public static $uploaderHoursBehind = 0;
+    
+    /**
+     * If this string is not empty AND the serve() option 'bubbleCssImports' is
+     * NOT set, then serve() will check CSS files for @import declarations that
+     * appear too late in the combined stylesheet. If found, serve() will prepend
+     * the output with this warning.
+     *
+     * @var string $importWarning
+     */
+    public static $importWarning = "/* See http://code.google.com/p/minify/wiki/CommonProblems#@imports_can_appear_in_invalid_locations_in_combined_CSS_files */\n";
+    
+    /**
+     * Specify a cache object (with identical interface as Minify_Cache_File) or
+     * a path to use with Minify_Cache_File.
+     * 
+     * If not called, Minify will not use a cache and, for each 200 response, will 
+     * need to recombine files, minify and encode the output.
+     *
+     * @param mixed $cache object with identical interface as Minify_Cache_File or
+     * a directory path, or null to disable caching. (default = '')
+     * 
+     * @param bool $fileLocking (default = true) This only applies if the first
+     * parameter is a string.
+     *
+     * @return null
+     */
+    public static function setCache($cache = '', $fileLocking = true)
+    {
+        if (is_string($cache)) {
+            #require_once 'Minify/Cache/File.php';
+            self::$_cache = new Minify_Cache_File($cache, $fileLocking);
+        } else {
+            self::$_cache = $cache;
+        }
+    }
+    
+    /**
+     * Serve a request for a minified file. 
+     * 
+     * Here are the available options and defaults in the base controller:
+     * 
+     * 'isPublic' : send "public" instead of "private" in Cache-Control 
+     * headers, allowing shared caches to cache the output. (default true)
+     * 
+     * 'quiet' : set to true to have serve() return an array rather than sending
+     * any headers/output (default false)
+     * 
+     * 'encodeOutput' : set to false to disable content encoding, and not send
+     * the Vary header (default true)
+     * 
+     * 'encodeMethod' : generally you should let this be determined by 
+     * HTTP_Encoder (leave null), but you can force a particular encoding
+     * to be returned, by setting this to 'gzip' or '' (no encoding)
+     * 
+     * 'encodeLevel' : level of encoding compression (0 to 9, default 9)
+     * 
+     * 'contentTypeCharset' : appended to the Content-Type header sent. Set to a falsey
+     * value to remove. (default 'utf-8')  
+     * 
+     * 'maxAge' : set this to the number of seconds the client should use its cache
+     * before revalidating with the server. This sets Cache-Control: max-age and the
+     * Expires header. Unlike the old 'setExpires' setting, this setting will NOT
+     * prevent conditional GETs. Note this has nothing to do with server-side caching.
+     * 
+     * 'rewriteCssUris' : If true, serve() will automatically set the 'currentDir'
+     * minifier option to enable URI rewriting in CSS files (default true)
+     * 
+     * 'bubbleCssImports' : If true, all @import declarations in combined CSS
+     * files will be move to the top. Note this may alter effective CSS values
+     * due to a change in order. (default false)
+     * 
+     * 'debug' : set to true to minify all sources with the 'Lines' controller, which
+     * eases the debugging of combined files. This also prevents 304 responses.
+     * @see Minify_Lines::minify()
+     * 
+     * 'minifiers' : to override Minify's default choice of minifier function for 
+     * a particular content-type, specify your callback under the key of the 
+     * content-type:
+     * <code>
+     * // call customCssMinifier($css) for all CSS minification
+     * $options['minifiers'][Minify::TYPE_CSS] = 'customCssMinifier';
+     * 
+     * // don't minify Javascript at all
+     * $options['minifiers'][Minify::TYPE_JS] = '';
+     * </code>
+     * 
+     * 'minifierOptions' : to send options to the minifier function, specify your options
+     * under the key of the content-type. E.g. To send the CSS minifier an option: 
+     * <code>
+     * // give CSS minifier array('optionName' => 'optionValue') as 2nd argument 
+     * $options['minifierOptions'][Minify::TYPE_CSS]['optionName'] = 'optionValue';
+     * </code>
+     * 
+     * 'contentType' : (optional) this is only needed if your file extension is not 
+     * js/css/html. The given content-type will be sent regardless of source file
+     * extension, so this should not be used in a Groups config with other
+     * Javascript/CSS files.
+     * 
+     * Any controller options are documented in that controller's setupSources() method.
+     * 
+     * @param mixed instance of subclass of Minify_Controller_Base or string name of
+     * controller. E.g. 'Files'
+     * 
+     * @param array $options controller/serve options
+     * 
+     * @return mixed null, or, if the 'quiet' option is set to true, an array
+     * with keys "success" (bool), "statusCode" (int), "content" (string), and
+     * "headers" (array).
+     */
+    public static function serve($controller, $options = array())
+    {
+        if (is_string($controller)) {
+            // make $controller into object
+            $class = 'Minify_Controller_' . $controller;
+            if (! class_exists($class, false)) {
+                #require_once "Minify/Controller/" 
+                    . str_replace('_', '/', $controller) . ".php";    
+            }
+            $controller = new $class();
+        }
+        
+        // set up controller sources and mix remaining options with
+        // controller defaults
+        $options = $controller->setupSources($options);
+        $options = $controller->analyzeSources($options);
+        self::$_options = $controller->mixInDefaultOptions($options);
+        
+        // check request validity
+        if (! $controller->sources) {
+            // invalid request!
+            if (! self::$_options['quiet']) {
+                header(self::$_options['badRequestHeader']);
+                echo self::$_options['badRequestHeader'];
+                return;
+            } else {
+                list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']);
+                return array(
+                    'success' => false
+                    ,'statusCode' => (int)$statusCode
+                    ,'content' => ''
+                    ,'headers' => array()
+                );
+            }
+        }
+        
+        self::$_controller = $controller;
+        
+        if (self::$_options['debug']) {
+            self::_setupDebug($controller->sources);
+            self::$_options['maxAge'] = 0;
+        }
+        
+        // determine encoding
+        if (self::$_options['encodeOutput']) {
+            if (self::$_options['encodeMethod'] !== null) {
+                // controller specifically requested this
+                $contentEncoding = self::$_options['encodeMethod'];
+            } else {
+                // sniff request header
+                #require_once 'HTTP/Encoder.php';
+                // depending on what the client accepts, $contentEncoding may be 
+                // 'x-gzip' while our internal encodeMethod is 'gzip'. Calling
+                // getAcceptedEncoding(false, false) leaves out compress and deflate as options.
+                list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(false, false);
+            }
+        } else {
+            self::$_options['encodeMethod'] = ''; // identity (no encoding)
+        }
+        
+        // check client cache
+        #require_once 'HTTP/ConditionalGet.php';
+        $cgOptions = array(
+            'lastModifiedTime' => self::$_options['lastModifiedTime']
+            ,'isPublic' => self::$_options['isPublic']
+            ,'encoding' => self::$_options['encodeMethod']
+        );
+        if (self::$_options['maxAge'] > 0) {
+            $cgOptions['maxAge'] = self::$_options['maxAge'];
+        }
+        $cg = new HTTP_ConditionalGet($cgOptions);
+        if ($cg->cacheIsValid) {
+            // client's cache is valid
+            if (! self::$_options['quiet']) {
+                $cg->sendHeaders();
+                return;
+            } else {
+                return array(
+                    'success' => true
+                    ,'statusCode' => 304
+                    ,'content' => ''
+                    ,'headers' => $cg->getHeaders()
+                );
+            }
+        } else {
+            // client will need output
+            $headers = $cg->getHeaders();
+            unset($cg);
+        }
+        
+        if (self::$_options['contentType'] === self::TYPE_CSS
+            && self::$_options['rewriteCssUris']) {
+            reset($controller->sources);
+            while (list($key, $source) = each($controller->sources)) {
+                if ($source->filepath 
+                    && !isset($source->minifyOptions['currentDir'])
+                    && !isset($source->minifyOptions['prependRelativePath'])
+                ) {
+                    $source->minifyOptions['currentDir'] = dirname($source->filepath);
+                }
+            }
+        }
+        
+        // check server cache
+        if (null !== self::$_cache) {
+            // using cache
+            // the goal is to use only the cache methods to sniff the length and 
+            // output the content, as they do not require ever loading the file into
+            // memory.
+            $cacheId = 'minify_' . self::_getCacheId();
+            $fullCacheId = (self::$_options['encodeMethod'])
+                ? $cacheId . '.gz'
+                : $cacheId;
+            // check cache for valid entry
+            $cacheIsReady = self::$_cache->isValid($fullCacheId, self::$_options['lastModifiedTime']); 
+            if ($cacheIsReady) {
+                $cacheContentLength = self::$_cache->getSize($fullCacheId);    
+            } else {
+                // generate & cache content
+                $content = self::_combineMinify();
+                self::$_cache->store($cacheId, $content);
+                if (function_exists('gzencode')) {
+                    self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel']));
+                }
+            }
+        } else {
+            // no cache
+            $cacheIsReady = false;
+            $content = self::_combineMinify();
+        }
+        if (! $cacheIsReady && self::$_options['encodeMethod']) {
+            // still need to encode
+            $content = gzencode($content, self::$_options['encodeLevel']);
+        }
+        
+        // add headers
+        $headers['Content-Length'] = $cacheIsReady
+            ? $cacheContentLength
+            : strlen($content);
+        $headers['Content-Type'] = self::$_options['contentTypeCharset']
+            ? self::$_options['contentType'] . '; charset=' . self::$_options['contentTypeCharset']
+            : self::$_options['contentType'];
+        if (self::$_options['encodeMethod'] !== '') {
+            $headers['Content-Encoding'] = $contentEncoding;
+        }
+        if (self::$_options['encodeOutput']) {
+            $headers['Vary'] = 'Accept-Encoding';
+        }
+
+        if (! self::$_options['quiet']) {
+            // output headers & content
+            foreach ($headers as $name => $val) {
+                header($name . ': ' . $val);
+            }
+            if ($cacheIsReady) {
+                self::$_cache->display($fullCacheId);
+            } else {
+                echo $content;
+            }
+        } else {
+            return array(
+                'success' => true
+                ,'statusCode' => 200
+                ,'content' => $cacheIsReady
+                    ? self::$_cache->fetch($fullCacheId)
+                    : $content
+                ,'headers' => $headers
+            );
+        }
+    }
+    
+    /**
+     * Return combined minified content for a set of sources
+     *
+     * No internal caching will be used and the content will not be HTTP encoded.
+     * 
+     * @param array $sources array of filepaths and/or Minify_Source objects
+     * 
+     * @param array $options (optional) array of options for serve. By default
+     * these are already set: quiet = true, encodeMethod = '', lastModifiedTime = 0.
+     * 
+     * @return string
+     */
+    public static function combine($sources, $options = array())
+    {
+        $cache = self::$_cache;
+        self::$_cache = null;
+        $options = array_merge(array(
+            'files' => (array)$sources
+            ,'quiet' => true
+            ,'encodeMethod' => ''
+            ,'lastModifiedTime' => 0
+        ), $options);
+        $out = self::serve('Files', $options);
+        self::$_cache = $cache;
+        return $out['content'];
+    }
+    
+    /**
+     * On IIS, create $_SERVER['DOCUMENT_ROOT']
+     * 
+     * @param bool $unsetPathInfo (default false) if true, $_SERVER['PATH_INFO']
+     * will be unset (it is inconsistent with Apache's setting)
+     * 
+     * @return null
+     */
+    public static function setDocRoot($unsetPathInfo = false)
+    {
+        if (isset($_SERVER['SERVER_SOFTWARE'])
+            && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/')
+        ) {
+            $_SERVER['DOCUMENT_ROOT'] = rtrim(substr(
+                $_SERVER['PATH_TRANSLATED']
+                ,0
+                ,strlen($_SERVER['PATH_TRANSLATED']) - strlen($_SERVER['SCRIPT_NAME'])
+            ), '\\');
+            if ($unsetPathInfo) {
+                unset($_SERVER['PATH_INFO']);
+            }
+            #require_once 'Minify/Logger.php';
+            Minify_Logger::log("setDocRoot() set DOCUMENT_ROOT to \"{$_SERVER['DOCUMENT_ROOT']}\"");
+        }
+    }
+    
+    /**
+     * @var mixed Minify_Cache_* object or null (i.e. no server cache is used)
+     */
+    private static $_cache = null;
+    
+    /**
+     * @var Minify_Controller active controller for current request
+     */
+    protected static $_controller = null;
+    
+    /**
+     * @var array options for current request
+     */
+    protected static $_options = null;
+    
+    /**
+     * Set up sources to use Minify_Lines
+     *
+     * @param array $sources Minify_Source instances
+     *
+     * @return null
+     */
+    protected static function _setupDebug($sources)
+    {
+        foreach ($sources as $source) {
+            $source->minifier = array('Minify_Lines', 'minify');
+            $id = $source->getId();
+            $source->minifyOptions = array(
+                'id' => (is_file($id) ? basename($id) : $id)
+            );
+        }
+    }
+    
+    /**
+     * Combines sources and minifies the result.
+     *
+     * @return string
+     */
+    protected static function _combineMinify()
+    {
+        $type = self::$_options['contentType']; // ease readability
+        
+        // when combining scripts, make sure all statements separated and
+        // trailing single line comment is terminated
+        $implodeSeparator = ($type === self::TYPE_JS)
+            ? "\n;"
+            : '';
+        // allow the user to pass a particular array of options to each
+        // minifier (designated by type). source objects may still override
+        // these
+        $defaultOptions = isset(self::$_options['minifierOptions'][$type])
+            ? self::$_options['minifierOptions'][$type]
+            : array();
+        // if minifier not set, default is no minification. source objects
+        // may still override this
+        $defaultMinifier = isset(self::$_options['minifiers'][$type])
+            ? self::$_options['minifiers'][$type]
+            : false;
+       
+        if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) {
+            // all source have same options/minifier, better performance
+            // to combine, then minify once
+            foreach (self::$_controller->sources as $source) {
+                $pieces[] = $source->getContent();
+            }
+            $content = implode($implodeSeparator, $pieces);
+            if ($defaultMinifier) {
+                self::$_controller->loadMinifier($defaultMinifier);
+                $content = call_user_func($defaultMinifier, $content, $defaultOptions);    
+            }
+        } else {
+            // minify each source with its own options and minifier, then combine
+            foreach (self::$_controller->sources as $source) {
+                // allow the source to override our minifier and options
+                $minifier = (null !== $source->minifier)
+                    ? $source->minifier
+                    : $defaultMinifier;
+                $options = (null !== $source->minifyOptions)
+                    ? array_merge($defaultOptions, $source->minifyOptions)
+                    : $defaultOptions;
+                if ($minifier) {
+                    self::$_controller->loadMinifier($minifier);
+                    // get source content and minify it
+                    $pieces[] = call_user_func($minifier, $source->getContent(), $options);     
+                } else {
+                    $pieces[] = $source->getContent();     
+                }
+            }
+            $content = implode($implodeSeparator, $pieces);
+        }
+        
+        if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) {
+            $content = self::_handleCssImports($content);
+        }
+        
+        // do any post-processing (esp. for editing build URIs)
+        if (self::$_options['postprocessorRequire']) {
+            #require_once self::$_options['postprocessorRequire'];
+        }
+        if (self::$_options['postprocessor']) {
+            $content = call_user_func(self::$_options['postprocessor'], $content, $type);
+        }
+        return $content;
+    }
+    
+    /**
+     * Make a unique cache id for for this request.
+     * 
+     * Any settings that could affect output are taken into consideration  
+     *
+     * @return string
+     */
+    protected static function _getCacheId()
+    {
+        return md5(serialize(array(
+            Minify_Source::getDigest(self::$_controller->sources)
+            ,self::$_options['minifiers'] 
+            ,self::$_options['minifierOptions']
+            ,self::$_options['postprocessor']
+            ,self::$_options['bubbleCssImports']
+        )));
+    }
+    
+    /**
+     * Bubble CSS @imports to the top or prepend a warning if an
+     * @import is detected not at the top.
+     */
+    protected static function _handleCssImports($css)
+    {
+        if (self::$_options['bubbleCssImports']) {
+            // bubble CSS imports
+            preg_match_all('/@import.*?;/', $css, $imports);
+            $css = implode('', $imports[0]) . preg_replace('/@import.*?;/', '', $css);
+        } else if ('' !== self::$importWarning) {
+            // remove comments so we don't mistake { in a comment as a block
+            $noCommentCss = preg_replace('@/\\*[\\s\\S]*?\\*/@', '', $css);
+            $lastImportPos = strrpos($noCommentCss, '@import');
+            $firstBlockPos = strpos($noCommentCss, '{');
+            if (false !== $lastImportPos
+                && false !== $firstBlockPos
+                && $firstBlockPos < $lastImportPos
+            ) {
+                // { appears before @import : prepend warning
+                $css = self::$importWarning . $css;
+            }
+        }
+        return $css;
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/ABOUT-MINIFY-AND-APOSTROPHE.txt
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/ABOUT-MINIFY-AND-APOSTROPHE.txt	(revision 2481)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/ABOUT-MINIFY-AND-APOSTROPHE.txt	(revision 2481)
@@ -0,0 +1,6 @@
+tom@punkave.com: I modified this to not try to resolve symbolic links aggressively
+as that makes little sense when the paths you have passed in are web paths - the
+webserver does not resolve symlinks before resolving relative paths, so it doesn't
+make sense to realpath() everything.
+
+We do not use Minify as a content server, we use it only as a very smart minifier.
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/FirePHP.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/FirePHP.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/FirePHP.php	(revision 1957)
@@ -0,0 +1,1370 @@
+<?php
+/**
+ * *** BEGIN LICENSE BLOCK *****
+ *  
+ * This file is part of FirePHP (http://www.firephp.org/).
+ * 
+ * Software License Agreement (New BSD License)
+ * 
+ * Copyright (c) 2006-2008, Christoph Dorn
+ * All rights reserved.
+ * 
+ * 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.
+ * 
+ *     * Neither the name of Christoph Dorn nor the names of its
+ *       contributors may be used to endorse or promote products derived from this
+ *       software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR 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.
+ * 
+ * ***** END LICENSE BLOCK *****
+ * 
+ * @copyright   Copyright (C) 2007-2008 Christoph Dorn
+ * @author      Christoph Dorn <christoph@christophdorn.com>
+ * @license     http://www.opensource.org/licenses/bsd-license.php
+ * @package     FirePHP
+ */
+ 
+ 
+/**
+ * Sends the given data to the FirePHP Firefox Extension.
+ * The data can be displayed in the Firebug Console or in the
+ * "Server" request tab.
+ * 
+ * For more information see: http://www.firephp.org/
+ * 
+ * @copyright   Copyright (C) 2007-2008 Christoph Dorn
+ * @author      Christoph Dorn <christoph@christophdorn.com>
+ * @license     http://www.opensource.org/licenses/bsd-license.php
+ * @package     FirePHP
+ */
+class FirePHP {
+  
+  /**
+   * FirePHP version
+   *
+   * @var string
+   */
+  const VERSION = '0.2.0';
+  
+  /**
+   * Firebug LOG level
+   *
+   * Logs a message to firebug console.
+   * 
+   * @var string
+   */
+  const LOG = 'LOG';
+  
+  /**
+   * Firebug INFO level
+   *
+   * Logs a message to firebug console and displays an info icon before the message.
+   * 
+   * @var string
+   */
+  const INFO = 'INFO';
+  
+  /**
+   * Firebug WARN level
+   *
+   * Logs a message to firebug console, displays an warning icon before the message and colors the line turquoise.
+   * 
+   * @var string
+   */
+  const WARN = 'WARN';
+  
+  /**
+   * Firebug ERROR level
+   *
+   * Logs a message to firebug console, displays an error icon before the message and colors the line yellow. Also increments the firebug error count.
+   * 
+   * @var string
+   */
+  const ERROR = 'ERROR';
+  
+  /**
+   * Dumps a variable to firebug's server panel
+   *
+   * @var string
+   */
+  const DUMP = 'DUMP';
+  
+  /**
+   * Displays a stack trace in firebug console
+   *
+   * @var string
+   */
+  const TRACE = 'TRACE';
+  
+  /**
+   * Displays an exception in firebug console
+   * 
+   * Increments the firebug error count.
+   *
+   * @var string
+   */
+  const EXCEPTION = 'EXCEPTION';
+  
+  /**
+   * Displays an table in firebug console
+   *
+   * @var string
+   */
+  const TABLE = 'TABLE';
+  
+  /**
+   * Starts a group in firebug console
+   * 
+   * @var string
+   */
+  const GROUP_START = 'GROUP_START';
+  
+  /**
+   * Ends a group in firebug console
+   * 
+   * @var string
+   */
+  const GROUP_END = 'GROUP_END';
+  
+  /**
+   * Singleton instance of FirePHP
+   *
+   * @var FirePHP
+   */
+  protected static $instance = null;
+  
+  /**
+   * Wildfire protocol message index
+   *
+   * @var int
+   */
+  protected $messageIndex = 1;
+    
+  /**
+   * Options for the library
+   * 
+   * @var array
+   */
+  protected $options = array();
+  
+  /**
+   * Filters used to exclude object members when encoding
+   * 
+   * @var array
+   */
+  protected $objectFilters = array();
+  
+  /**
+   * A stack of objects used to detect recursion during object encoding
+   * 
+   * @var object
+   */
+  protected $objectStack = array();
+  
+  /**
+   * Flag to enable/disable logging
+   * 
+   * @var boolean
+   */
+  protected $enabled = true;
+  
+  /**
+   * The object constructor
+   */
+  function __construct() {
+    $this->options['maxObjectDepth'] = 10;
+    $this->options['maxArrayDepth'] = 20;
+    $this->options['useNativeJsonEncode'] = true;
+    $this->options['includeLineNumbers'] = true;
+  }
+    
+  /**
+   * When the object gets serialized only include specific object members.
+   * 
+   * @return array
+   */  
+  public function __sleep() {
+    return array('options','objectFilters','enabled');
+  }
+    
+  /**
+   * Gets singleton instance of FirePHP
+   *
+   * @param boolean $AutoCreate
+   * @return FirePHP
+   */
+  public static function getInstance($AutoCreate=false) {
+    if($AutoCreate===true && !self::$instance) {
+      self::init();
+    }
+    return self::$instance;
+  }
+   
+  /**
+   * Creates FirePHP object and stores it for singleton access
+   *
+   * @return FirePHP
+   */
+  public static function init() {
+    return self::$instance = new self();
+  }
+  
+  /**
+   * Enable and disable logging to Firebug
+   * 
+   * @param boolean $Enabled TRUE to enable, FALSE to disable
+   * @return void
+   */
+  public function setEnabled($Enabled) {
+    $this->enabled = $Enabled;
+  }
+  
+  /**
+   * Check if logging is enabled
+   * 
+   * @return boolean TRUE if enabled
+   */
+  public function getEnabled() {
+    return $this->enabled;
+  }
+  
+  /**
+   * Specify a filter to be used when encoding an object
+   * 
+   * Filters are used to exclude object members.
+   * 
+   * @param string $Class The class name of the object
+   * @param array $Filter An array or members to exclude
+   * @return void
+   */
+  public function setObjectFilter($Class, $Filter) {
+    $this->objectFilters[$Class] = $Filter;
+  }
+  
+  /**
+   * Set some options for the library
+   * 
+   * Options:
+   *  - maxObjectDepth: The maximum depth to traverse objects (default: 10)
+   *  - maxArrayDepth: The maximum depth to traverse arrays (default: 20)
+   *  - useNativeJsonEncode: If true will use json_encode() (default: true)
+   *  - includeLineNumbers: If true will include line numbers and filenames (default: true)
+   * 
+   * @param array $Options The options to be set
+   * @return void
+   */
+  public function setOptions($Options) {
+    $this->options = array_merge($this->options,$Options);
+  }
+  
+  /**
+   * Register FirePHP as your error handler
+   * 
+   * Will throw exceptions for each php error.
+   */
+  public function registerErrorHandler()
+  {
+    //NOTE: The following errors will not be caught by this error handler:
+    //      E_ERROR, E_PARSE, E_CORE_ERROR,
+    //      E_CORE_WARNING, E_COMPILE_ERROR,
+    //      E_COMPILE_WARNING, E_STRICT
+    
+    set_error_handler(array($this,'errorHandler'));     
+  }
+
+  /**
+   * FirePHP's error handler
+   * 
+   * Throws exception for each php error that will occur.
+   *
+   * @param int $errno
+   * @param string $errstr
+   * @param string $errfile
+   * @param int $errline
+   * @param array $errcontext
+   */
+  public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext)
+  {
+    // Don't throw exception if error reporting is switched off
+    if (error_reporting() == 0) {
+      return;
+    }
+    // Only throw exceptions for errors we are asking for
+    if (error_reporting() & $errno) {
+      throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
+    }
+  }
+  
+  /**
+   * Register FirePHP as your exception handler
+   */
+  public function registerExceptionHandler()
+  {
+    set_exception_handler(array($this,'exceptionHandler'));     
+  }
+  
+  /**
+   * FirePHP's exception handler
+   * 
+   * Logs all exceptions to your firebug console and then stops the script.
+   *
+   * @param Exception $Exception
+   * @throws Exception
+   */
+  function exceptionHandler($Exception) {
+    $this->fb($Exception);
+  }
+  
+  /**
+   * Set custom processor url for FirePHP
+   *
+   * @param string $URL
+   */    
+  public function setProcessorUrl($URL)
+  {
+    $this->setHeader('X-FirePHP-ProcessorURL', $URL);
+  }
+
+  /**
+   * Set custom renderer url for FirePHP
+   *
+   * @param string $URL
+   */
+  public function setRendererUrl($URL)
+  {
+    $this->setHeader('X-FirePHP-RendererURL', $URL);
+  }
+  
+  /**
+   * Start a group for following messages
+   *
+   * @param string $Name
+   * @return true
+   * @throws Exception
+   */
+  public function group($Name) {
+    return $this->fb(null, $Name, FirePHP::GROUP_START);
+  }
+  
+  /**
+   * Ends a group you have started before
+   *
+   * @return true
+   * @throws Exception
+   */
+  public function groupEnd() {
+    return $this->fb(null, null, FirePHP::GROUP_END);
+  }
+
+  /**
+   * Log object with label to firebug console
+   *
+   * @see FirePHP::LOG
+   * @param mixes $Object
+   * @param string $Label
+   * @return true
+   * @throws Exception
+   */
+  public function log($Object, $Label=null) {
+    return $this->fb($Object, $Label, FirePHP::LOG);
+  } 
+
+  /**
+   * Log object with label to firebug console
+   *
+   * @see FirePHP::INFO
+   * @param mixes $Object
+   * @param string $Label
+   * @return true
+   * @throws Exception
+   */
+  public function info($Object, $Label=null) {
+    return $this->fb($Object, $Label, FirePHP::INFO);
+  } 
+
+  /**
+   * Log object with label to firebug console
+   *
+   * @see FirePHP::WARN
+   * @param mixes $Object
+   * @param string $Label
+   * @return true
+   * @throws Exception
+   */
+  public function warn($Object, $Label=null) {
+    return $this->fb($Object, $Label, FirePHP::WARN);
+  } 
+
+  /**
+   * Log object with label to firebug console
+   *
+   * @see FirePHP::ERROR
+   * @param mixes $Object
+   * @param string $Label
+   * @return true
+   * @throws Exception
+   */
+  public function error($Object, $Label=null) {
+    return $this->fb($Object, $Label, FirePHP::ERROR);
+  } 
+
+  /**
+   * Dumps key and variable to firebug server panel
+   *
+   * @see FirePHP::DUMP
+   * @param string $Key
+   * @param mixed $Variable
+   * @return true
+   * @throws Exception
+   */
+  public function dump($Key, $Variable) {
+    return $this->fb($Variable, $Key, FirePHP::DUMP);
+  }
+  
+  /**
+   * Log a trace in the firebug console
+   *
+   * @see FirePHP::TRACE
+   * @param string $Label
+   * @return true
+   * @throws Exception
+   */
+  public function trace($Label) {
+    return $this->fb($Label, FirePHP::TRACE);
+  } 
+
+  /**
+   * Log a table in the firebug console
+   *
+   * @see FirePHP::TABLE
+   * @param string $Label
+   * @param string $Table
+   * @return true
+   * @throws Exception
+   */
+  public function table($Label, $Table) {
+    return $this->fb($Table, $Label, FirePHP::TABLE);
+  }
+  
+  /**
+   * Check if FirePHP is installed on client
+   *
+   * @return boolean
+   */
+  public function detectClientExtension() {
+    /* Check if FirePHP is installed on client */
+    if(!@preg_match_all('/\sFirePHP\/([\.|\d]*)\s?/si',$this->getUserAgent(),$m) ||
+       !version_compare($m[1][0],'0.0.6','>=')) {
+      return false;
+    }
+    return true;    
+  }
+ 
+  /**
+   * Log varible to Firebug
+   * 
+   * @see http://www.firephp.org/Wiki/Reference/Fb
+   * @param mixed $Object The variable to be logged
+   * @return true Return TRUE if message was added to headers, FALSE otherwise
+   * @throws Exception
+   */
+  public function fb($Object) {
+  
+    if(!$this->enabled) {
+      return false;
+    }
+  
+    if (headers_sent($filename, $linenum)) {
+        throw $this->newException('Headers already sent in '.$filename.' on line '.$linenum.'. Cannot send log data to FirePHP. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.');
+    }
+  
+    $Type = null;
+    $Label = null;
+  
+    if(func_num_args()==1) {
+    } else
+    if(func_num_args()==2) {
+      switch(func_get_arg(1)) {
+        case self::LOG:
+        case self::INFO:
+        case self::WARN:
+        case self::ERROR:
+        case self::DUMP:
+        case self::TRACE:
+        case self::EXCEPTION:
+        case self::TABLE:
+        case self::GROUP_START:
+        case self::GROUP_END:
+          $Type = func_get_arg(1);
+          break;
+        default:
+          $Label = func_get_arg(1);
+          break;
+      }
+    } else
+    if(func_num_args()==3) {
+      $Type = func_get_arg(2);
+      $Label = func_get_arg(1);
+    } else {
+      throw $this->newException('Wrong number of arguments to fb() function!');
+    }
+  
+  
+    if(!$this->detectClientExtension()) {
+      return false;
+    }
+  
+    $meta = array();
+    $skipFinalObjectEncode = false;
+  
+    if($Object instanceof Exception) {
+
+      $meta['file'] = $this->_escapeTraceFile($Object->getFile());
+      $meta['line'] = $Object->getLine();
+      
+      $trace = $Object->getTrace();
+      if($Object instanceof ErrorException
+         && isset($trace[0]['function'])
+         && $trace[0]['function']=='errorHandler'
+         && isset($trace[0]['class'])
+         && $trace[0]['class']=='FirePHP') {
+           
+        $severity = false;
+        switch($Object->getSeverity()) {
+          case E_WARNING: $severity = 'E_WARNING'; break;
+          case E_NOTICE: $severity = 'E_NOTICE'; break;
+          case E_USER_ERROR: $severity = 'E_USER_ERROR'; break;
+          case E_USER_WARNING: $severity = 'E_USER_WARNING'; break;
+          case E_USER_NOTICE: $severity = 'E_USER_NOTICE'; break;
+          case E_STRICT: $severity = 'E_STRICT'; break;
+          case E_RECOVERABLE_ERROR: $severity = 'E_RECOVERABLE_ERROR'; break;
+          case E_DEPRECATED: $severity = 'E_DEPRECATED'; break;
+          case E_USER_DEPRECATED: $severity = 'E_USER_DEPRECATED'; break;
+        }
+           
+        $Object = array('Class'=>get_class($Object),
+                        'Message'=>$severity.': '.$Object->getMessage(),
+                        'File'=>$this->_escapeTraceFile($Object->getFile()),
+                        'Line'=>$Object->getLine(),
+                        'Type'=>'trigger',
+                        'Trace'=>$this->_escapeTrace(array_splice($trace,2)));
+        $skipFinalObjectEncode = true;
+      } else {
+        $Object = array('Class'=>get_class($Object),
+                        'Message'=>$Object->getMessage(),
+                        'File'=>$this->_escapeTraceFile($Object->getFile()),
+                        'Line'=>$Object->getLine(),
+                        'Type'=>'throw',
+                        'Trace'=>$this->_escapeTrace($trace));
+        $skipFinalObjectEncode = true;
+      }
+      $Type = self::EXCEPTION;
+      
+    } else
+    if($Type==self::TRACE) {
+      
+      $trace = debug_backtrace();
+      if(!$trace) return false;
+      for( $i=0 ; $i<sizeof($trace) ; $i++ ) {
+
+        if(isset($trace[$i]['class'])
+           && isset($trace[$i]['file'])
+           && ($trace[$i]['class']=='FirePHP'
+               || $trace[$i]['class']=='FB')
+           && (substr($this->_standardizePath($trace[$i]['file']),-18,18)=='FirePHPCore/fb.php'
+               || substr($this->_standardizePath($trace[$i]['file']),-29,29)=='FirePHPCore/FirePHP.class.php')) {
+          /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */
+        } else
+        if(isset($trace[$i]['class'])
+           && isset($trace[$i+1]['file'])
+           && $trace[$i]['class']=='FirePHP'
+           && substr($this->_standardizePath($trace[$i+1]['file']),-18,18)=='FirePHPCore/fb.php') {
+          /* Skip fb() */
+        } else
+        if($trace[$i]['function']=='fb'
+           || $trace[$i]['function']=='trace'
+           || $trace[$i]['function']=='send') {
+          $Object = array('Class'=>isset($trace[$i]['class'])?$trace[$i]['class']:'',
+                          'Type'=>isset($trace[$i]['type'])?$trace[$i]['type']:'',
+                          'Function'=>isset($trace[$i]['function'])?$trace[$i]['function']:'',
+                          'Message'=>$trace[$i]['args'][0],
+                          'File'=>isset($trace[$i]['file'])?$this->_escapeTraceFile($trace[$i]['file']):'',
+                          'Line'=>isset($trace[$i]['line'])?$trace[$i]['line']:'',
+                          'Args'=>isset($trace[$i]['args'])?$this->encodeObject($trace[$i]['args']):'',
+                          'Trace'=>$this->_escapeTrace(array_splice($trace,$i+1)));
+
+          $skipFinalObjectEncode = true;
+          $meta['file'] = isset($trace[$i]['file'])?$this->_escapeTraceFile($trace[$i]['file']):'';
+          $meta['line'] = isset($trace[$i]['line'])?$trace[$i]['line']:'';
+          break;
+        }
+      }
+
+    } else
+    if($Type==self::TABLE) {
+      
+      if(isset($Object[0]) && is_string($Object[0])) {
+        $Object[1] = $this->encodeTable($Object[1]);
+      } else {
+        $Object = $this->encodeTable($Object);
+      }
+
+      $skipFinalObjectEncode = true;
+      
+    } else {
+      if($Type===null) {
+        $Type = self::LOG;
+      }
+    }
+    
+    if($this->options['includeLineNumbers']) {
+      if(!isset($meta['file']) || !isset($meta['line'])) {
+
+        $trace = debug_backtrace();
+        for( $i=0 ; $trace && $i<sizeof($trace) ; $i++ ) {
+  
+          if(isset($trace[$i]['class'])
+             && isset($trace[$i]['file'])
+             && ($trace[$i]['class']=='FirePHP'
+                 || $trace[$i]['class']=='FB')
+             && (substr($this->_standardizePath($trace[$i]['file']),-18,18)=='FirePHPCore/fb.php'
+                 || substr($this->_standardizePath($trace[$i]['file']),-29,29)=='FirePHPCore/FirePHP.class.php')) {
+            /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */
+          } else
+          if(isset($trace[$i]['class'])
+             && isset($trace[$i+1]['file'])
+             && $trace[$i]['class']=='FirePHP'
+             && substr($this->_standardizePath($trace[$i+1]['file']),-18,18)=='FirePHPCore/fb.php') {
+            /* Skip fb() */
+          } else
+          if(isset($trace[$i]['file'])
+             && substr($this->_standardizePath($trace[$i]['file']),-18,18)=='FirePHPCore/fb.php') {
+            /* Skip FB::fb() */
+          } else {
+            $meta['file'] = isset($trace[$i]['file'])?$this->_escapeTraceFile($trace[$i]['file']):'';
+            $meta['line'] = isset($trace[$i]['line'])?$trace[$i]['line']:'';
+            break;
+          }
+        }      
+      
+      }
+    } else {
+      unset($meta['file']);
+      unset($meta['line']);
+    }
+
+  	$this->setHeader('X-Wf-Protocol-1','http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
+  	$this->setHeader('X-Wf-1-Plugin-1','http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/'.self::VERSION);
+ 
+    $structure_index = 1;
+    if($Type==self::DUMP) {
+      $structure_index = 2;
+    	$this->setHeader('X-Wf-1-Structure-2','http://meta.firephp.org/Wildfire/Structure/FirePHP/Dump/0.1');
+    } else {
+    	$this->setHeader('X-Wf-1-Structure-1','http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1');
+    }
+  
+    if($Type==self::DUMP) {
+    	$msg = '{"'.$Label.'":'.$this->jsonEncode($Object, $skipFinalObjectEncode).'}';
+    } else {
+      $msg_meta = array('Type'=>$Type);
+      if($Label!==null) {
+        $msg_meta['Label'] = $Label;
+      }
+      if(isset($meta['file'])) {
+        $msg_meta['File'] = $meta['file'];
+      }
+      if(isset($meta['line'])) {
+        $msg_meta['Line'] = $meta['line'];
+      }
+    	$msg = '['.$this->jsonEncode($msg_meta).','.$this->jsonEncode($Object, $skipFinalObjectEncode).']';
+    }
+    
+    $parts = explode("\n",chunk_split($msg, 5000, "\n"));
+
+    for( $i=0 ; $i<count($parts) ; $i++) {
+        
+        $part = $parts[$i];
+        if ($part) {
+            
+            if(count($parts)>2) {
+              // Message needs to be split into multiple parts
+              $this->setHeader('X-Wf-1-'.$structure_index.'-'.'1-'.$this->messageIndex,
+                               (($i==0)?strlen($msg):'')
+                               . '|' . $part . '|'
+                               . (($i<count($parts)-2)?'\\':''));
+            } else {
+              $this->setHeader('X-Wf-1-'.$structure_index.'-'.'1-'.$this->messageIndex,
+                               strlen($part) . '|' . $part . '|');
+            }
+            
+            $this->messageIndex++;
+            
+            if ($this->messageIndex > 99999) {
+                throw new Exception('Maximum number (99,999) of messages reached!');             
+            }
+        }
+    }
+
+  	$this->setHeader('X-Wf-1-Index',$this->messageIndex-1);
+
+    return true;
+  }
+  
+  /**
+   * Standardizes path for windows systems.
+   *
+   * @param string $Path
+   * @return string
+   */
+  protected function _standardizePath($Path) {
+    return preg_replace('/\\\\+/','/',$Path);    
+  }
+  
+  /**
+   * Escape trace path for windows systems
+   *
+   * @param array $Trace
+   * @return array
+   */
+  protected function _escapeTrace($Trace) {
+    if(!$Trace) return $Trace;
+    for( $i=0 ; $i<sizeof($Trace) ; $i++ ) {
+      if(isset($Trace[$i]['file'])) {
+        $Trace[$i]['file'] = $this->_escapeTraceFile($Trace[$i]['file']);
+      }
+      if(isset($Trace[$i]['args'])) {
+        $Trace[$i]['args'] = $this->encodeObject($Trace[$i]['args']);
+      }
+    }
+    return $Trace;    
+  }
+  
+  /**
+   * Escape file information of trace for windows systems
+   *
+   * @param string $File
+   * @return string
+   */
+  protected function _escapeTraceFile($File) {
+    /* Check if we have a windows filepath */
+    if(strpos($File,'\\')) {
+      /* First strip down to single \ */
+      
+      $file = preg_replace('/\\\\+/','\\',$File);
+      
+      return $file;
+    }
+    return $File;
+  }
+
+  /**
+   * Send header
+   *
+   * @param string $Name
+   * @param string_type $Value
+   */
+  protected function setHeader($Name, $Value) {
+    return header($Name.': '.$Value);
+  }
+
+  /**
+   * Get user agent
+   *
+   * @return string|false
+   */
+  protected function getUserAgent() {
+    if(!isset($_SERVER['HTTP_USER_AGENT'])) return false;
+    return $_SERVER['HTTP_USER_AGENT'];
+  }
+
+  /**
+   * Returns a new exception
+   *
+   * @param string $Message
+   * @return Exception
+   */
+  protected function newException($Message) {
+    return new Exception($Message);
+  }
+  
+  /**
+   * Encode an object into a JSON string
+   * 
+   * Uses PHP's jeson_encode() if available
+   * 
+   * @param object $Object The object to be encoded
+   * @return string The JSON string
+   */
+  protected function jsonEncode($Object, $skipObjectEncode=false)
+  {
+    if(!$skipObjectEncode) {
+      $Object = $this->encodeObject($Object);
+    }
+    
+    if(function_exists('json_encode')
+       && $this->options['useNativeJsonEncode']!=false) {
+
+      return json_encode($Object);
+    } else {
+      return $this->json_encode($Object);
+    }
+  }
+  
+  /**
+   * Encodes a table by encoding each row and column with encodeObject()
+   * 
+   * @param array $Table The table to be encoded
+   * @return array
+   */  
+  protected function encodeTable($Table) {
+    if(!$Table) return $Table;
+    for( $i=0 ; $i<count($Table) ; $i++ ) {
+      if(is_array($Table[$i])) {
+        for( $j=0 ; $j<count($Table[$i]) ; $j++ ) {
+          $Table[$i][$j] = $this->encodeObject($Table[$i][$j]);
+        }
+      }
+    }
+    return $Table;
+  }
+  
+  /**
+   * Encodes an object including members with
+   * protected and private visibility
+   * 
+   * @param Object $Object The object to be encoded
+   * @param int $Depth The current traversal depth
+   * @return array All members of the object
+   */
+  protected function encodeObject($Object, $ObjectDepth = 1, $ArrayDepth = 1)
+  {
+    $return = array();
+    
+    if (is_object($Object)) {
+
+        if ($ObjectDepth > $this->options['maxObjectDepth']) {
+          return '** Max Object Depth ('.$this->options['maxObjectDepth'].') **';
+        }
+        
+        foreach ($this->objectStack as $refVal) {
+            if ($refVal === $Object) {
+                return '** Recursion ('.get_class($Object).') **';
+            }
+        }
+        array_push($this->objectStack, $Object);
+                
+        $return['__className'] = $class = get_class($Object);
+
+        $reflectionClass = new ReflectionClass($class);  
+        $properties = array();
+        foreach( $reflectionClass->getProperties() as $property) {
+          $properties[$property->getName()] = $property;
+        }
+            
+        $members = (array)$Object;
+            
+        foreach( $properties as $raw_name => $property ) {
+          
+          $name = $raw_name;
+          if($property->isStatic()) {
+            $name = 'static:'.$name;
+          }
+          if($property->isPublic()) {
+            $name = 'public:'.$name;
+          } else
+          if($property->isPrivate()) {
+            $name = 'private:'.$name;
+            $raw_name = "\0".$class."\0".$raw_name;
+          } else
+          if($property->isProtected()) {
+            $name = 'protected:'.$name;
+            $raw_name = "\0".'*'."\0".$raw_name;
+          }
+          
+          if(!(isset($this->objectFilters[$class])
+               && is_array($this->objectFilters[$class])
+               && in_array($raw_name,$this->objectFilters[$class]))) {
+
+            if(array_key_exists($raw_name,$members)
+               && !$property->isStatic()) {
+              
+              $return[$name] = $this->encodeObject($members[$raw_name], $ObjectDepth + 1, 1);      
+            
+            } else {
+              if(method_exists($property,'setAccessible')) {
+                $property->setAccessible(true);
+                $return[$name] = $this->encodeObject($property->getValue($Object), $ObjectDepth + 1, 1);
+              } else
+              if($property->isPublic()) {
+                $return[$name] = $this->encodeObject($property->getValue($Object), $ObjectDepth + 1, 1);
+              } else {
+                $return[$name] = '** Need PHP 5.3 to get value **';
+              }
+            }
+          } else {
+            $return[$name] = '** Excluded by Filter **';
+          }
+        }
+        
+        // Include all members that are not defined in the class
+        // but exist in the object
+        foreach( $members as $raw_name => $value ) {
+          
+          $name = $raw_name;
+          
+          if ($name{0} == "\0") {
+            $parts = explode("\0", $name);
+            $name = $parts[2];
+          }
+          
+          if(!isset($properties[$name])) {
+            $name = 'undeclared:'.$name;
+              
+            if(!(isset($this->objectFilters[$class])
+                 && is_array($this->objectFilters[$class])
+                 && in_array($raw_name,$this->objectFilters[$class]))) {
+              
+              $return[$name] = $this->encodeObject($value, $ObjectDepth + 1, 1);
+            } else {
+              $return[$name] = '** Excluded by Filter **';
+            }
+          }
+        }
+        
+        array_pop($this->objectStack);
+        
+    } elseif (is_array($Object)) {
+
+        if ($ArrayDepth > $this->options['maxArrayDepth']) {
+          return '** Max Array Depth ('.$this->options['maxArrayDepth'].') **';
+        }
+      
+        foreach ($Object as $key => $val) {
+          
+          // Encoding the $GLOBALS PHP array causes an infinite loop
+          // if the recursion is not reset here as it contains
+          // a reference to itself. This is the only way I have come up
+          // with to stop infinite recursion in this case.
+          if($key=='GLOBALS'
+             && is_array($val)
+             && array_key_exists('GLOBALS',$val)) {
+            $val['GLOBALS'] = '** Recursion (GLOBALS) **';
+          }
+          
+          $return[$key] = $this->encodeObject($val, 1, $ArrayDepth + 1);
+        }
+    } else {
+      if(self::is_utf8($Object)) {
+        return $Object;
+      } else {
+        return utf8_encode($Object);
+      }
+    }
+    return $return;
+  }
+
+  /**
+   * Returns true if $string is valid UTF-8 and false otherwise.
+   *
+   * @param mixed $str String to be tested
+   * @return boolean
+   */
+  protected static function is_utf8($str) {
+    $c=0; $b=0;
+    $bits=0;
+    $len=strlen($str);
+    for($i=0; $i<$len; $i++){
+        $c=ord($str[$i]);
+        if($c > 128){
+            if(($c >= 254)) return false;
+            elseif($c >= 252) $bits=6;
+            elseif($c >= 248) $bits=5;
+            elseif($c >= 240) $bits=4;
+            elseif($c >= 224) $bits=3;
+            elseif($c >= 192) $bits=2;
+            else return false;
+            if(($i+$bits) > $len) return false;
+            while($bits > 1){
+                $i++;
+                $b=ord($str[$i]);
+                if($b < 128 || $b > 191) return false;
+                $bits--;
+            }
+        }
+    }
+    return true;
+  } 
+
+  /**
+   * 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, including C, C++, C#, Java,
+   * JavaScript, Perl, TCL, and many others. 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!
+   *
+   * 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.
+   *
+   * @category
+   * @package     Services_JSON
+   * @author      Michal Migurski <mike-json@teczno.com>
+   * @author      Matt Knapp <mdknapp[at]gmail[dot]com>
+   * @author      Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
+   * @author      Christoph Dorn <christoph@christophdorn.com>
+   * @copyright   2005 Michal Migurski
+   * @version     CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
+   * @license     http://www.opensource.org/licenses/bsd-license.php
+   * @link        http://pear.php.net/pepr/pepr-proposal-show.php?id=198
+   */
+   
+     
+  /**
+   * Keep a list of objects as we descend into the array so we can detect recursion.
+   */
+  private $json_objectStack = array();
+
+
+ /**
+  * convert a string from one UTF-8 char to one UTF-16 char
+  *
+  * Normally should be handled by mb_convert_encoding, but
+  * provides a slower PHP-only method for installations
+  * that lack the multibye string extension.
+  *
+  * @param    string  $utf8   UTF-8 character
+  * @return   string  UTF-16 character
+  * @access   private
+  */
+  private function json_utf82utf16($utf8)
+  {
+      // oh please oh please oh please oh please oh please
+      if(function_exists('mb_convert_encoding')) {
+          return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
+      }
+
+      switch(strlen($utf8)) {
+          case 1:
+              // this case should never be reached, because we are in ASCII range
+              // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+              return $utf8;
+
+          case 2:
+              // return a UTF-16 character from a 2-byte UTF-8 char
+              // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+              return chr(0x07 & (ord($utf8{0}) >> 2))
+                   . chr((0xC0 & (ord($utf8{0}) << 6))
+                       | (0x3F & ord($utf8{1})));
+
+          case 3:
+              // return a UTF-16 character from a 3-byte UTF-8 char
+              // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+              return chr((0xF0 & (ord($utf8{0}) << 4))
+                       | (0x0F & (ord($utf8{1}) >> 2)))
+                   . chr((0xC0 & (ord($utf8{1}) << 6))
+                       | (0x7F & ord($utf8{2})));
+      }
+
+      // ignoring UTF-32 for now, sorry
+      return '';
+  }
+
+ /**
+  * encodes an arbitrary variable into JSON format
+  *
+  * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
+  *                           see argument 1 to Services_JSON() above for array-parsing behavior.
+  *                           if var is a strng, note that encode() always expects it
+  *                           to be in ASCII or UTF-8 format!
+  *
+  * @return   mixed   JSON string representation of input var or an error if a problem occurs
+  * @access   public
+  */
+  private function json_encode($var)
+  {
+    
+    if(is_object($var)) {
+      if(in_array($var,$this->json_objectStack)) {
+        return '"** Recursion **"';
+      }
+    }
+          
+      switch (gettype($var)) {
+          case 'boolean':
+              return $var ? 'true' : 'false';
+
+          case 'NULL':
+              return 'null';
+
+          case 'integer':
+              return (int) $var;
+
+          case 'double':
+          case 'float':
+              return (float) $var;
+
+          case 'string':
+              // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
+              $ascii = '';
+              $strlen_var = strlen($var);
+
+             /*
+              * Iterate over every character in the string,
+              * escaping with a slash or encoding to UTF-8 where necessary
+              */
+              for ($c = 0; $c < $strlen_var; ++$c) {
+
+                  $ord_var_c = ord($var{$c});
+
+                  switch (true) {
+                      case $ord_var_c == 0x08:
+                          $ascii .= '\b';
+                          break;
+                      case $ord_var_c == 0x09:
+                          $ascii .= '\t';
+                          break;
+                      case $ord_var_c == 0x0A:
+                          $ascii .= '\n';
+                          break;
+                      case $ord_var_c == 0x0C:
+                          $ascii .= '\f';
+                          break;
+                      case $ord_var_c == 0x0D:
+                          $ascii .= '\r';
+                          break;
+
+                      case $ord_var_c == 0x22:
+                      case $ord_var_c == 0x2F:
+                      case $ord_var_c == 0x5C:
+                          // double quote, slash, slosh
+                          $ascii .= '\\'.$var{$c};
+                          break;
+
+                      case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
+                          // characters U-00000000 - U-0000007F (same as ASCII)
+                          $ascii .= $var{$c};
+                          break;
+
+                      case (($ord_var_c & 0xE0) == 0xC0):
+                          // characters U-00000080 - U-000007FF, mask 110XXXXX
+                          // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                          $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
+                          $c += 1;
+                          $utf16 = $this->json_utf82utf16($char);
+                          $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                          break;
+
+                      case (($ord_var_c & 0xF0) == 0xE0):
+                          // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+                          // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                          $char = pack('C*', $ord_var_c,
+                                       ord($var{$c + 1}),
+                                       ord($var{$c + 2}));
+                          $c += 2;
+                          $utf16 = $this->json_utf82utf16($char);
+                          $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                          break;
+
+                      case (($ord_var_c & 0xF8) == 0xF0):
+                          // characters U-00010000 - U-001FFFFF, mask 11110XXX
+                          // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                          $char = pack('C*', $ord_var_c,
+                                       ord($var{$c + 1}),
+                                       ord($var{$c + 2}),
+                                       ord($var{$c + 3}));
+                          $c += 3;
+                          $utf16 = $this->json_utf82utf16($char);
+                          $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                          break;
+
+                      case (($ord_var_c & 0xFC) == 0xF8):
+                          // characters U-00200000 - U-03FFFFFF, mask 111110XX
+                          // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                          $char = pack('C*', $ord_var_c,
+                                       ord($var{$c + 1}),
+                                       ord($var{$c + 2}),
+                                       ord($var{$c + 3}),
+                                       ord($var{$c + 4}));
+                          $c += 4;
+                          $utf16 = $this->json_utf82utf16($char);
+                          $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                          break;
+
+                      case (($ord_var_c & 0xFE) == 0xFC):
+                          // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+                          // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                          $char = pack('C*', $ord_var_c,
+                                       ord($var{$c + 1}),
+                                       ord($var{$c + 2}),
+                                       ord($var{$c + 3}),
+                                       ord($var{$c + 4}),
+                                       ord($var{$c + 5}));
+                          $c += 5;
+                          $utf16 = $this->json_utf82utf16($char);
+                          $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                          break;
+                  }
+              }
+
+              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))) {
+                  
+                  $this->json_objectStack[] = $var;
+
+                  $properties = array_map(array($this, 'json_name_value'),
+                                          array_keys($var),
+                                          array_values($var));
+
+                  array_pop($this->json_objectStack);
+
+                  foreach($properties as $property) {
+                      if($property instanceof Exception) {
+                          return $property;
+                      }
+                  }
+
+                  return '{' . join(',', $properties) . '}';
+              }
+
+              $this->json_objectStack[] = $var;
+
+              // treat it like a regular array
+              $elements = array_map(array($this, 'json_encode'), $var);
+
+              array_pop($this->json_objectStack);
+
+              foreach($elements as $element) {
+                  if($element instanceof Exception) {
+                      return $element;
+                  }
+              }
+
+              return '[' . join(',', $elements) . ']';
+
+          case 'object':
+              $vars = self::encodeObject($var);
+
+              $this->json_objectStack[] = $var;
+
+              $properties = array_map(array($this, 'json_name_value'),
+                                      array_keys($vars),
+                                      array_values($vars));
+
+              array_pop($this->json_objectStack);
+              
+              foreach($properties as $property) {
+                  if($property instanceof Exception) {
+                      return $property;
+                  }
+              }
+                     
+              return '{' . join(',', $properties) . '}';
+
+          default:
+              return null;
+      }
+  }
+
+ /**
+  * array-walking function for use in generating JSON-formatted name-value pairs
+  *
+  * @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'
+  * @access   private
+  */
+  private function json_name_value($name, $value)
+  {
+      // Encoding the $GLOBALS PHP array causes an infinite loop
+      // if the recursion is not reset here as it contains
+      // a reference to itself. This is the only way I have come up
+      // with to stop infinite recursion in this case.
+      if($name=='GLOBALS'
+         && is_array($value)
+         && array_key_exists('GLOBALS',$value)) {
+        $value['GLOBALS'] = '** Recursion **';
+      }
+    
+      $encoded_value = $this->json_encode($value);
+
+      if($encoded_value instanceof Exception) {
+          return $encoded_value;
+      }
+
+      return $this->json_encode(strval($name)) . ':' . $encoded_value;
+  }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/JSMin.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/JSMin.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/JSMin.php	(revision 1957)
@@ -0,0 +1,314 @@
+<?php
+/**
+ * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
+ *
+ * This is a direct port of jsmin.c to PHP with a few PHP performance tweaks and
+ * modifications to preserve some comments (see below). Also, rather than using
+ * stdin/stdout, JSMin::minify() accepts a string as input and returns another
+ * string as output.
+ * 
+ * Comments containing IE conditional compilation are preserved, as are multi-line
+ * comments that begin with "/*!" (for documentation purposes). In the latter case
+ * newlines are inserted around the comment to enhance readability.
+ *
+ * PHP 5 or higher is required.
+ *
+ * Permission is hereby granted to use this version of the library under the
+ * same terms as jsmin.c, which has the following license:
+ *
+ * --
+ * Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * The Software shall be used for Good, not Evil.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ * --
+ *
+ * @package JSMin
+ * @author Ryan Grove <ryan@wonko.com> (PHP port)
+ * @author Steve Clay <steve@mrclay.org> (modifications + cleanup)
+ * @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp)
+ * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
+ * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
+ * @license http://opensource.org/licenses/mit-license.php MIT License
+ * @link http://code.google.com/p/jsmin-php/
+ */
+
+class JSMin {
+    const ORD_LF            = 10;
+    const ORD_SPACE         = 32;
+    const ACTION_KEEP_A     = 1;
+    const ACTION_DELETE_A   = 2;
+    const ACTION_DELETE_A_B = 3;
+    
+    protected $a           = "\n";
+    protected $b           = '';
+    protected $input       = '';
+    protected $inputIndex  = 0;
+    protected $inputLength = 0;
+    protected $lookAhead   = null;
+    protected $output      = '';
+    
+    /**
+     * Minify Javascript
+     *
+     * @param string $js Javascript to be minified
+     * @return string
+     */
+    public static function minify($js)
+    {
+        $jsmin = new JSMin($js);
+        return $jsmin->min();
+    }
+    
+    /**
+     * Setup process
+     */
+    public function __construct($input)
+    {
+        $this->input       = str_replace("\r\n", "\n", $input);
+        $this->inputLength = strlen($this->input);
+    }
+    
+    /**
+     * Perform minification, return result
+     */
+    public function min()
+    {
+        if ($this->output !== '') { // min already run
+            return $this->output;
+        }
+        $this->action(self::ACTION_DELETE_A_B);
+        
+        while ($this->a !== null) {
+            // determine next command
+            $command = self::ACTION_KEEP_A; // default
+            if ($this->a === ' ') {
+                if (! $this->isAlphaNum($this->b)) {
+                    $command = self::ACTION_DELETE_A;
+                }
+            } elseif ($this->a === "\n") {
+                if ($this->b === ' ') {
+                    $command = self::ACTION_DELETE_A_B;
+                } elseif (false === strpos('{[(+-', $this->b) 
+                          && ! $this->isAlphaNum($this->b)) {
+                    $command = self::ACTION_DELETE_A;
+                }
+            } elseif (! $this->isAlphaNum($this->a)) {
+                if ($this->b === ' '
+                    || ($this->b === "\n" 
+                        && (false === strpos('}])+-"\'', $this->a)))) {
+                    $command = self::ACTION_DELETE_A_B;
+                }
+            }
+            $this->action($command);
+        }
+        $this->output = trim($this->output);
+        return $this->output;
+    }
+    
+    /**
+     * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
+     * ACTION_DELETE_A = Copy B to A. Get the next B.
+     * ACTION_DELETE_A_B = Get the next B.
+     */
+    protected function action($command)
+    {
+        switch ($command) {
+            case self::ACTION_KEEP_A:
+                $this->output .= $this->a;
+                // fallthrough
+            case self::ACTION_DELETE_A:
+                $this->a = $this->b;
+                if ($this->a === "'" || $this->a === '"') { // string literal
+                    $str = $this->a; // in case needed for exception
+                    while (true) {
+                        $this->output .= $this->a;
+                        $this->a       = $this->get();
+                        if ($this->a === $this->b) { // end quote
+                            break;
+                        }
+                        if (ord($this->a) <= self::ORD_LF) {
+                            throw new JSMin_UnterminatedStringException(
+                                'Unterminated String: ' . var_export($str, true));
+                        }
+                        $str .= $this->a;
+                        if ($this->a === '\\') {
+                            $this->output .= $this->a;
+                            $this->a       = $this->get();
+                            $str .= $this->a;
+                        }
+                    }
+                }
+                // fallthrough
+            case self::ACTION_DELETE_A_B:
+                $this->b = $this->next();
+                if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal
+                    $this->output .= $this->a . $this->b;
+                    $pattern = '/'; // in case needed for exception
+                    while (true) {
+                        $this->a = $this->get();
+                        $pattern .= $this->a;
+                        if ($this->a === '/') { // end pattern
+                            break; // while (true)
+                        } elseif ($this->a === '\\') {
+                            $this->output .= $this->a;
+                            $this->a       = $this->get();
+                            $pattern      .= $this->a;
+                        } elseif (ord($this->a) <= self::ORD_LF) {
+                            throw new JSMin_UnterminatedRegExpException(
+                                'Unterminated RegExp: '. var_export($pattern, true));
+                        }
+                        $this->output .= $this->a;
+                    }
+                    $this->b = $this->next();
+                }
+            // end case ACTION_DELETE_A_B
+        }
+    }
+    
+    protected function isRegexpLiteral()
+    {
+        if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing
+            return true;
+        }
+        if (' ' === $this->a) {
+            $length = strlen($this->output);
+            if ($length < 2) { // weird edge case
+                return true;
+            }
+            // you can't divide a keyword
+            if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) {
+                if ($this->output === $m[0]) { // odd but could happen
+                    return true;
+                }
+                // make sure it's a keyword, not end of an identifier
+                $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
+                if (! $this->isAlphaNum($charBeforeKeyword)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Get next char. Convert ctrl char to space.
+     */
+    protected function get()
+    {
+        $c = $this->lookAhead;
+        $this->lookAhead = null;
+        if ($c === null) {
+            if ($this->inputIndex < $this->inputLength) {
+                $c = $this->input[$this->inputIndex];
+                $this->inputIndex += 1;
+            } else {
+                return null;
+            }
+        }
+        if ($c === "\r" || $c === "\n") {
+            return "\n";
+        }
+        if (ord($c) < self::ORD_SPACE) { // control char
+            return ' ';
+        }
+        return $c;
+    }
+    
+    /**
+     * Get next char. If is ctrl character, translate to a space or newline.
+     */
+    protected function peek()
+    {
+        $this->lookAhead = $this->get();
+        return $this->lookAhead;
+    }
+    
+    /**
+     * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII?
+     */
+    protected function isAlphaNum($c)
+    {
+        return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126);
+    }
+    
+    protected function singleLineComment()
+    {
+        $comment = '';
+        while (true) {
+            $get = $this->get();
+            $comment .= $get;
+            if (ord($get) <= self::ORD_LF) { // EOL reached
+                // if IE conditional comment
+                if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
+                    return "/{$comment}";
+                }
+                return $get;
+            }
+        }
+    }
+    
+    protected function multipleLineComment()
+    {
+        $this->get();
+        $comment = '';
+        while (true) {
+            $get = $this->get();
+            if ($get === '*') {
+                if ($this->peek() === '/') { // end of comment reached
+                    $this->get();
+                    // if comment preserved by YUI Compressor
+                    if (0 === strpos($comment, '!')) {
+                        return "\n/*" . substr($comment, 1) . "*/\n";
+                    }
+                    // if IE conditional comment
+                    if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
+                        return "/*{$comment}*/";
+                    }
+                    return ' ';
+                }
+            } elseif ($get === null) {
+                throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true));
+            }
+            $comment .= $get;
+        }
+    }
+    
+    /**
+     * Get the next character, skipping over comments.
+     * Some comments may be preserved.
+     */
+    protected function next()
+    {
+        $get = $this->get();
+        if ($get !== '/') {
+            return $get;
+        }
+        switch ($this->peek()) {
+            case '/': return $this->singleLineComment();
+            case '*': return $this->multipleLineComment();
+            default: return $get;
+        }
+    }
+}
+
+class JSMin_UnterminatedStringException extends Exception {}
+class JSMin_UnterminatedCommentException extends Exception {}
+class JSMin_UnterminatedRegExpException extends Exception {}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/JSMinPlus.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/JSMinPlus.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/JSMinPlus.php	(revision 1957)
@@ -0,0 +1,1872 @@
+<?php
+
+/**
+ * JSMinPlus version 1.1
+ *
+ * Minifies a javascript file using a javascript parser
+ *
+ * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript)
+ * References: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
+ * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/
+ * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716
+ *
+ * Tino Zijdel <crisp@tweakers.net>
+ *
+ * Usage: $minified = JSMinPlus::minify($script [, $filename])
+ *
+ * Versionlog (see also changelog.txt):
+ * 12-04-2009 - some small bugfixes and performance improvements
+ * 09-04-2009 - initial open sourced version 1.0
+ *
+ * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
+ *
+ */
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Narcissus JavaScript engine.
+ *
+ * The Initial Developer of the Original Code is
+ * Brendan Eich <brendan@mozilla.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2004
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s): Tino Zijdel <crisp@tweakers.net>
+ * PHP port, modifications and minifier routine are (C) 2009
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('TOKEN_END', 1);
+define('TOKEN_NUMBER', 2);
+define('TOKEN_IDENTIFIER', 3);
+define('TOKEN_STRING', 4);
+define('TOKEN_REGEXP', 5);
+define('TOKEN_NEWLINE', 6);
+define('TOKEN_CONDCOMMENT_MULTILINE', 7);
+
+define('JS_SCRIPT', 100);
+define('JS_BLOCK', 101);
+define('JS_LABEL', 102);
+define('JS_FOR_IN', 103);
+define('JS_CALL', 104);
+define('JS_NEW_WITH_ARGS', 105);
+define('JS_INDEX', 106);
+define('JS_ARRAY_INIT', 107);
+define('JS_OBJECT_INIT', 108);
+define('JS_PROPERTY_INIT', 109);
+define('JS_GETTER', 110);
+define('JS_SETTER', 111);
+define('JS_GROUP', 112);
+define('JS_LIST', 113);
+
+define('DECLARED_FORM', 0);
+define('EXPRESSED_FORM', 1);
+define('STATEMENT_FORM', 2);
+
+class JSMinPlus
+{
+	private $parser;
+	private $reserved = array(
+		'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
+		'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
+		'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
+		'void', 'while', 'with',
+		// Words reserved for future use
+		'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
+		'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
+		'implements', 'import', 'int', 'interface', 'long', 'native',
+		'package', 'private', 'protected', 'public', 'short', 'static',
+		'super', 'synchronized', 'throws', 'transient', 'volatile',
+		// These are not reserved, but should be taken into account
+		// in isValidIdentifier (See jslint source code)
+		'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
+	);
+
+	private function __construct()
+	{
+		$this->parser = new JSParser();
+	}
+
+	public static function minify($js, $filename='')
+	{
+		static $instance;
+
+		// this is a singleton
+		if(!$instance)
+			$instance = new JSMinPlus();
+
+		return $instance->min($js, $filename);
+	}
+
+	private function min($js, $filename)
+	{
+		try
+		{
+			$n = $this->parser->parse($js, $filename, 1);
+			return $this->parseTree($n);
+		}
+		catch(Exception $e)
+		{
+			echo $e->getMessage() . "\n";
+		}
+
+		return false;
+	}
+
+	private function parseTree($n, $noBlockGrouping = false)
+	{
+		$s = '';
+
+		switch ($n->type)
+		{
+			case KEYWORD_FUNCTION:
+				$s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
+				$params = $n->params;
+				for ($i = 0, $j = count($params); $i < $j; $i++)
+					$s .= ($i ? ',' : '') . $params[$i];
+				$s .= '){' . $this->parseTree($n->body, true) . '}';
+			break;
+
+			case JS_SCRIPT:
+				// we do nothing with funDecls or varDecls
+				$noBlockGrouping = true;
+			// fall through
+			case JS_BLOCK:
+				$childs = $n->treeNodes;
+				for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++)
+				{
+					$t = $this->parseTree($childs[$i]);
+					if (strlen($t))
+					{
+						if ($c)
+						{
+							if ($childs[$i]->type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
+								$s .= "\n"; // put declared functions on a new line
+							else
+								$s .= ';';
+						}
+
+						$s .= $t;
+
+						$c++;
+					}
+				}
+
+				if ($c > 1 && !$noBlockGrouping)
+				{
+					$s = '{' . $s . '}';
+				}
+			break;
+
+			case KEYWORD_IF:
+				$s = 'if(' . $this->parseTree($n->condition) . ')';
+				$thenPart = $this->parseTree($n->thenPart);
+				$elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;
+
+				// quite a rancid hack to see if we should enclose the thenpart in brackets
+				if ($thenPart[0] != '{')
+				{
+					if (strpos($thenPart, 'if(') !== false)
+						$thenPart = '{' . $thenPart . '}';
+					elseif ($elsePart)
+						$thenPart .= ';';
+				}
+
+				$s .= $thenPart;
+
+				if ($elsePart)
+				{
+					$s .= 'else';
+
+					if ($elsePart[0] != '{')
+						$s .= ' ';
+
+					$s .= $elsePart;
+				}
+			break;
+
+			case KEYWORD_SWITCH:
+				$s = 'switch(' . $this->parseTree($n->discriminant) . '){';
+				$cases = $n->cases;
+				for ($i = 0, $j = count($cases); $i < $j; $i++)
+				{
+					$case = $cases[$i];
+					if ($case->type == KEYWORD_CASE)
+						$s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':';
+					else
+						$s .= 'default:';
+
+					$statement = $this->parseTree($case->statements);
+					if ($statement)
+						$s .= $statement . ';';
+				}
+				$s = rtrim($s, ';') . '}';
+			break;
+
+			case KEYWORD_FOR:
+				$s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
+					. ';' . ($n->condition ? $this->parseTree($n->condition) : '')
+					. ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'
+					. $this->parseTree($n->body);
+			break;
+
+			case KEYWORD_WHILE:
+				$s = 'while(' . $this->parseTree($n->condition) . ')' . $this->parseTree($n->body);
+			break;
+
+			case JS_FOR_IN:
+				$s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
+			break;
+
+			case KEYWORD_DO:
+				$s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')';
+			break;
+
+			case KEYWORD_BREAK:
+			case KEYWORD_CONTINUE:
+				$s = $n->value . ($n->label ? ' ' . $n->label : '');
+			break;
+
+			case KEYWORD_TRY:
+				$s = 'try{' . $this->parseTree($n->tryBlock, true) . '}';
+				$catchClauses = $n->catchClauses;
+				for ($i = 0, $j = count($catchClauses); $i < $j; $i++)
+				{
+					$t = $catchClauses[$i];
+					$s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}';
+				}
+				if ($n->finallyBlock)
+					$s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}';
+			break;
+
+			case KEYWORD_THROW:
+				$s = 'throw ' . $this->parseTree($n->exception);
+			break;
+
+			case KEYWORD_RETURN:
+				$s = 'return' . ($n->value ? ' ' . $this->parseTree($n->value) : '');
+			break;
+
+			case KEYWORD_WITH:
+				$s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
+			break;
+
+			case KEYWORD_VAR:
+			case KEYWORD_CONST:
+				$s = $n->value . ' ';
+				$childs = $n->treeNodes;
+				for ($i = 0, $j = count($childs); $i < $j; $i++)
+				{
+					$t = $childs[$i];
+					$s .= ($i ? ',' : '') . $t->name;
+					$u = $t->initializer;
+					if ($u)
+						$s .= '=' . $this->parseTree($u);
+				}
+			break;
+
+			case KEYWORD_DEBUGGER:
+				throw new Exception('NOT IMPLEMENTED: DEBUGGER');
+			break;
+
+			case TOKEN_CONDCOMMENT_MULTILINE:
+				$s = $n->value . ' ';
+				$childs = $n->treeNodes;
+				for ($i = 0, $j = count($childs); $i < $j; $i++)
+					$s .= $this->parseTree($childs[$i]);
+			break;
+
+			case OP_SEMICOLON:
+				if ($expression = $n->expression)
+					$s = $this->parseTree($expression);
+			break;
+
+			case JS_LABEL:
+				$s = $n->label . ':' . $this->parseTree($n->statement);
+			break;
+
+			case OP_COMMA:
+				$childs = $n->treeNodes;
+				for ($i = 0, $j = count($childs); $i < $j; $i++)
+					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
+			break;
+
+			case OP_ASSIGN:
+				$s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]);
+			break;
+
+			case OP_HOOK:
+				$s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]);
+			break;
+
+			case OP_OR: case OP_AND:
+			case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND:
+			case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
+			case OP_LT: case OP_LE: case OP_GE: case OP_GT:
+			case OP_LSH: case OP_RSH: case OP_URSH:
+			case OP_MUL: case OP_DIV: case OP_MOD:
+				$s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]);
+			break;
+
+			case OP_PLUS:
+			case OP_MINUS:
+				$s = $this->parseTree($n->treeNodes[0]) . $n->type;
+				$nextTokenType = $n->treeNodes[1]->type;
+				if (	$nextTokenType == OP_PLUS || $nextTokenType == OP_MINUS ||
+					$nextTokenType == OP_INCREMENT || $nextTokenType == OP_DECREMENT ||
+					$nextTokenType == OP_UNARY_PLUS || $nextTokenType == OP_UNARY_MINUS
+				)
+					$s .= ' ';
+				$s .= $this->parseTree($n->treeNodes[1]);
+			break;
+
+			case KEYWORD_IN:
+				$s = $this->parseTree($n->treeNodes[0]) . ' in ' . $this->parseTree($n->treeNodes[1]);
+			break;
+
+			case KEYWORD_INSTANCEOF:
+				$s = $this->parseTree($n->treeNodes[0]) . ' instanceof ' . $this->parseTree($n->treeNodes[1]);
+			break;
+
+			case KEYWORD_DELETE:
+				$s = 'delete ' . $this->parseTree($n->treeNodes[0]);
+			break;
+
+			case KEYWORD_VOID:
+				$s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
+			break;
+
+			case KEYWORD_TYPEOF:
+				$s = 'typeof ' . $this->parseTree($n->treeNodes[0]);
+			break;
+
+			case OP_NOT:
+			case OP_BITWISE_NOT:
+			case OP_UNARY_PLUS:
+			case OP_UNARY_MINUS:
+				$s = $n->value . $this->parseTree($n->treeNodes[0]);
+			break;
+
+			case OP_INCREMENT:
+			case OP_DECREMENT:
+				if ($n->postfix)
+					$s = $this->parseTree($n->treeNodes[0]) . $n->value;
+				else
+					$s = $n->value . $this->parseTree($n->treeNodes[0]);
+			break;
+
+			case OP_DOT:
+				$s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]);
+			break;
+
+			case JS_INDEX:
+				$s = $this->parseTree($n->treeNodes[0]);
+				// See if we can replace named index with a dot saving 3 bytes
+				if (	$n->treeNodes[0]->type == TOKEN_IDENTIFIER &&
+					$n->treeNodes[1]->type == TOKEN_STRING &&
+					$this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1))
+				)
+					$s .= '.' . substr($n->treeNodes[1]->value, 1, -1);
+				else
+					$s .= '[' . $this->parseTree($n->treeNodes[1]) . ']';
+			break;
+
+			case JS_LIST:
+				$childs = $n->treeNodes;
+				for ($i = 0, $j = count($childs); $i < $j; $i++)
+					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
+			break;
+
+			case JS_CALL:
+				$s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')';
+			break;
+
+			case KEYWORD_NEW:
+			case JS_NEW_WITH_ARGS:
+				$s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')';
+			break;
+
+			case JS_ARRAY_INIT:
+				$s = '[';
+				$childs = $n->treeNodes;
+				for ($i = 0, $j = count($childs); $i < $j; $i++)
+				{
+					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
+				}
+				$s .= ']';
+			break;
+
+			case JS_OBJECT_INIT:
+				$s = '{';
+				$childs = $n->treeNodes;
+				for ($i = 0, $j = count($childs); $i < $j; $i++)
+				{
+					$t = $childs[$i];
+					if ($i)
+						$s .= ',';
+					if ($t->type == JS_PROPERTY_INIT)
+					{
+						// Ditch the quotes when the index is a valid identifier
+						if (	$t->treeNodes[0]->type == TOKEN_STRING &&
+							$this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1))
+						)
+							$s .= substr($t->treeNodes[0]->value, 1, -1);
+						else
+							$s .= $t->treeNodes[0]->value;
+
+						$s .= ':' . $this->parseTree($t->treeNodes[1]);
+					}
+					else
+					{
+						$s .= $t->type == JS_GETTER ? 'get' : 'set';
+						$s .= ' ' . $t->name . '(';
+						$params = $t->params;
+						for ($i = 0, $j = count($params); $i < $j; $i++)
+							$s .= ($i ? ',' : '') . $params[$i];
+						$s .= '){' . $this->parseTree($t->body, true) . '}';
+					}
+				}
+				$s .= '}';
+			break;
+
+			case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
+			case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
+				$s = $n->value;
+			break;
+
+			case JS_GROUP:
+				$s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
+			break;
+
+			default:
+				throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type);
+		}
+
+		return $s;
+	}
+
+	private function isValidIdentifier($string)
+	{
+		return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
+	}
+}
+
+class JSParser
+{
+	private $t;
+
+	private $opPrecedence = array(
+		';' => 0,
+		',' => 1,
+		'=' => 2, '?' => 2, ':' => 2,
+		// The above all have to have the same precedence, see bug 330975.
+		'||' => 4,
+		'&&' => 5,
+		'|' => 6,
+		'^' => 7,
+		'&' => 8,
+		'==' => 9, '!=' => 9, '===' => 9, '!==' => 9,
+		'<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10,
+		'<<' => 11, '>>' => 11, '>>>' => 11,
+		'+' => 12, '-' => 12,
+		'*' => 13, '/' => 13, '%' => 13,
+		'delete' => 14, 'void' => 14, 'typeof' => 14,
+		'!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14,
+		'++' => 15, '--' => 15,
+		'new' => 16,
+		'.' => 17,
+		JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0,
+		JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0
+	);
+
+	private $opArity = array(
+		',' => -2,
+		'=' => 2,
+		'?' => 3,
+		'||' => 2,
+		'&&' => 2,
+		'|' => 2,
+		'^' => 2,
+		'&' => 2,
+		'==' => 2, '!=' => 2, '===' => 2, '!==' => 2,
+		'<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2,
+		'<<' => 2, '>>' => 2, '>>>' => 2,
+		'+' => 2, '-' => 2,
+		'*' => 2, '/' => 2, '%' => 2,
+		'delete' => 1, 'void' => 1, 'typeof' => 1,
+		'!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1,
+		'++' => 1, '--' => 1,
+		'new' => 1,
+		'.' => 2,
+		JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2,
+		JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1,
+		TOKEN_CONDCOMMENT_MULTILINE => 1
+	);
+
+	public function __construct()
+	{
+		$this->t = new JSTokenizer();
+	}
+
+	public function parse($s, $f, $l)
+	{
+		// initialize tokenizer
+		$this->t->init($s, $f, $l);
+
+		$x = new JSCompilerContext(false);
+		$n = $this->Script($x);
+		if (!$this->t->isDone())
+			throw $this->t->newSyntaxError('Syntax error');
+
+		return $n;
+	}
+
+	private function Script($x)
+	{
+		$n = $this->Statements($x);
+		$n->type = JS_SCRIPT;
+		$n->funDecls = $x->funDecls;
+		$n->varDecls = $x->varDecls;
+
+		return $n;
+	}
+
+	private function Statements($x)
+	{
+		$n = new JSNode($this->t, JS_BLOCK);
+		array_push($x->stmtStack, $n);
+
+		while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY)
+			$n->addNode($this->Statement($x));
+
+		array_pop($x->stmtStack);
+
+		return $n;
+	}
+
+	private function Block($x)
+	{
+		$this->t->mustMatch(OP_LEFT_CURLY);
+		$n = $this->Statements($x);
+		$this->t->mustMatch(OP_RIGHT_CURLY);
+
+		return $n;
+	}
+
+	private function Statement($x)
+	{
+		$tt = $this->t->get();
+		$n2 = null;
+
+		// Cases for statements ending in a right curly return early, avoiding the
+		// common semicolon insertion magic after this switch.
+		switch ($tt)
+		{
+			case KEYWORD_FUNCTION:
+				return $this->FunctionDefinition(
+					$x,
+					true,
+					count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM
+				);
+			break;
+
+			case OP_LEFT_CURLY:
+				$n = $this->Statements($x);
+				$this->t->mustMatch(OP_RIGHT_CURLY);
+			return $n;
+
+			case KEYWORD_IF:
+				$n = new JSNode($this->t);
+				$n->condition = $this->ParenExpression($x);
+				array_push($x->stmtStack, $n);
+				$n->thenPart = $this->Statement($x);
+				$n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null;
+				array_pop($x->stmtStack);
+			return $n;
+
+			case KEYWORD_SWITCH:
+				$n = new JSNode($this->t);
+				$this->t->mustMatch(OP_LEFT_PAREN);
+				$n->discriminant = $this->Expression($x);
+				$this->t->mustMatch(OP_RIGHT_PAREN);
+				$n->cases = array();
+				$n->defaultIndex = -1;
+
+				array_push($x->stmtStack, $n);
+
+				$this->t->mustMatch(OP_LEFT_CURLY);
+
+				while (($tt = $this->t->get()) != OP_RIGHT_CURLY)
+				{
+					switch ($tt)
+					{
+						case KEYWORD_DEFAULT:
+							if ($n->defaultIndex >= 0)
+								throw $this->t->newSyntaxError('More than one switch default');
+							// FALL THROUGH
+						case KEYWORD_CASE:
+							$n2 = new JSNode($this->t);
+							if ($tt == KEYWORD_DEFAULT)
+								$n->defaultIndex = count($n->cases);
+							else
+								$n2->caseLabel = $this->Expression($x, OP_COLON);
+								break;
+						default:
+							throw $this->t->newSyntaxError('Invalid switch case');
+					}
+
+					$this->t->mustMatch(OP_COLON);
+					$n2->statements = new JSNode($this->t, JS_BLOCK);
+					while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY)
+						$n2->statements->addNode($this->Statement($x));
+
+					array_push($n->cases, $n2);
+				}
+
+				array_pop($x->stmtStack);
+			return $n;
+
+			case KEYWORD_FOR:
+				$n = new JSNode($this->t);
+				$n->isLoop = true;
+				$this->t->mustMatch(OP_LEFT_PAREN);
+
+				if (($tt = $this->t->peek()) != OP_SEMICOLON)
+				{
+					$x->inForLoopInit = true;
+					if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST)
+					{
+						$this->t->get();
+						$n2 = $this->Variables($x);
+					}
+					else
+					{
+						$n2 = $this->Expression($x);
+					}
+					$x->inForLoopInit = false;
+				}
+
+				if ($n2 && $this->t->match(KEYWORD_IN))
+				{
+					$n->type = JS_FOR_IN;
+					if ($n2->type == KEYWORD_VAR)
+					{
+						if (count($n2->treeNodes) != 1)
+						{
+							throw $this->t->SyntaxError(
+								'Invalid for..in left-hand side',
+								$this->t->filename,
+								$n2->lineno
+							);
+						}
+
+						// NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
+						$n->iterator = $n2->treeNodes[0];
+						$n->varDecl = $n2;
+					}
+					else
+					{
+						$n->iterator = $n2;
+						$n->varDecl = null;
+					}
+
+					$n->object = $this->Expression($x);
+				}
+				else
+				{
+					$n->setup = $n2 ? $n2 : null;
+					$this->t->mustMatch(OP_SEMICOLON);
+					$n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x);
+					$this->t->mustMatch(OP_SEMICOLON);
+					$n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x);
+				}
+
+				$this->t->mustMatch(OP_RIGHT_PAREN);
+				$n->body = $this->nest($x, $n);
+			return $n;
+
+			case KEYWORD_WHILE:
+			        $n = new JSNode($this->t);
+			        $n->isLoop = true;
+			        $n->condition = $this->ParenExpression($x);
+			        $n->body = $this->nest($x, $n);
+			return $n;
+
+			case KEYWORD_DO:
+				$n = new JSNode($this->t);
+				$n->isLoop = true;
+				$n->body = $this->nest($x, $n, KEYWORD_WHILE);
+				$n->condition = $this->ParenExpression($x);
+				if (!$x->ecmaStrictMode)
+				{
+					// <script language="JavaScript"> (without version hints) may need
+					// automatic semicolon insertion without a newline after do-while.
+					// See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
+					$this->t->match(OP_SEMICOLON);
+					return $n;
+				}
+			break;
+
+			case KEYWORD_BREAK:
+			case KEYWORD_CONTINUE:
+				$n = new JSNode($this->t);
+
+				if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER)
+				{
+					$this->t->get();
+					$n->label = $this->t->currentToken()->value;
+				}
+
+				$ss = $x->stmtStack;
+				$i = count($ss);
+				$label = $n->label;
+				if ($label)
+				{
+					do
+					{
+						if (--$i < 0)
+							throw $this->t->newSyntaxError('Label not found');
+					}
+					while ($ss[$i]->label != $label);
+				}
+				else
+				{
+					do
+					{
+						if (--$i < 0)
+							throw $this->t->newSyntaxError('Invalid ' . $tt);
+					}
+					while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH));
+				}
+
+				$n->target = $ss[$i];
+			break;
+
+			case KEYWORD_TRY:
+				$n = new JSNode($this->t);
+				$n->tryBlock = $this->Block($x);
+				$n->catchClauses = array();
+
+				while ($this->t->match(KEYWORD_CATCH))
+				{
+					$n2 = new JSNode($this->t);
+					$this->t->mustMatch(OP_LEFT_PAREN);
+					$n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value;
+
+					if ($this->t->match(KEYWORD_IF))
+					{
+						if ($x->ecmaStrictMode)
+							throw $this->t->newSyntaxError('Illegal catch guard');
+
+						if (count($n->catchClauses) && !end($n->catchClauses)->guard)
+							throw $this->t->newSyntaxError('Guarded catch after unguarded');
+
+						$n2->guard = $this->Expression($x);
+					}
+					else
+					{
+						$n2->guard = null;
+					}
+
+					$this->t->mustMatch(OP_RIGHT_PAREN);
+					$n2->block = $this->Block($x);
+					array_push($n->catchClauses, $n2);
+				}
+
+				if ($this->t->match(KEYWORD_FINALLY))
+					$n->finallyBlock = $this->Block($x);
+
+				if (!count($n->catchClauses) && !$n->finallyBlock)
+					throw $this->t->newSyntaxError('Invalid try statement');
+			return $n;
+
+			case KEYWORD_CATCH:
+			case KEYWORD_FINALLY:
+				throw $this->t->newSyntaxError($tt + ' without preceding try');
+
+			case KEYWORD_THROW:
+				$n = new JSNode($this->t);
+				$n->exception = $this->Expression($x);
+			break;
+
+			case KEYWORD_RETURN:
+				if (!$x->inFunction)
+					throw $this->t->newSyntaxError('Invalid return');
+
+				$n = new JSNode($this->t);
+				$tt = $this->t->peekOnSameLine();
+				if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
+					$n->value = $this->Expression($x);
+				else
+					$n->value = null;
+			break;
+
+			case KEYWORD_WITH:
+				$n = new JSNode($this->t);
+				$n->object = $this->ParenExpression($x);
+				$n->body = $this->nest($x, $n);
+			return $n;
+
+			case KEYWORD_VAR:
+			case KEYWORD_CONST:
+			        $n = $this->Variables($x);
+			break;
+
+			case TOKEN_CONDCOMMENT_MULTILINE:
+				$n = new JSNode($this->t);
+			return $n;
+
+			case KEYWORD_DEBUGGER:
+				$n = new JSNode($this->t);
+			break;
+
+			case TOKEN_NEWLINE:
+			case OP_SEMICOLON:
+				$n = new JSNode($this->t, OP_SEMICOLON);
+				$n->expression = null;
+			return $n;
+
+			default:
+				if ($tt == TOKEN_IDENTIFIER)
+				{
+					$this->t->scanOperand = false;
+					$tt = $this->t->peek();
+					$this->t->scanOperand = true;
+					if ($tt == OP_COLON)
+					{
+						$label = $this->t->currentToken()->value;
+						$ss = $x->stmtStack;
+						for ($i = count($ss) - 1; $i >= 0; --$i)
+						{
+							if ($ss[$i]->label == $label)
+								throw $this->t->newSyntaxError('Duplicate label');
+						}
+
+						$this->t->get();
+						$n = new JSNode($this->t, JS_LABEL);
+						$n->label = $label;
+						$n->statement = $this->nest($x, $n);
+
+						return $n;
+					}
+				}
+
+				$n = new JSNode($this->t, OP_SEMICOLON);
+				$this->t->unget();
+				$n->expression = $this->Expression($x);
+				$n->end = $n->expression->end;
+			break;
+		}
+
+		if ($this->t->lineno == $this->t->currentToken()->lineno)
+		{
+			$tt = $this->t->peekOnSameLine();
+			if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
+				throw $this->t->newSyntaxError('Missing ; before statement');
+		}
+
+		$this->t->match(OP_SEMICOLON);
+
+		return $n;
+	}
+
+	private function FunctionDefinition($x, $requireName, $functionForm)
+	{
+		$f = new JSNode($this->t);
+
+		if ($f->type != KEYWORD_FUNCTION)
+			$f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER;
+
+		if ($this->t->match(TOKEN_IDENTIFIER))
+			$f->name = $this->t->currentToken()->value;
+		elseif ($requireName)
+			throw $this->t->newSyntaxError('Missing function identifier');
+
+		$this->t->mustMatch(OP_LEFT_PAREN);
+			$f->params = array();
+
+		while (($tt = $this->t->get()) != OP_RIGHT_PAREN)
+		{
+			if ($tt != TOKEN_IDENTIFIER)
+				throw $this->t->newSyntaxError('Missing formal parameter');
+
+			array_push($f->params, $this->t->currentToken()->value);
+
+			if ($this->t->peek() != OP_RIGHT_PAREN)
+				$this->t->mustMatch(OP_COMMA);
+		}
+
+		$this->t->mustMatch(OP_LEFT_CURLY);
+
+		$x2 = new JSCompilerContext(true);
+		$f->body = $this->Script($x2);
+
+		$this->t->mustMatch(OP_RIGHT_CURLY);
+		$f->end = $this->t->currentToken()->end;
+
+		$f->functionForm = $functionForm;
+		if ($functionForm == DECLARED_FORM)
+			array_push($x->funDecls, $f);
+
+		return $f;
+	}
+
+	private function Variables($x)
+	{
+		$n = new JSNode($this->t);
+
+		do
+		{
+			$this->t->mustMatch(TOKEN_IDENTIFIER);
+
+			$n2 = new JSNode($this->t);
+			$n2->name = $n2->value;
+
+			if ($this->t->match(OP_ASSIGN))
+			{
+				if ($this->t->currentToken()->assignOp)
+					throw $this->t->newSyntaxError('Invalid variable initialization');
+
+				$n2->initializer = $this->Expression($x, OP_COMMA);
+			}
+
+			$n2->readOnly = $n->type == KEYWORD_CONST;
+
+			$n->addNode($n2);
+			array_push($x->varDecls, $n2);
+		}
+		while ($this->t->match(OP_COMMA));
+
+		return $n;
+	}
+
+	private function Expression($x, $stop=false)
+	{
+		$operators = array();
+		$operands = array();
+		$n = false;
+
+		$bl = $x->bracketLevel;
+		$cl = $x->curlyLevel;
+		$pl = $x->parenLevel;
+		$hl = $x->hookLevel;
+
+		while (($tt = $this->t->get()) != TOKEN_END)
+		{
+			if ($tt == $stop &&
+				$x->bracketLevel == $bl &&
+				$x->curlyLevel == $cl &&
+				$x->parenLevel == $pl &&
+				$x->hookLevel == $hl
+			)
+			{
+				// Stop only if tt matches the optional stop parameter, and that
+				// token is not quoted by some kind of bracket.
+				break;
+			}
+
+			switch ($tt)
+			{
+				case OP_SEMICOLON:
+					// NB: cannot be empty, Statement handled that.
+					break 2;
+
+				case OP_ASSIGN:
+				case OP_HOOK:
+				case OP_COLON:
+					if ($this->t->scanOperand)
+						break 2;
+
+					// Use >, not >=, for right-associative ASSIGN and HOOK/COLON.
+					while (	!empty($operators) &&
+						(	$this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] ||
+							($tt == OP_COLON && end($operators)->type == OP_ASSIGN)
+						)
+					)
+						$this->reduce($operators, $operands);
+
+					if ($tt == OP_COLON)
+					{
+						$n = end($operators);
+						if ($n->type != OP_HOOK)
+							throw $this->t->newSyntaxError('Invalid label');
+
+						--$x->hookLevel;
+					}
+					else
+					{
+						array_push($operators, new JSNode($this->t));
+						if ($tt == OP_ASSIGN)
+							end($operands)->assignOp = $this->t->currentToken()->assignOp;
+						else
+							++$x->hookLevel;
+					}
+
+					$this->t->scanOperand = true;
+				break;
+
+				case KEYWORD_IN:
+					// An in operator should not be parsed if we're parsing the head of
+					// a for (...) loop, unless it is in the then part of a conditional
+					// expression, or parenthesized somehow.
+					if ($x->inForLoopInit && !$x->hookLevel &&
+						!$x->bracketLevel && !$x->curlyLevel &&
+						!$x->parenLevel
+					)
+					{
+						break 2;
+					}
+				// FALL THROUGH
+				case OP_COMMA:
+					// Treat comma as left-associative so reduce can fold left-heavy
+					// COMMA trees into a single array.
+					// FALL THROUGH
+				case OP_OR:
+				case OP_AND:
+				case OP_BITWISE_OR:
+				case OP_BITWISE_XOR:
+				case OP_BITWISE_AND:
+				case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
+				case OP_LT: case OP_LE: case OP_GE: case OP_GT:
+				case KEYWORD_INSTANCEOF:
+				case OP_LSH: case OP_RSH: case OP_URSH:
+				case OP_PLUS: case OP_MINUS:
+				case OP_MUL: case OP_DIV: case OP_MOD:
+				case OP_DOT:
+					if ($this->t->scanOperand)
+						break 2;
+
+					while (	!empty($operators) &&
+						$this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt]
+					)
+						$this->reduce($operators, $operands);
+
+					if ($tt == OP_DOT)
+					{
+						$this->t->mustMatch(TOKEN_IDENTIFIER);
+						array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
+					}
+					else
+					{
+						array_push($operators, new JSNode($this->t));
+						$this->t->scanOperand = true;
+					}
+				break;
+
+				case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF:
+				case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS:
+				case KEYWORD_NEW:
+					if (!$this->t->scanOperand)
+						break 2;
+
+					array_push($operators, new JSNode($this->t));
+				break;
+
+				case OP_INCREMENT: case OP_DECREMENT:
+					if ($this->t->scanOperand)
+					{
+						array_push($operators, new JSNode($this->t));  // prefix increment or decrement
+					}
+					else
+					{
+						// Don't cross a line boundary for postfix {in,de}crement.
+						$t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
+						if ($t && $t->lineno != $this->t->lineno)
+							break 2;
+
+						if (!empty($operators))
+						{
+							// Use >, not >=, so postfix has higher precedence than prefix.
+							while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt])
+								$this->reduce($operators, $operands);
+						}
+
+						$n = new JSNode($this->t, $tt, array_pop($operands));
+						$n->postfix = true;
+						array_push($operands, $n);
+					}
+				break;
+
+				case KEYWORD_FUNCTION:
+					if (!$this->t->scanOperand)
+						break 2;
+
+					array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
+					$this->t->scanOperand = false;
+				break;
+
+				case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
+				case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
+					if (!$this->t->scanOperand)
+						break 2;
+
+					array_push($operands, new JSNode($this->t));
+					$this->t->scanOperand = false;
+				break;
+
+				case TOKEN_CONDCOMMENT_MULTILINE:
+					if ($this->t->scanOperand)
+						array_push($operators, new JSNode($this->t));
+					else
+						array_push($operands, new JSNode($this->t));
+				break;
+
+				case OP_LEFT_BRACKET:
+					if ($this->t->scanOperand)
+					{
+						// Array initialiser.  Parse using recursive descent, as the
+						// sub-grammar here is not an operator grammar.
+						$n = new JSNode($this->t, JS_ARRAY_INIT);
+						while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET)
+						{
+							if ($tt == OP_COMMA)
+							{
+								$this->t->get();
+								$n->addNode(null);
+								continue;
+							}
+
+							$n->addNode($this->Expression($x, OP_COMMA));
+							if (!$this->t->match(OP_COMMA))
+								break;
+						}
+
+						$this->t->mustMatch(OP_RIGHT_BRACKET);
+						array_push($operands, $n);
+						$this->t->scanOperand = false;
+					}
+					else
+					{
+						// Property indexing operator.
+						array_push($operators, new JSNode($this->t, JS_INDEX));
+						$this->t->scanOperand = true;
+						++$x->bracketLevel;
+					}
+				break;
+
+				case OP_RIGHT_BRACKET:
+					if ($this->t->scanOperand || $x->bracketLevel == $bl)
+						break 2;
+
+					while ($this->reduce($operators, $operands)->type != JS_INDEX)
+						continue;
+
+					--$x->bracketLevel;
+				break;
+
+				case OP_LEFT_CURLY:
+					if (!$this->t->scanOperand)
+						break 2;
+
+					// Object initialiser.  As for array initialisers (see above),
+					// parse using recursive descent.
+					++$x->curlyLevel;
+					$n = new JSNode($this->t, JS_OBJECT_INIT);
+					while (!$this->t->match(OP_RIGHT_CURLY))
+					{
+						do
+						{
+							$tt = $this->t->get();
+							$tv = $this->t->currentToken()->value;
+							if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER)
+							{
+								if ($x->ecmaStrictMode)
+									throw $this->t->newSyntaxError('Illegal property accessor');
+
+								$n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
+							}
+							else
+							{
+								switch ($tt)
+								{
+									case TOKEN_IDENTIFIER:
+									case TOKEN_NUMBER:
+									case TOKEN_STRING:
+										$id = new JSNode($this->t);
+									break;
+
+									case OP_RIGHT_CURLY:
+										if ($x->ecmaStrictMode)
+											throw $this->t->newSyntaxError('Illegal trailing ,');
+									break 3;
+
+									default:
+										throw $this->t->newSyntaxError('Invalid property name');
+								}
+
+								$this->t->mustMatch(OP_COLON);
+								$n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
+							}
+						}
+						while ($this->t->match(OP_COMMA));
+
+						$this->t->mustMatch(OP_RIGHT_CURLY);
+						break;
+					}
+
+					array_push($operands, $n);
+					$this->t->scanOperand = false;
+					--$x->curlyLevel;
+				break;
+
+				case OP_RIGHT_CURLY:
+					if (!$this->t->scanOperand && $x->curlyLevel != $cl)
+						throw new Exception('PANIC: right curly botch');
+				break 2;
+
+				case OP_LEFT_PAREN:
+					if ($this->t->scanOperand)
+					{
+						array_push($operators, new JSNode($this->t, JS_GROUP));
+					}
+					else
+					{
+						while (	!empty($operators) &&
+							$this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
+						)
+							$this->reduce($operators, $operands);
+
+						// Handle () now, to regularize the n-ary case for n > 0.
+						// We must set scanOperand in case there are arguments and
+						// the first one is a regexp or unary+/-.
+						$n = end($operators);
+						$this->t->scanOperand = true;
+						if ($this->t->match(OP_RIGHT_PAREN))
+						{
+							if ($n && $n->type == KEYWORD_NEW)
+							{
+								array_pop($operators);
+								$n->addNode(array_pop($operands));
+							}
+							else
+							{
+								$n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
+							}
+
+							array_push($operands, $n);
+							$this->t->scanOperand = false;
+							break;
+						}
+
+						if ($n && $n->type == KEYWORD_NEW)
+							$n->type = JS_NEW_WITH_ARGS;
+						else
+							array_push($operators, new JSNode($this->t, JS_CALL));
+					}
+
+					++$x->parenLevel;
+				break;
+
+				case OP_RIGHT_PAREN:
+					if ($this->t->scanOperand || $x->parenLevel == $pl)
+						break 2;
+
+					while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
+						$tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
+					)
+					{
+						continue;
+					}
+
+					if ($tt != JS_GROUP)
+					{
+						$n = end($operands);
+						if ($n->treeNodes[1]->type != OP_COMMA)
+							$n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
+						else
+							$n->treeNodes[1]->type = JS_LIST;
+					}
+
+					--$x->parenLevel;
+				break;
+
+				// Automatic semicolon insertion means we may scan across a newline
+				// and into the beginning of another statement.  If so, break out of
+				// the while loop and let the t.scanOperand logic handle errors.
+				default:
+					break 2;
+			}
+		}
+
+		if ($x->hookLevel != $hl)
+			throw $this->t->newSyntaxError('Missing : after ?');
+
+		if ($x->parenLevel != $pl)
+			throw $this->t->newSyntaxError('Missing ) in parenthetical');
+
+		if ($x->bracketLevel != $bl)
+			throw $this->t->newSyntaxError('Missing ] in index expression');
+
+		if ($this->t->scanOperand)
+			throw $this->t->newSyntaxError('Missing operand');
+
+		// Resume default mode, scanning for operands, not operators.
+		$this->t->scanOperand = true;
+		$this->t->unget();
+
+		while (count($operators))
+			$this->reduce($operators, $operands);
+
+		return array_pop($operands);
+	}
+
+	private function ParenExpression($x)
+	{
+		$this->t->mustMatch(OP_LEFT_PAREN);
+		$n = $this->Expression($x);
+		$this->t->mustMatch(OP_RIGHT_PAREN);
+
+		return $n;
+	}
+
+	// Statement stack and nested statement handler.
+	private function nest($x, $node, $end = false)
+	{
+		array_push($x->stmtStack, $node);
+		$n = $this->statement($x);
+		array_pop($x->stmtStack);
+
+		if ($end)
+			$this->t->mustMatch($end);
+
+		return $n;
+	}
+
+	private function reduce(&$operators, &$operands)
+	{
+		$n = array_pop($operators);
+		$op = $n->type;
+		$arity = $this->opArity[$op];
+		$c = count($operands);
+		if ($arity == -2)
+		{
+			// Flatten left-associative trees
+			if ($c >= 2)
+			{
+				$left = $operands[$c - 2];
+				if ($left->type == $op)
+				{
+					$right = array_pop($operands);
+					$left->addNode($right);
+					return $left;
+				}
+			}
+			$arity = 2;
+		}
+
+		// Always use push to add operands to n, to update start and end
+		$a = array_splice($operands, $c - $arity);
+		for ($i = 0; $i < $arity; $i++)
+			$n->addNode($a[$i]);
+
+		// Include closing bracket or postfix operator in [start,end]
+		$te = $this->t->currentToken()->end;
+		if ($n->end < $te)
+			$n->end = $te;
+
+		array_push($operands, $n);
+
+		return $n;
+	}
+}
+
+class JSCompilerContext
+{
+	public $inFunction = false;
+	public $inForLoopInit = false;
+	public $ecmaStrictMode = false;
+	public $bracketLevel = 0;
+	public $curlyLevel = 0;
+	public $parenLevel = 0;
+	public $hookLevel = 0;
+
+	public $stmtStack = array();
+	public $funDecls = array();
+	public $varDecls = array();
+
+	public function __construct($inFunction)
+	{
+		$this->inFunction = $inFunction;
+	}
+}
+
+class JSNode
+{
+	private $type;
+	private $value;
+	private $lineno;
+	private $start;
+	private $end;
+
+	public $treeNodes = array();
+	public $funDecls = array();
+	public $varDecls = array();
+
+	public function __construct($t, $type=0)
+	{
+		if ($token = $t->currentToken())
+		{
+			$this->type = $type ? $type : $token->type;
+			$this->value = $token->value;
+			$this->lineno = $token->lineno;
+			$this->start = $token->start;
+			$this->end = $token->end;
+		}
+		else
+		{
+			$this->type = $type;
+			$this->lineno = $t->lineno;
+		}
+
+		if (($numargs = func_num_args()) > 2)
+		{
+			$args = func_get_args();;
+			for ($i = 2; $i < $numargs; $i++)
+				$this->addNode($args[$i]);
+		}
+	}
+
+	// we don't want to bloat our object with all kind of specific properties, so we use overloading
+	public function __set($name, $value)
+	{
+		$this->$name = $value;
+	}
+
+	public function __get($name)
+	{
+		if (isset($this->$name))
+			return $this->$name;
+
+		return null;
+	}
+
+	public function addNode($node)
+	{
+		$this->treeNodes[] = $node;
+	}
+}
+
+class JSTokenizer
+{
+	private $cursor = 0;
+	private $source;
+
+	public $tokens = array();
+	public $tokenIndex = 0;
+	public $lookahead = 0;
+	public $scanNewlines = false;
+	public $scanOperand = true;
+
+	public $filename;
+	public $lineno;
+
+	private $keywords = array(
+		'break',
+		'case', 'catch', 'const', 'continue',
+		'debugger', 'default', 'delete', 'do',
+		'else', 'enum',
+		'false', 'finally', 'for', 'function',
+		'if', 'in', 'instanceof',
+		'new', 'null',
+		'return',
+		'switch',
+		'this', 'throw', 'true', 'try', 'typeof',
+		'var', 'void',
+		'while', 'with'
+	);
+
+	private $opTypeNames = array(
+		';'	=> 'SEMICOLON',
+		','	=> 'COMMA',
+		'?'	=> 'HOOK',
+		':'	=> 'COLON',
+		'||'	=> 'OR',
+		'&&'	=> 'AND',
+		'|'	=> 'BITWISE_OR',
+		'^'	=> 'BITWISE_XOR',
+		'&'	=> 'BITWISE_AND',
+		'==='	=> 'STRICT_EQ',
+		'=='	=> 'EQ',
+		'='	=> 'ASSIGN',
+		'!=='	=> 'STRICT_NE',
+		'!='	=> 'NE',
+		'<<'	=> 'LSH',
+		'<='	=> 'LE',
+		'<'	=> 'LT',
+		'>>>'	=> 'URSH',
+		'>>'	=> 'RSH',
+		'>='	=> 'GE',
+		'>'	=> 'GT',
+		'++'	=> 'INCREMENT',
+		'--'	=> 'DECREMENT',
+		'+'	=> 'PLUS',
+		'-'	=> 'MINUS',
+		'*'	=> 'MUL',
+		'/'	=> 'DIV',
+		'%'	=> 'MOD',
+		'!'	=> 'NOT',
+		'~'	=> 'BITWISE_NOT',
+		'.'	=> 'DOT',
+		'['	=> 'LEFT_BRACKET',
+		']'	=> 'RIGHT_BRACKET',
+		'{'	=> 'LEFT_CURLY',
+		'}'	=> 'RIGHT_CURLY',
+		'('	=> 'LEFT_PAREN',
+		')'	=> 'RIGHT_PAREN',
+		'@*/'	=> 'CONDCOMMENT_END'
+	);
+
+	private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
+	private $opRegExp;
+
+	public function __construct()
+	{
+		$this->opRegExp = '#^(' . implode('|', array_map('preg_quote', array_keys($this->opTypeNames))) . ')#';
+
+		// this is quite a hidden yet convenient place to create the defines for operators and keywords
+		foreach ($this->opTypeNames as $operand => $name)
+			define('OP_' . $name, $operand);
+
+		define('OP_UNARY_PLUS', 'U+');
+		define('OP_UNARY_MINUS', 'U-');
+
+		foreach ($this->keywords as $keyword)
+			define('KEYWORD_' . strtoupper($keyword), $keyword);
+	}
+
+	public function init($source, $filename = '', $lineno = 1)
+	{
+		$this->source = $source;
+		$this->filename = $filename ? $filename : '[inline]';
+		$this->lineno = $lineno;
+
+		$this->cursor = 0;
+		$this->tokens = array();
+		$this->tokenIndex = 0;
+		$this->lookahead = 0;
+		$this->scanNewlines = false;
+		$this->scanOperand = true;
+	}
+
+	public function getInput($chunksize)
+	{
+		if ($chunksize)
+			return substr($this->source, $this->cursor, $chunksize);
+
+		return substr($this->source, $this->cursor);
+	}
+
+	public function isDone()
+	{
+		return $this->peek() == TOKEN_END;
+	}
+
+	public function match($tt)
+	{
+		return $this->get() == $tt || $this->unget();
+	}
+
+	public function mustMatch($tt)
+	{
+	        if (!$this->match($tt))
+			throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
+
+		return $this->currentToken();
+	}
+
+	public function peek()
+	{
+		if ($this->lookahead)
+		{
+			$next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
+			if ($this->scanNewlines && $next->lineno != $this->lineno)
+				$tt = TOKEN_NEWLINE;
+			else
+				$tt = $next->type;
+		}
+		else
+		{
+			$tt = $this->get();
+			$this->unget();
+		}
+
+		return $tt;
+	}
+
+	public function peekOnSameLine()
+	{
+		$this->scanNewlines = true;
+		$tt = $this->peek();
+		$this->scanNewlines = false;
+
+		return $tt;
+	}
+
+	public function currentToken()
+	{
+		if (!empty($this->tokens))
+			return $this->tokens[$this->tokenIndex];
+	}
+
+	public function get($chunksize = 1000)
+	{
+		while($this->lookahead)
+		{
+			$this->lookahead--;
+			$this->tokenIndex = ($this->tokenIndex + 1) & 3;
+			$token = $this->tokens[$this->tokenIndex];
+			if ($token->type != TOKEN_NEWLINE || $this->scanNewlines)
+				return $token->type;
+		}
+
+		$conditional_comment = false;
+
+		// strip whitespace and comments
+		while(true)
+		{
+			$input = $this->getInput($chunksize);
+
+			// whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
+			$re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
+			if (preg_match($re, $input, $match))
+			{
+				$spaces = $match[0];
+				$spacelen = strlen($spaces);
+				$this->cursor += $spacelen;
+				if (!$this->scanNewlines)
+					$this->lineno += substr_count($spaces, "\n");
+
+				if ($spacelen == $chunksize)
+					continue; // complete chunk contained whitespace
+
+				$input = $this->getInput($chunksize);
+				if ($input == '' || $input[0] != '/')
+					break;
+			}
+
+			// Comments
+			if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?(?:.|\n)*?\*\/|\/.*)/', $input, $match))
+			{
+				if (!$chunksize)
+					break;
+
+				// retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
+				$chunksize = null;
+				continue;
+			}
+
+			// check if this is a conditional (JScript) comment
+			if (!empty($match[1]))
+			{
+				//$match[0] = '/*' . $match[1];
+				$conditional_comment = true;
+				break;
+			}
+			else
+			{
+				$this->cursor += strlen($match[0]);
+				$this->lineno += substr_count($match[0], "\n");
+			}
+		}
+
+		if ($input == '')
+		{
+			$tt = TOKEN_END;
+			$match = array('');
+		}
+		elseif ($conditional_comment)
+		{
+			$tt = TOKEN_CONDCOMMENT_MULTILINE;
+		}
+		else
+		{
+			switch ($input[0])
+			{
+				case '0': case '1': case '2': case '3': case '4':
+				case '5': case '6': case '7': case '8': case '9':
+					if (preg_match('/^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+/', $input, $match))
+					{
+						$tt = TOKEN_NUMBER;
+					}
+					elseif (preg_match('/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/', $input, $match))
+					{
+						// this should always match because of \d+
+						$tt = TOKEN_NUMBER;
+					}
+				break;
+
+				case '"':
+				case "'":
+					if (preg_match('/^"(?:\\\\(?:.|\r?\n)|[^\\\\"\r\n])*"|^\'(?:\\\\(?:.|\r?\n)|[^\\\\\'\r\n])*\'/', $input, $match))
+					{
+						$tt = TOKEN_STRING;
+					}
+					else
+					{
+						if ($chunksize)
+							return $this->get(null); // retry with a full chunk fetch
+
+						throw $this->newSyntaxError('Unterminated string literal');
+					}
+				break;
+
+				case '/':
+					if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
+					{
+						$tt = TOKEN_REGEXP;
+						break;
+					}
+				// fall through
+
+				case '|':
+				case '^':
+				case '&':
+				case '<':
+				case '>':
+				case '+':
+				case '-':
+				case '*':
+				case '%':
+				case '=':
+				case '!':
+					// should always match
+					preg_match($this->opRegExp, $input, $match);
+					$op = $match[0];
+					if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=')
+					{
+						$tt = OP_ASSIGN;
+						$match[0] .= '=';
+					}
+					else
+					{
+						$tt = $op;
+						if ($this->scanOperand)
+						{
+							if ($op == OP_PLUS)
+								$tt = OP_UNARY_PLUS;
+							elseif ($op == OP_MINUS)
+								$tt = OP_UNARY_MINUS;
+						}
+						$op = null;
+					}
+				break;
+
+				case '.':
+					if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
+					{
+						$tt = TOKEN_NUMBER;
+						break;
+					}
+				// fall through
+
+				case ';':
+				case ',':
+				case '?':
+				case ':':
+				case '~':
+				case '[':
+				case ']':
+				case '{':
+				case '}':
+				case '(':
+				case ')':
+					// these are all single
+					$match = array($input[0]);
+					$tt = $input[0];
+				break;
+
+				case '@':
+					throw $this->newSyntaxError('Illegal token');
+				break;
+
+				case "\n":
+					if ($this->scanNewlines)
+					{
+						$match = array("\n");
+						$tt = TOKEN_NEWLINE;
+					}
+					else
+						throw $this->newSyntaxError('Illegal token');
+				break;
+
+				default:
+					// FIXME: add support for unicode and unicode escape sequence \uHHHH
+					if (preg_match('/^[$\w]+/', $input, $match))
+					{
+						$tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
+					}
+					else
+						throw $this->newSyntaxError('Illegal token');
+			}
+		}
+
+		$this->tokenIndex = ($this->tokenIndex + 1) & 3;
+
+		if (!isset($this->tokens[$this->tokenIndex]))
+			$this->tokens[$this->tokenIndex] = new JSToken();
+
+		$token = $this->tokens[$this->tokenIndex];
+		$token->type = $tt;
+
+		if ($tt == OP_ASSIGN)
+			$token->assignOp = $op;
+
+		$token->start = $this->cursor;
+
+		$token->value = $match[0];
+		$this->cursor += strlen($match[0]);
+
+		$token->end = $this->cursor;
+		$token->lineno = $this->lineno;
+
+		return $tt;
+	}
+
+	public function unget()
+	{
+		if (++$this->lookahead == 4)
+			throw $this->newSyntaxError('PANIC: too much lookahead!');
+
+		$this->tokenIndex = ($this->tokenIndex - 1) & 3;
+	}
+
+	public function newSyntaxError($m)
+	{
+		return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);
+	}
+}
+
+class JSToken
+{
+	public $type;
+	public $value;
+	public $start;
+	public $end;
+	public $lineno;
+	public $assignOp;
+}
+
+?>
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/ImportProcessor.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/ImportProcessor.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/ImportProcessor.php	(revision 1957)
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Class Minify_ImportProcessor  
+ * @package Minify
+ */
+
+/**
+ * Linearize a CSS/JS file by including content specified by CSS import
+ * declarations. In CSS files, relative URIs are fixed.
+ * 
+ * @imports will be processed regardless of where they appear in the source 
+ * files; i.e. @imports commented out or in string content will still be
+ * processed!
+ * 
+ * This has a unit test but should be considered "experimental".
+ *
+ * @package Minify
+ * @author Stephen Clay <steve@mrclay.org>
+ */
+class Minify_ImportProcessor {
+    
+    public static $filesIncluded = array();
+    
+    public static function process($file)
+    {
+        self::$filesIncluded = array();
+        self::$_isCss = (strtolower(substr($file, -4)) === '.css');
+        $obj = new Minify_ImportProcessor(dirname($file));
+        return $obj->_getContent($file);
+    }
+    
+    // allows callback funcs to know the current directory
+    private $_currentDir = null;
+    
+    // allows _importCB to write the fetched content back to the obj
+    private $_importedContent = '';
+    
+    private static $_isCss = null;
+    
+    private function __construct($currentDir)
+    {
+        $this->_currentDir = $currentDir;
+    }
+    
+    private function _getContent($file)
+    {
+        $file = realpath($file);
+        if (! $file
+            || in_array($file, self::$filesIncluded)
+            || false === ($content = @file_get_contents($file))
+        ) {
+            // file missing, already included, or failed read
+            return '';
+        }
+        self::$filesIncluded[] = realpath($file);
+        $this->_currentDir = dirname($file);
+        
+        // remove UTF-8 BOM if present
+        if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {
+            $content = substr($content, 3);
+        }
+        // ensure uniform EOLs
+        $content = str_replace("\r\n", "\n", $content);
+        
+        // process @imports
+        $content = preg_replace_callback(
+            '/
+                @import\\s+
+                (?:url\\(\\s*)?      # maybe url(
+                [\'"]?               # maybe quote
+                (.*?)                # 1 = URI
+                [\'"]?               # maybe end quote
+                (?:\\s*\\))?         # maybe )
+                ([a-zA-Z,\\s]*)?     # 2 = media list
+                ;                    # end token
+            /x'
+            ,array($this, '_importCB')
+            ,$content
+        );
+        
+        if (self::$_isCss) {
+            // rewrite remaining relative URIs
+            $content = preg_replace_callback(
+                '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
+                ,array($this, '_urlCB')
+                ,$content
+            );
+        }
+        
+        return $this->_importedContent . $content;
+    }
+    
+    private function _importCB($m)
+    {
+        $url = $m[1];
+        $mediaList = preg_replace('/\\s+/', '', $m[2]);
+        
+        if (strpos($url, '://') > 0) {
+            // protocol, leave in place for CSS, comment for JS
+            return self::$_isCss
+                ? $m[0]
+                : "/* Minify_ImportProcessor will not include remote content */";
+        }
+        if ('/' === $url[0]) {
+            // protocol-relative or root path
+            $url = ltrim($url, '/');
+            $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
+                . strtr($url, '/', DIRECTORY_SEPARATOR);
+        } else {
+            // relative to current path
+            $file = $this->_currentDir . DIRECTORY_SEPARATOR 
+                . strtr($url, '/', DIRECTORY_SEPARATOR);
+        }
+        $obj = new Minify_ImportProcessor(dirname($file));
+        $content = $obj->_getContent($file);
+        if ('' === $content) {
+            // failed. leave in place for CSS, comment for JS
+            return self::$_isCss
+                ? $m[0]
+                : "/* Minify_ImportProcessor could not fetch '{$file}' */";;
+        }
+        return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))
+            ? $content
+            : "@media {$mediaList} {\n{$content}\n}\n";
+    }
+    
+    private function _urlCB($m)
+    {
+        // $m[1] is either quoted or not
+        $quote = ($m[1][0] === "'" || $m[1][0] === '"')
+            ? $m[1][0]
+            : '';
+        $url = ($quote === '')
+            ? $m[1]
+            : substr($m[1], 1, strlen($m[1]) - 2);
+        if ('/' !== $url[0]) {
+            if (strpos($url, '//') > 0) {
+                // probably starts with protocol, do not alter
+            } else {
+                // prepend path with current dir separator (OS-independent)
+                $path = $this->_currentDir 
+                    . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);
+                // strip doc root
+                $path = substr($path, strlen(realpath($_SERVER['DOCUMENT_ROOT'])));
+                // fix to absolute URL
+                $url = strtr($path, '/\\', '//');
+                // remove /./ and /../ where possible
+                $url = str_replace('/./', '/', $url);
+                // inspired by patch from Oleg Cherniy
+                do {
+                    $url = preg_replace('@/[^/]+/\\.\\./@', '/', $url, 1, $changed);
+                } while ($changed);
+            }
+        }
+        return "url({$quote}{$url}{$quote})";
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Lines.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Lines.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Lines.php	(revision 1957)
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Class Minify_Lines  
+ * @package Minify
+ */
+
+/**
+ * Add line numbers in C-style comments for easier debugging of combined content
+ *
+ * @package Minify
+ * @author Stephen Clay <steve@mrclay.org>
+ * @author Adam Pedersen (Issue 55 fix)
+ */
+class Minify_Lines {
+
+    /**
+     * Add line numbers in C-style comments
+     *
+     * This uses a very basic parser easily fooled by comment tokens inside
+     * strings or regexes, but, otherwise, generally clean code will not be 
+     * mangled. URI rewriting can also be performed.
+     *
+     * @param string $content
+     * 
+     * @param array $options available options:
+     * 
+     * 'id': (optional) string to identify file. E.g. file name/path
+     *
+     * 'currentDir': (default null) if given, this is assumed to be the
+     * directory of the current CSS file. Using this, minify will rewrite
+     * all relative URIs in import/url declarations to correctly point to
+     * the desired files, and prepend a comment with debugging information about
+     * this process.
+     * 
+     * @return string 
+     */
+    public static function minify($content, $options = array()) 
+    {
+        $id = (isset($options['id']) && $options['id'])
+            ? $options['id']
+            : '';
+        $content = str_replace("\r\n", "\n", $content);
+        $lines = explode("\n", $content);
+        $numLines = count($lines);
+        // determine left padding
+        $padTo = strlen($numLines);
+        $inComment = false;
+        $i = 0;
+        $newLines = array();
+        while (null !== ($line = array_shift($lines))) {
+            if (('' !== $id) && (0 == $i % 50)) {
+                array_push($newLines, '', "/* {$id} */", '');
+            }
+            ++$i;
+            $newLines[] = self::_addNote($line, $i, $inComment, $padTo);
+            $inComment = self::_eolInComment($line, $inComment);
+        }
+        $content = implode("\n", $newLines) . "\n";
+        
+        // check for desired URI rewriting
+        if (isset($options['currentDir'])) {
+            #require_once 'Minify/CSS/UriRewriter.php';
+            Minify_CSS_UriRewriter::$debugText = '';
+            $content = Minify_CSS_UriRewriter::rewrite(
+                 $content
+                ,$options['currentDir']
+                ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT']
+                ,isset($options['symlinks']) ? $options['symlinks'] : array()
+            );
+            $content = "/* Minify_CSS_UriRewriter::\$debugText\n\n" 
+                     . Minify_CSS_UriRewriter::$debugText . "*/\n"
+                     . $content;
+        }
+        
+        return $content;
+    }
+    
+    /**
+     * Is the parser within a C-style comment at the end of this line?
+     *
+     * @param string $line current line of code
+     * 
+     * @param bool $inComment was the parser in a comment at the
+     * beginning of the line?
+     * 
+     * @return bool
+     */
+    private static function _eolInComment($line, $inComment)
+    {
+        while (strlen($line)) {
+            $search = $inComment
+                ? '*/'
+                : '/*';
+            $pos = strpos($line, $search);
+            if (false === $pos) {
+                return $inComment;
+            } else {
+                if ($pos == 0
+                    || ($inComment
+                        ? substr($line, $pos, 3)
+                        : substr($line, $pos-1, 3)) != '*/*')
+                {
+                        $inComment = ! $inComment;
+                }
+                $line = substr($line, $pos + 2);
+            }
+        }
+        return $inComment;
+    }
+    
+    /**
+     * Prepend a comment (or note) to the given line
+     *
+     * @param string $line current line of code
+     *
+     * @param string $note content of note/comment
+     * 
+     * @param bool $inComment was the parser in a comment at the
+     * beginning of the line?
+     *
+     * @param int $padTo minimum width of comment
+     * 
+     * @return string
+     */
+    private static function _addNote($line, $note, $inComment, $padTo)
+    {
+        return $inComment
+            ? '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' *| ' . $line
+            : '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' */ ' . $line;
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/File.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/File.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/File.php	(revision 1957)
@@ -0,0 +1,125 @@
+<?php
+/**
+ * Class Minify_Cache_File  
+ * @package Minify
+ */
+
+class Minify_Cache_File {
+    
+    public function __construct($path = '', $fileLocking = false)
+    {
+        if (! $path) {
+            #require_once 'Solar/Dir.php';
+            $path = rtrim(Solar_Dir::tmp(), DIRECTORY_SEPARATOR);
+        }
+        $this->_locking = $fileLocking;
+        $this->_path = $path;
+    }
+    
+    /**
+     * Write data to cache.
+     *
+     * @param string $id cache id (e.g. a filename)
+     * 
+     * @param string $data
+     * 
+     * @return bool success
+     */
+    public function store($id, $data)
+    {
+        $flag = $this->_locking
+            ? LOCK_EX
+            : null;
+        if (is_file($this->_path . '/' . $id)) {
+            @unlink($this->_path . '/' . $id);
+        }
+        if (! @file_put_contents($this->_path . '/' . $id, $data, $flag)) {
+            return false;
+        }
+        // write control
+        if ($data !== $this->fetch($id)) {
+            @unlink($file);
+            return false;
+        }
+        return true;
+    }
+    
+    /**
+     * Get the size of a cache entry
+     *
+     * @param string $id cache id (e.g. a filename)
+     * 
+     * @return int size in bytes
+     */
+    public function getSize($id)
+    {
+        return filesize($this->_path . '/' . $id);
+    }
+    
+    /**
+     * Does a valid cache entry exist?
+     *
+     * @param string $id cache id (e.g. a filename)
+     * 
+     * @param int $srcMtime mtime of the original source file(s)
+     * 
+     * @return bool exists
+     */
+    public function isValid($id, $srcMtime)
+    {
+        $file = $this->_path . '/' . $id;
+        return (is_file($file) && (filemtime($file) >= $srcMtime));
+    }
+    
+    /**
+     * Send the cached content to output
+     *
+     * @param string $id cache id (e.g. a filename)
+     */
+    public function display($id)
+    {
+        if ($this->_locking) {
+            $fp = fopen($this->_path . '/' . $id, 'rb');
+            flock($fp, LOCK_SH);
+            fpassthru($fp);
+            flock($fp, LOCK_UN);
+            fclose($fp);
+        } else {
+            readfile($this->_path . '/' . $id);            
+        }
+    }
+    
+	/**
+     * Fetch the cached content
+     *
+     * @param string $id cache id (e.g. a filename)
+     * 
+     * @return string
+     */
+    public function fetch($id)
+    {
+        if ($this->_locking) {
+            $fp = fopen($this->_path . '/' . $id, 'rb');
+            flock($fp, LOCK_SH);
+            $ret = stream_get_contents($fp);
+            flock($fp, LOCK_UN);
+            fclose($fp);
+            return $ret;
+        } else {
+            return file_get_contents($this->_path . '/' . $id);
+        }
+    }
+    
+    /**
+     * Fetch the cache path used
+     *
+     * @return string
+     */
+    public function getPath()
+    {
+        return $this->_path;
+    }
+    
+    private $_path = null;
+    private $_locking = null;
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/Memcache.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/Memcache.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/Memcache.php	(revision 1957)
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Class Minify_Cache_Memcache
+ * @package Minify
+ */
+
+/**
+ * Memcache-based cache class for Minify
+ * 
+ * <code>
+ * // fall back to disk caching if memcache can't connect
+ * $memcache = new Memcache;
+ * if ($memcache->connect('localhost', 11211)) {
+ *     Minify::setCache(new Minify_Cache_Memcache($memcache));
+ * } else {
+ *     Minify::setCache();
+ * }
+ * </code>
+ **/
+class Minify_Cache_Memcache {
+    
+    /**
+     * Create a Minify_Cache_Memcache object, to be passed to 
+     * Minify::setCache().
+     *
+     * @param Memcache $memcache already-connected instance
+     * 
+     * @param int $expire seconds until expiration (default = 0
+     * meaning the item will not get an expiration date)
+     * 
+     * @return null
+     */
+    public function __construct($memcache, $expire = 0)
+    {
+        $this->_mc = $memcache;
+        $this->_exp = $expire;
+    }
+    
+    /**
+     * Write data to cache.
+     *
+     * @param string $id cache id
+     * 
+     * @param string $data
+     * 
+     * @return bool success
+     */
+    public function store($id, $data)
+    {
+        return $this->_mc->set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", 0, $this->_exp);
+    }
+    
+    
+    /**
+     * Get the size of a cache entry
+     *
+     * @param string $id cache id
+     * 
+     * @return int size in bytes
+     */
+    public function getSize($id)
+    {
+        return $this->_fetch($id)
+            ? strlen($this->_data)
+            : false;
+    }
+    
+    /**
+     * Does a valid cache entry exist?
+     *
+     * @param string $id cache id
+     * 
+     * @param int $srcMtime mtime of the original source file(s)
+     * 
+     * @return bool exists
+     */
+    public function isValid($id, $srcMtime)
+    {
+        return ($this->_fetch($id) && ($this->_lm >= $srcMtime));
+    }
+    
+    /**
+     * Send the cached content to output
+     *
+     * @param string $id cache id
+     */
+    public function display($id)
+    {
+        echo $this->_fetch($id)
+            ? $this->_data
+            : '';
+    }
+    
+	/**
+     * Fetch the cached content
+     *
+     * @param string $id cache id
+     * 
+     * @return string
+     */
+    public function fetch($id)
+    {
+        return $this->_fetch($id)
+            ? $this->_data
+            : '';
+    }
+    
+    private $_mc = null;
+    private $_exp = null;
+    
+    // cache of most recently fetched id
+    private $_lm = null;
+    private $_data = null;
+    private $_id = null;
+    
+	/**
+     * Fetch data and timestamp from memcache, store in instance
+     * 
+     * @param string $id
+     * 
+     * @return bool success
+     */
+    private function _fetch($id)
+    {
+        if ($this->_id === $id) {
+            return true;
+        }
+        $ret = $this->_mc->get($id);
+        if (false === $ret) {
+            $this->_id = null;
+            return false;
+        }
+        list($this->_lm, $this->_data) = explode('|', $ret, 2);
+        $this->_id = $id;
+        return true;
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/APC.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/APC.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Cache/APC.php	(revision 1957)
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Class Minify_Cache_APC
+ * @package Minify
+ */
+
+/**
+ * APC-based cache class for Minify
+ * 
+ * <code>
+ * Minify::setCache(new Minify_Cache_APC());
+ * </code>
+ * 
+ * @package Minify
+ * @author Chris Edwards
+ **/
+class Minify_Cache_APC {
+
+    /**
+     * Create a Minify_Cache_APC object, to be passed to
+     * Minify::setCache().
+     *
+     *
+     * @param int $expire seconds until expiration (default = 0
+     * meaning the item will not get an expiration date)
+     *
+     * @return null
+     */
+    public function __construct($expire = 0)
+    {
+        $this->_exp = $expire;
+    }
+
+    /**
+     * Write data to cache.
+     *
+     * @param string $id cache id
+     *
+     * @param string $data
+     *
+     * @return bool success
+     */
+    public function store($id, $data)
+    {
+        return apc_store($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp);
+    }
+
+    /**
+     * Get the size of a cache entry
+     *
+     * @param string $id cache id
+     *
+     * @return int size in bytes
+     */
+    public function getSize($id)
+    {
+        return $this->_fetch($id)
+            ? strlen($this->_data)
+            : false;
+    }
+
+    /**
+     * Does a valid cache entry exist?
+     *
+     * @param string $id cache id
+     *
+     * @param int $srcMtime mtime of the original source file(s)
+     *
+     * @return bool exists
+     */
+    public function isValid($id, $srcMtime)
+    {
+        return ($this->_fetch($id) && ($this->_lm >= $srcMtime));
+    }
+
+    /**
+     * Send the cached content to output
+     *
+     * @param string $id cache id
+     */
+    public function display($id)
+    {
+        echo $this->_fetch($id)
+            ? $this->_data
+            : '';
+    }
+
+    /**
+     * Fetch the cached content
+     *
+     * @param string $id cache id
+     *
+     * @return string
+     */
+    public function fetch($id)
+    {
+        return $this->_fetch($id)
+            ? $this->_data
+            : '';
+    }
+
+    private $_exp = null;
+
+    // cache of most recently fetched id
+    private $_lm = null;
+    private $_data = null;
+    private $_id = null;
+
+    /**
+     * Fetch data and timestamp from apc, store in instance
+     *
+     * @param string $id
+     *
+     * @return bool success
+     */
+    private function _fetch($id)
+    {
+        if ($this->_id === $id) {
+            return true;
+        }
+        $ret = apc_fetch($id);
+        if (false === $ret) {
+            $this->_id = null;
+            return false;
+        }
+        list($this->_lm, $this->_data) = explode('|', $ret, 2);
+        $this->_id = $id;
+        return true;
+    }
+}
Index: /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Controller/Base.php
===================================================================
--- /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Controller/Base.php	(revision 1957)
+++ /plugins/apostrophePlugin/tags/RELEASE_1_5_0/lib/minify/Minify/Controller/Base.php	(revision 1957)
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Class Minify_Controller_Base  
+ * @package Minify
+ */
+
+/**
+ * Base class for Minify controller
+ * 
+ * The controller class validates a request and uses it to create sources
+ * for minification and set options like contentType. It's also responsible
+ * for loading minifier code upon request.
+ * 
+ * @package Minify
+ * @author Stephen Clay <steve@mrclay.org>
+ */
+abstract class Minify_Controller_Base {
+    
+    /**
+     * Setup controller sources and set an needed options for Minify::source
+     * 
+     * You must override this method in your subclass controller to set 
+     * $this->sources. If the request is NOT valid, make sure $this->sources 
+     * is left an empty array. Then strip any controller-specific options from 
+     * $options and return it. To serve files, $this->sources must be an array of
+     * Minify_Source objects.
+     * 
+     * @param array $options controller and Minify options
+     * 
+     * return array $options Minify::serve options
+     */
+    abstract public function setupSources($options);
+    
+    /**
+     * Get default Minify options for this controller.
+     * 
+     * Override in subclass to change defaults
+     *
+     * @return array options for Minify
+     */
+    public function getDefaultMinifyOptions() {
+        return array(
+            'isPublic' => true
+            ,'encodeOutput' => function_exists('gzdeflate')
+            ,'encodeMethod' => null // determine later
+            ,'encodeLevel' => 9
+            ,'minifierOptions' => array() // no minifier options
+            ,'contentTypeCharset' => 'utf-8'
+            ,'maxAge' => 1800 // 30 minutes
+            ,'rewriteCssUris' => true
+            ,'bubbleCssImports' => false
+            ,'quiet' => false // serve() will send headers and output
+            ,'debug' => false
+            
+            // if you override this, the response code MUST be directly after 
+            // the first space.
+            ,'badRequestHeader' => 'HTTP/1.0 400 Bad Request'
+            
+            // callback function to see/modify content of all sources
+            ,'postprocessor' => null
+            // file to require to load preprocessor
+            ,'postprocessorRequire' => null
+        );
+    }  
+
+    /**
+     * Get default minifiers for this controller.
+     * 
+     * Override in subclass to change defaults
+     *
+     * @return array minifier callbacks for common types
+     */
+    public function getDefaultMinifers() {
+        $ret[Minify::TYPE_JS] = array('JSMin', 'minify');
+        $ret[Minify::TYPE_CSS] = array('Minify_CSS', 'minify');
+        $ret[Minify::TYPE_HTML] = array('Minify_HTML', 'minify');
+        return $ret;
+    }
+    
+    /**
+     * Load any code necessary to execute the given minifier callback.
+     * 
+     * The controller is responsible for loading minification code on demand
+     * via this method. This built-in function will only load classes for
+     * static method callbacks where the class isn't already defined. It uses
+     * the PEAR convention, so, given array('Jimmy_Minifier', 'minCss'), this 
+     * function will include 'Jimmy/Minifier.php'.
+     * 
+     * If you need code loaded on demand and this doesn't suit you, you'll need
+     * to override this function in your subclass. 
+     * @see Minify_Controller_Page::loadMinifier()
+     * 
+     * @param callback $minifierCallback callback of minifier function
+     * 
+     * @return null
+     */
+    public function loadMinifier($minifierCallback)
+    {
+        if (is_array($minifierCallback)
+            && is_string($minifierCallback[0])
+            && !class_exists($minifierCallback[0], false)) {
+            
+            require str_replace('_', '/', $minifierCallback[0]) . '.php';
+        }
+    }
+    
+    /**
+     * Is a user-given file within an allowable directory, existing,
+     * and having an extension js/css/html/txt ?
+     * 
+     * This is a convenience function for controllers that have to accept
+     * user-given paths
+     *
+     * @param string $file full file path (already processed by realpath())
+     * 
+     * @param array $safeDirs directories where files are safe to serve. Files can also
+     * be in subdirectories of these directories.
+  