Browse Source

first global commit

Bachir Soussi Chiadmi 3 years ago
commit
78adf3a099
100 changed files with 59209 additions and 0 deletions
  1. 6 0
      .gitignore
  2. 139 0
      .htaccess
  3. 1715 0
      CHANGELOG.txt
  4. 44 0
      COPYRIGHT.txt
  5. 42 0
      INSTALL.mysql.txt
  6. 44 0
      INSTALL.pgsql.txt
  7. 31 0
      INSTALL.sqlite.txt
  8. 398 0
      INSTALL.txt
  9. 339 0
      LICENSE.txt
  10. 303 0
      MAINTAINERS.txt
  11. 123 0
      README.txt
  12. 236 0
      UPGRADE.txt
  13. 174 0
      authorize.php
  14. 26 0
      cron.php
  15. BIN
      favicon.ico
  16. 388 0
      includes/actions.inc
  17. 1212 0
      includes/ajax.inc
  18. 68 0
      includes/archiver.inc
  19. 334 0
      includes/authorize.inc
  20. 539 0
      includes/batch.inc
  21. 84 0
      includes/batch.queue.inc
  22. 3399 0
      includes/bootstrap.inc
  23. 74 0
      includes/cache-install.inc
  24. 545 0
      includes/cache.inc
  25. 1401 0
      includes/common.inc
  26. 3039 0
      includes/database/database.inc
  27. 161 0
      includes/database/log.inc
  28. 203 0
      includes/database/mysql/database.inc
  29. 33 0
      includes/database/mysql/install.inc
  30. 107 0
      includes/database/mysql/query.inc
  31. 536 0
      includes/database/mysql/schema.inc
  32. 223 0
      includes/database/pgsql/database.inc
  33. 197 0
      includes/database/pgsql/install.inc
  34. 209 0
      includes/database/pgsql/query.inc
  35. 617 0
      includes/database/pgsql/schema.inc
  36. 108 0
      includes/database/pgsql/select.inc
  37. 507 0
      includes/database/prefetch.inc
  38. 1964 0
      includes/database/query.inc
  39. 727 0
      includes/database/schema.inc
  40. 1613 0
      includes/database/select.inc
  41. 519 0
      includes/database/sqlite/database.inc
  42. 51 0
      includes/database/sqlite/install.inc
  43. 139 0
      includes/database/sqlite/query.inc
  44. 683 0
      includes/database/sqlite/schema.inc
  45. 27 0
      includes/database/sqlite/select.inc
  46. 196 0
      includes/date.inc
  47. 1365 0
      includes/entity.inc
  48. 286 0
      includes/errors.inc
  49. 2489 0
      includes/file.inc
  50. 859 0
      includes/file.mimetypes.inc
  51. 427 0
      includes/filetransfer/filetransfer.inc
  52. 144 0
      includes/filetransfer/ftp.inc
  53. 76 0
      includes/filetransfer/local.inc
  54. 108 0
      includes/filetransfer/ssh.inc
  55. 4603 0
      includes/form.inc
  56. 145 0
      includes/graph.inc
  57. 435 0
      includes/image.inc
  58. 1700 0
      includes/install.core.inc
  59. 1326 0
      includes/install.inc
  60. 483 0
      includes/iso.inc
  61. 102 0
      includes/json-encode.inc
  62. 581 0
      includes/language.inc
  63. 2442 0
      includes/locale.inc
  64. 274 0
      includes/lock.inc
  65. 622 0
      includes/mail.inc
  66. 3883 0
      includes/menu.inc
  67. 1065 0
      includes/module.inc
  68. 664 0
      includes/pager.inc
  69. 287 0
      includes/password.inc
  70. 587 0
      includes/path.inc
  71. 189 0
      includes/registry.inc
  72. 533 0
      includes/session.inc
  73. 836 0
      includes/stream_wrappers.inc
  74. 256 0
      includes/tablesort.inc
  75. 2905 0
      includes/theme.inc
  76. 211 0
      includes/theme.maintenance.inc
  77. 264 0
      includes/token.inc
  78. 265 0
      includes/unicode.entities.inc
  79. 672 0
      includes/unicode.inc
  80. 1477 0
      includes/update.inc
  81. 427 0
      includes/updater.inc
  82. 66 0
      includes/utility.inc
  83. 624 0
      includes/xmlrpc.inc
  84. 384 0
      includes/xmlrpcs.inc
  85. 21 0
      index.php
  86. 26 0
      install.php
  87. 622 0
      misc/ajax.js
  88. BIN
      misc/arrow-asc.png
  89. BIN
      misc/arrow-desc.png
  90. 27 0
      misc/authorize.js
  91. 324 0
      misc/autocomplete.js
  92. 32 0
      misc/batch.js
  93. 103 0
      misc/collapse.js
  94. BIN
      misc/configure.png
  95. BIN
      misc/draggable.png
  96. 433 0
      misc/drupal.js
  97. BIN
      misc/druplicon.png
  98. 36 0
      misc/farbtastic/farbtastic.css
  99. 0 0
      misc/farbtastic/farbtastic.js
  100. BIN
      misc/farbtastic/marker.png

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+# Ignore configuration files that may contain sensitive information.
+sites/*/settings*.php
+
+# Ignore paths that contain user-generated content.
+sites/*/files
+sites/*/private

+ 139 - 0
.htaccess

@@ -0,0 +1,139 @@
+#
+# Apache/PHP/Drupal settings:
+#
+
+# Protect files and directories from prying eyes.
+<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(\..*|Entries.*|Repository|Root|Tag|Template)$">
+  Order allow,deny
+</FilesMatch>
+
+# Don't show directory listings for URLs which map to a directory.
+Options -Indexes
+
+# Follow symbolic links in this directory.
+Options +FollowSymLinks
+
+# Make Drupal handle any 404 errors.
+ErrorDocument 404 /index.php
+
+# Force simple error message for requests for non-existent favicon.ico.
+# <Files favicon.ico>
+#   # There is no end quote below, for compatibility with Apache 1.3.
+#   ErrorDocument 404 "The requested file favicon.ico was not found."
+# </Files>
+
+# Set the default handler.
+DirectoryIndex index.php index.html index.htm
+
+# Override PHP settings that cannot be changed at runtime. See
+# sites/default/default.settings.php and drupal_initialize_variables() in
+# includes/bootstrap.inc for settings that can be changed at runtime.
+
+# PHP 5, Apache 1 and 2.
+<IfModule mod_php5.c>
+  php_flag magic_quotes_gpc                 off
+  php_flag magic_quotes_sybase              off
+  php_flag register_globals                 off
+  php_flag session.auto_start               off
+  php_value mbstring.http_input             pass
+  php_value mbstring.http_output            pass
+  php_flag mbstring.encoding_translation    off
+</IfModule>
+
+# Requires mod_expires to be enabled.
+<IfModule mod_expires.c>
+  # Enable expirations.
+  ExpiresActive On
+
+  # Cache all files for 2 weeks after access (A).
+  ExpiresDefault A1209600
+
+  <FilesMatch \.php$>
+    # Do not allow PHP scripts to be cached unless they explicitly send cache
+    # headers themselves. Otherwise all scripts would have to overwrite the
+    # headers set by mod_expires if they want another caching behavior. This may
+    # fail if an error occurs early in the bootstrap process, and it may cause
+    # problems if a non-Drupal PHP file is installed in a subdirectory.
+    ExpiresActive Off
+  </FilesMatch>
+</IfModule>
+
+# Various rewrite rules.
+<IfModule mod_rewrite.c>
+  RewriteEngine on
+
+  # Block access to "hidden" directories whose names begin with a period. This
+  # includes directories used by version control systems such as Subversion or
+  # Git to store control files. Files whose names begin with a period, as well
+  # as the control files used by CVS, are protected by the FilesMatch directive
+  # above.
+  #
+  # NOTE: This only works when mod_rewrite is loaded. Without mod_rewrite, it is
+  # not possible to block access to entire directories from .htaccess, because
+  # <DirectoryMatch> is not allowed here.
+  #
+  # If you do not have mod_rewrite installed, you should remove these
+  # directories from your webroot or otherwise protect them from being
+  # downloaded.
+  RewriteRule "(^|/)\." - [F]
+
+  # If your site can be accessed both with and without the 'www.' prefix, you
+  # can use one of the following settings to redirect users to your preferred
+  # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:
+  #
+  # To redirect all users to access the site WITH the 'www.' prefix,
+  # (http://example.com/... will be redirected to http://www.example.com/...)
+  # uncomment the following:
+  # RewriteCond %{HTTP_HOST} !^www\. [NC]
+  # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+  #
+  # To redirect all users to access the site WITHOUT the 'www.' prefix,
+  # (http://www.example.com/... will be redirected to http://example.com/...)
+  # uncomment the following:
+  # RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
+  # RewriteRule ^ http://%1%{REQUEST_URI} [L,R=301]
+
+  # Modify the RewriteBase if you are using Drupal in a subdirectory or in a
+  # VirtualDocumentRoot and the rewrite rules are not working properly.
+  # For example if your site is at http://example.com/drupal uncomment and
+  # modify the following line:
+  # RewriteBase /drupal
+  #
+  # If your site is running in a VirtualDocumentRoot at http://example.com/,
+  # uncomment the following line:
+  RewriteBase /
+
+  # Pass all requests not referring directly to files in the filesystem to
+  # index.php. Clean URLs are handled in drupal_environment_initialize().
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteCond %{REQUEST_URI} !=/favicon.ico
+  RewriteRule ^ index.php [L]
+
+  # Rules to correctly serve gzip compressed CSS and JS files.
+  # Requires both mod_rewrite and mod_headers to be enabled.
+  <IfModule mod_headers.c>
+    # Serve gzip compressed CSS files if they exist and the client accepts gzip.
+    RewriteCond %{HTTP:Accept-encoding} gzip
+    RewriteCond %{REQUEST_FILENAME}\.gz -s
+    RewriteRule ^(.*)\.css $1\.css\.gz [QSA]
+
+    # Serve gzip compressed JS files if they exist and the client accepts gzip.
+    RewriteCond %{HTTP:Accept-encoding} gzip
+    RewriteCond %{REQUEST_FILENAME}\.gz -s
+    RewriteRule ^(.*)\.js $1\.js\.gz [QSA]
+
+    # Serve correct content types, and prevent mod_deflate double gzip.
+    RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1]
+    RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1]
+
+    <FilesMatch "(\.js\.gz|\.css\.gz)$">
+      # Serve correct encoding type.
+      Header append Content-Encoding gzip
+      # Force proxies to cache gzipped & non-gzipped css/js files separately.
+      Header append Vary Accept-Encoding
+    </FilesMatch>
+  </IfModule>
+</IfModule>
+
+php_value memory_limit 1024M

+ 1715 - 0
CHANGELOG.txt

@@ -0,0 +1,1715 @@
+
+Drupal 7.22, 2013-04-03
+-----------------------
+- Allowed the drupal_http_request() function to be overridden so that
+  additional HTTP request capabilities can be added by contributed modules.
+- Changed the Simpletest module to allow PSR-0 test classes to be used in
+  Drupal 7.
+- Removed an unnecessary "Content-Disposition" header from private file
+  downloads; it prevented many private files from being viewed inline in a web
+  browser.
+- Changed various field API functions to allow them to optionally act on a
+  single field within an entity (API addition: http://drupal.org/node/1825844).
+- Fixed a bug which prevented Drupal's file transfer functionality from working
+  on some PHP 5.4 systems.
+- Fixed incorrect log message when theme() is called for a theme hook that does
+  not exist (minor string change).
+- Fixed Drupal's token-replacement system to allow spaces in the token value.
+- Changed the default behavior after a user creates a node they do not have
+  access to view. The user will now be redirected to the front page rather than
+  an access denied page.
+- Fixed a bug which prevented empty HTTP headers (such as "0") from being set.
+  (Minor behavior change: Callers of drupal_add_http_header() must now set
+  FALSE explicitly to prevent a header from being sent at all; this was already
+  indicated in the function's documentation.)
+- Fixed OpenID errors when more than one module implements hook_openid(). The
+  behavior is now changed so that if more than one module tries to set the same
+  parameter, the last module's change takes effect.
+- Fixed a serious documentation bug: The $name variable in the
+  taxonomy-term.tpl.php theme template was incorrectly documented as being
+  sanitized when in fact it is not.
+- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
+  duplicate permission names in the User module's database tables.
+- Added an empty "datatype" attribute to taxonomy term and username links to
+  make the RDFa markup upward compatible with RDFa 1.1 (minor markup addition).
+- Fixed a bug which caused the denial-of-service protection added in Drupal
+  7.20 to break certain valid image URLs that had an extra slash in them.
+- Fixed a bug with update queries in the SQLite database driver that prevented
+  Drupal from being installed with SQLite on PHP 5.4.
+- Fixed enforced dependencies errors updating to recent versions of Drupal 7 on
+  certain non-MySQL databases.
+- Refactored the Field module's caching behavior to obtain large improvements
+  in memory usage for sites with many fields and instances (API addition:
+  http://drupal.org/node/1915646).
+- Fixed entity argument not being passed to implementations of
+  hook_file_download_access_alter(). The fix adds an additional context
+  parameter that can be passed when calling drupal_alter() for any hook (API
+  change: http://drupal.org/node/1882722).
+- Fixed broken support for translatable comment fields (API change:
+  http://drupal.org/node/1874724).
+- Added an assertThemeOutput() method to Simpletest to allow tests to check
+  that themed output matches an expected HTML string (API addition).
+- Added a link to "Install another module" after a module has been successfully
+  downloaded via the Update Manager (UI change).
+- Added an optional "exclusive" flag to installation profile .info files which
+  allows Drupal distributions to force a profile to be selected during
+  installation (API addition).
+- Fixed a bug which caused the database API to not properly close database
+  connections.
+- Added a link to the URL for running cron from outside the site to the Cron
+  settings page (UI change).
+- Fixed a bug which prevented image styles from being reverted on PHP 5.4.
+- Made the default .htaccess rules protocol sensitive to improve security for
+  sites which use HTTPS and redirect between "www" and non-"www" versions of
+  the page.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.21, 2013-03-06
+-----------------------
+- Allowed sites using the 'image_allow_insecure_derivatives' variable to still
+  have partial protection from the security issues fixed in Drupal 7.20.
+
+Drupal 7.20, 2013-02-20
+-----------------------
+- Fixed security issues (denial of service). See SA-CORE-2013-002.
+
+Drupal 7.19, 2013-01-16
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2013-001.
+
+Drupal 7.18, 2012-12-19
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2012-004.
+
+Drupal 7.17, 2012-11-07
+-----------------------
+- Changed the default value of the '404_fast_html' variable to have a DOCTYPE
+  declaration.
+- Made it possible to use associative arrays for the 'items' variable in
+  theme_item_list().
+- Fixed a bug which prevented required form elements without a title from being
+  given an "error" class when the form fails validation.
+- Prevented duplicate HTML IDs from appearing when two forms are displayed on
+  the same page and one of them is submitted with invalid data (minor markup
+  change).
+- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
+  stale data in the Upload module's database tables.
+- Fixed a bug in the States API which prevented certain types of form elements
+  from being disabled when requested.
+- Allowed aggregator feed items with author names longer than 255 characters to
+  have a truncated version saved to the database (rather than causing a fatal
+  error).
+- Allowed aggregator feed items to have URLs longer than 255 characters
+  (schema change which results in several columns in the Aggregator module's
+  database tables changing from VARCHAR to TEXT fields).
+- Added hook_taxonomy_term_view() and standardized the process for rendering
+  taxonomy terms to invoke hook_entity_view() and otherwise make it consistent
+  with other entities (API change: http://drupal.org/node/1808870).
+- Added hook_entity_view_mode_alter() to allow modules to change entity view
+  modes on display (API addition: http://drupal.org/node/1833086).
+- Fixed a bug which made database queries running a "LIKE" query on blob fields
+  fail on PostgreSQL databases. This caused errors during the Drupal 6 to
+  Drupal 7 upgrade.
+- Changed the hook_menu() entry for Drupal's rss.xml page to prevent extra path
+  components from being accidentally passed to the page callback function (data
+  structure change).
+- Removed a non-standard "name" attribute from Drupal's default Content-Type
+  header for file downloads.
+- Fixed the theme settings form to properly clean up submitted values in
+  $form_state['values'] when the form is submitted (data structure change).
+- Fixed an inconsistency by removing the colon from the end of the label on
+  multi-valued form fields (minor string change).
+- Added support for 'weight' in hook_field_widget_info() to allow modules to
+  control the order in which widgets are displayed in the Field UI.
+- Updated various tables in the OpenID and Book modules to use the default
+  "empty table" text pattern (string change).
+- Added proxy server support to drupal_http_request().
+- Added "lang" attributes to language links, to better support screen readers.
+- Fixed double occurrence of a "ul" HTML tag on secondary local tasks in the
+  Seven theme (markup change).
+- Fixed bugs which caused taxonomy vocabulary and shortcut set titles to be
+  double-escaped. The fix replaces the taxonomy vocabulary overview page and
+  "Edit shortcuts" menu items' title callback entries in hook_menu() with new
+  functions that do not escape HTML characters (data structure change).
+- Modified the Update manager module to allow drupal.org to collect usage
+  statistics for individual modules and themes, rather than only for entire
+  projects.
+- Modified the node listing database query on Drupal's default front page to
+  add table aliases for better query altering (this is a data structure change
+  affecting code which implements hook_query_alter() on this query).
+- Improved the translatability of the "Field type(s) in use" message on the
+  modules page (admin-facing string change).
+- Fixed a regression which caused a "call to undefined function
+  drupal_find_base_themes()" fatal error under rare circumstances.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.16, 2012-10-17
+-----------------------
+- Fixed security issues (Arbitrary PHP code execution and information
+  disclosure). See SA-CORE-2012-003.
+
+Drupal 7.15, 2012-08-01
+-----------------------
+- Introduced a 'user_password_reset_timeout' variable to allow the 24-hour
+  expiration for user password reset links to be adjusted (API addition).
+- Fixed database errors due to ambiguous column names that occurred when
+  EntityFieldQuery was used in certain situations.
+- Changed the drupal_array_get_nested_value() function to return a reference
+  (API addition).
+- Changed the System module's hook_block_info() implementation to assign the
+  "Main page content" and "System help" blocks to appropriate regions by
+  default and prevent error messages on the block administration page (data
+  structure change).
+- Fixed regression: Non-node entities couldn't be accessed with
+  EntityFieldQuery.
+- Fixed regression: Optional radio buttons with an empty, non-NULL default
+  value led to an illegal choice error when none were selected.
+- Reorganized the testing framework to split setUp() into specific sub-methods
+  and fix several regressions in the process.
+- Fixed bug which made it impossible to search for strings that have not been
+  translated into a particular language.
+- Renamed the "Field" column on the Manage Fields screen to "Field type", since
+  the former was confusing and inaccurate (UI change).
+- Performance improvement: Removed needless call to system_rebuild_module_data()
+  in field_sync_field_status(), greatly speeding up bulk module enable/disable.
+- Fixed bug which prevented notifications from being sent when core, module, and
+  theme updates are available.
+- Fixed bug which prevented sub-themes from inheriting the default values of
+  theme settings defined by the base theme.
+- Fixed bug which prevented the jQuery UI Datepicker from being localized.
+- Made Ajax alert dialogs respect error reporting settings.
+- Fixed bug which prevented image styles from being deleted on PHP 5.4.
+- Fixed bug: Language detection by domain only worked on port 80.
+- Fixed regression: The first plural index on a page was not calculated
+  correctly.
+- Introduced generic entity language support. Entities may now declare their
+  language property in hook_entity_info(), and modules working with entities
+  may access the language using entity_language() (API change:
+  http://drupal.org/node/1626346).
+- Added EntityFieldQuery support for taxonomy bundles.
+- Fixed issue where field form structure was incomplete if field_access()
+  returned FALSE. Instead of being incomplete, the form structure now has
+  #access set to FALSE and field form validation is skipped (data structure
+  change: http://drupal.org/node/1663020).
+- Fixed data loss issue due to field_has_data() returning inconsistent results.
+  The fix adds an optional DANGEROUS_ACCESS_CHECK_OPT_OUT tag to entity field
+  queries which field storage engines can respond to (API addition:
+  http://drupal.org/node/1597378).
+- Fixed notice: Undefined index: default_image in image_field_prepare_view()
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.14 2012-05-02
+----------------------
+- Fixed "integrity constraint" fatal errors when rebuilding registry.
+- Fixed custom logo and favicon functionality referencing incorrect paths.
+- Fixed DB Case Sensitivity: Allow BINARY attribute in MySQL.
+- Split field_bundle_settings out per bundle.
+- Improve UX for machine names for fields (UI change).
+- Fixed User pictures are not removed properly.
+- Fixed HTTPS sessions not working in all cases.
+- Fixed Regression: Required radios throw illegal choice error when none
+  selected.
+- Fixed allow autocompletion requests to include slashes.
+- Eliminate $user->cache and {session}.cache in favor of
+  $_SESSION['cache_expiration'][$bin] (Performance).
+- Fixed focus jumps to tab when pressing enter on a form element within tab.
+- Fixed race condition in locale() - duplicates in {locales_source}.
+- Fixed Missing "Default image" per field instance.
+- Quit clobbering people's work when they click the filter tips link
+- Form API #states: Fix conditionals to allow OR and XOR constructions.
+- Fixed Focus jumps to tab when pressing enter on a form element within tab.
+  (Accessibility)
+- Improved performance of node_access queries.
+- Fixed Fieldsets inside vertical tabs have no title and can't be collapsed.
+- Reduce size of cache_menu table (Performance).
+- Fixed unnecessary aggregation of CSS/JS (Performance).
+- Fixed taxonomy_autocomplete() produces SQL error for nonexistent field.
+- Fixed HTML filter is not run first by default, despite default weight.
+- Fixed Overlay does not work with prefixed URL paths.
+- Better debug info for field errors (string change).
+- Fixed Data corruption in comment IDs (results in broken threading on
+  PostgreSQL).
+- Fixed machine name not editable if every character is replaced.
+- Fixed user picture not appearing in comment preview (Markup change).
+- Added optional vid argument for taxonomy_get_term_by_name().
+- Fixed Invalid Unicode code range in PREG_CLASS_UNICODE_WORD_BOUNDARY fails
+  with PCRE 8.30.
+- Fixed {trigger_assignments()}.hook has only 32 characters, is too short.
+- Numerous fixes to run-tests.sh.
+- Fixed Tests in profiles/[name]/modules cannot be run and cannot use a
+  different profile for running tests.
+- Numerous JavaScript performance fixes.
+- Numerous documentation fixes.
+- Fixed All pager links have an 'active' CSS class.
+- Numerous upgrade path fixes; notably:
+  - system_update_7061() fails on inserting files with same name but different
+    case.
+  - system_update_7061() converts filepaths too aggressively.
+  - Trigger upgrade path: Node triggers removed when upgrading to 7-x from 6.25.
+
+Drupal 7.13 2012-05-02
+----------------------
+- Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-002.
+
+Drupal 7.12, 2012-02-01
+----------------------
+- Fixed bug preventing custom menus from receiving an active trail.
+- Fixed hook_field_delete() no longer invoked during field_purge_data().
+- Fixed bug causing entity info cache to not be cleared with the rest of caches.
+- Fixed file_unmanaged_copy() fails with Drupal 7.7+ and safe_mode() or
+  open_basedir().
+- Fixed Nested transactions throw exceptions when they got out of scope.
+- Fixed bugs with the Return-Path when sending mail on both Windows and
+  non-Windows systems.
+- Fixed bug with DrupalCacheArray property visibility preventing others from
+  extending it (API change: http://drupal.org/node/1422264).
+- Fixed bug with handling of non-ASCII characters in file names (API change:
+  http://drupal.org/node/1424840).
+- Reconciled field maximum length with database column size in image and
+  aggregator modules.
+- Fixes to various core JavaScript files to allow for minification and
+  aggregation.
+- Fixed Prevent tests from deleting main installation's tables when
+  parent::setUp() is not called.
+- Fixed several Poll module bugs.
+- Fixed several Shortcut module bugs.
+- Added new hook_system_theme_info() to provide ability for contributed modules
+  to test theme functionality.
+- Added ability to cancel mail sending from hook_mail_alter().
+- Added support for configurable PDO connection options, enabling master-master
+  database replication.
+- Numerous improvements to tests and test runner to pave the way for faster test
+  runs.
+- Expanded test coverage.
+- Numerous API documentation improvements.
+- Numerous performance improvements, including token replacement and render
+  cache.
+
+Drupal 7.11, 2012-02-01
+----------------------
+- Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-001.
+
+Drupal 7.10, 2011-12-05
+----------------------
+- Fixed Content-Language HTTP header to not cause issues with Drush 5.x.
+- Reduce memory usage of theme registry (performance).
+- Fixed PECL upload progress bar for FileField
+- Fixed running update.php doesn't always clear the cache.
+- Fixed PDO exceptions on long titles.
+- Fixed Overlay redirect does not include query string.
+- Fixed D6 modules satisfy D7 module dependencies.
+- Fixed the ordering of module hooks when using module_implements_alter().
+- Fixed "floating" submit buttons during AJAX requests.
+- Fixed timezone selected on install not propogating to admin account.
+- Added msgctx context to JS translation functions, for feature parity with t().
+- Profiles' .install files now available during hook_install_tasks().
+- Added test coverage of 7.0 -> 7.x upgrade path.
+- Numerous notice fixes.
+- Numerous documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.9, 2011-10-26
+----------------------
+- Critical fixes to OpenID to spec violations that could allow for
+  impersonation in certain scenarios. Existing OpenID users should see
+  http://drupal.org/node/1120290#comment-5092796 for more information on
+  transitioning.
+- Fixed files getting lost when adding multiple files to multiple file fields
+  at the same time.
+- Improved usability of the clean URL test screens.
+- Restored height/width attributes on images run through the theme system.
+- Fixed usability bug with first password field being pre-filled by certain
+  browser plugins.
+- Fixed file_usage_list() so that it can return more than one result.
+- Fixed bug preventing preview of private images on node form.
+- Fixed PDO error when inserting an aggregator title longer than 255 characters.
+- Spelled out what TRADITIONAL means in MySQL sql_mode.
+- Deprecated "!=" operator for DBTNG; should be "<>".
+- Added two new API functions (menu_tree_set_path()/menu_tree_get_path()) were
+  added in order to enable setting the active menu trail for dynamically
+  generated menu paths.
+- Added new "fast 404" capability in settings.php to bypass Drupal bootstrap
+  when serving 404 pages for certain file types.
+- Added format_string() function which can perform string munging ala the t()
+  function without the overhead of the translation system.
+- Numerous #states system fixes.
+- Numerous EntityFieldQuery, DBTNG, and SQLite fixes.
+- Numerous Shortcut module fixes.
+- Numerous language system fixes.
+- Numerous token fixes.
+- Numerous CSS fixes.
+- Numerous upgrade path fixes.
+- Numerous minor string fixes.
+- Numerous notice fixes.
+
+Drupal 7.8, 2011-08-31
+----------------------
+- Fixed critical upgrade path issue with multilingual sites, leading to lost
+  content.
+- Numerous fixes to upgrade path, preventing fatal errors due to incorrect
+  dependencies.
+- Fixed issue with saving files on hosts with open_basedir restrictions.
+- Fixed Update manger error when used with Overlay.
+- Fixed RTL support in Seven administration theme and Overlay.
+- Fixes to nested transaction support.
+- Introduced performance pattern to reduce Drupal core's RAM usage.
+- Added support for HTML 5 tags to filter_xss_admin().
+- Added exception handling to cron.
+- Added new hook hook_field_widget_form_alter() for contribtued modules.
+- element_validate_*() functions now available to contrib.
+- Added new maintainers for several subsystems.
+- Numerous testing system improvements.
+- Numerous markup and CSS fixes.
+- Numerous poll module fixes.
+- Numerous notice/warning fixes.
+- Numerous documentation fixes.
+- Numerous token fixes.
+
+Drupal 7.7, 2011-07-27
+----------------------
+- Fixed VERSION string.
+
+Drupal 7.6, 2011-07-27
+----------------------
+- Fixed support for remote streamwrappers.
+- AJAX now binds to 'click' instead of 'mousedown'.
+- 'Translatable' flag on fields created in UI now defaults to FALSE, to match those created via the API.
+- Performance enhancement to permissions page on large numbers of permissions.
+- More secure password generation.
+- Fix for temporary directory on Windows servers.
+- run-tests.sh now uses proc_open() instead of pcntl_fork() for better Windows support.
+- Numerous upgrade path fixes.
+- Numerous documentation fixes.
+- Numerous notice fixes.
+- Numerous fixes to improve PHP 5.4 support.
+- Numerous RTL improvements.
+
+Drupal 7.5, 2011-07-27
+----------------------
+- Fixed security issue (Access bypass), see SA-CORE-2011-003.
+
+Drupal 7.4, 2011-06-29
+----------------------
+- Rolled back patch that caused fatal errors in CTools, Feeds, and other modules using the class registry.
+- Fixed critical bug with saving default images.
+- Fixed fatal errors when uninstalling some modules.
+- Added workaround for MySQL transaction support breaking on DDL statments.
+- Improved page caching with external caching systems.
+- Fix to Batch API, which was terminating too early.
+- Numerous upgrade path fixes.
+- Performance fixes.
+- Additional test coverage.
+- Numerous documentation fixes.
+
+Drupal 7.3, 2011-06-29
+----------------------
+- Fixed security issue (Access bypass), see SA-CORE-2011-002.
+
+Drupal 7.2, 2011-05-25
+----------------------
+- Added a default .gitignore file.
+- Improved PostgreSQL and SQLite support.
+- Numerous critical performance improvements.
+- Numerous critical fixes to the upgrade path.
+- Numerous fixes to language and translation systems.
+- Numerous fixes to AJAX and #states systems.
+- Improvements to the locking system.
+- Numerous documentation fixes.
+- Numerous styling and theme system fixes.
+- Numerous fixes for schema mis-matches between Drupal 6 and 7.
+- Minor internal API clean-ups.
+
+Drupal 7.1, 2011-05-25
+----------------------
+- Fixed security issues (Cross site scripting, File access bypass), see SA-CORE-2011-001.
+
+Drupal 7.0, 2011-01-05 
+----------------------
+- Database:
+    * Fully rewritten database layer utilizing PHP 5's PDO abstraction layer.
+    * Drupal now requires MySQL >= 5.0.15 or PostgreSQL >= 8.3.
+    * Added query builders for INSERT, UPDATE, DELETE, MERGE, and SELECT queries.
+    * Support for master/slave replication, transactions, multi-insert queries,
+      and other features.
+    * Added support for the SQLite database engine.
+    * Default to InnoDB engine, rather than MyISAM, on MySQL when available.
+      This offers increased scalability and data integrity.
+- Security:
+    * Protected cron.php -- cron will only run if the proper key is provided.
+    * Implemented a pluggable password system and much stronger password hashes
+      that are compatible with the Portable PHP password hashing framework.
+    * Rate limited login attempts to prevent brute-force password guessing, and
+      improved the flood control API to allow variable time windows and
+      identifiers for limiting user access to resources.
+    * Transformed the "Update status" module into the "Update manager" which
+      can securely install or update modules and themes via a web interface.
+- Usability:
+    * Added contextual links (a.k.a. local tasks) to page elements, such as
+      blocks, nodes, or comments, which allows to perform the most common tasks
+      with a single click only.
+    * Improved installer requirements check.
+    * Improved support for integration of WYSIWYG editors.
+    * Implemented drag-and-drop positioning for input format listings.
+    * Implemented drag-and-drop positioning for language listing.
+    * Implemented drag-and-drop positioning for poll options.
+    * Provided descriptions and human-readable names for user permissions.
+    * Removed comment controls for users.
+    * Removed display order settings for comment module. Comment display
+      order can now be customized using the Views module.
+    * Removed the 'related terms' feature from taxonomy module since this can
+      now be achieved with Field API.
+    * Added additional features to the default installation profile, and
+      implemented a "slimmed down" profile designed for developers.
+    * Added a built-in, automated cron run feature, which is triggered by site
+      visitors.
+    * Added an administrator role which is assigned all permissions for
+      installed modules automatically.
+    * Image toolkits are now provided by modules (rather than requiring a
+      manual file copy to the includes directory).
+    * Added an edit tab to taxonomy term pages.
+    * Redesigned password strength validator.
+    * Redesigned the add content type screen.
+    * Highlight duplicate URL aliases.
+    * Renamed "input formats" to "text formats".
+    * Moved text format permissions to the main permissions page.
+    * Added configurable ability for users to cancel their own accounts.
+    * Added "vertical tabs", a reusable interface component that features
+      automatic summaries and increases usability.
+    * Replaced fieldsets on node edit and add pages with vertical tabs.
+- Performance:
+    * Improved performance on uncached page views by loading multiple core
+      objects in a single database query.
+    * Improved performance for logged-in users by reducing queries for path
+      alias lookups.
+    * Improved support for HTTP proxies (including reverse proxies), allowing
+      anonymous page views to be served entirely from the proxy.
+- Documentation:
+    * Hook API documentation now included in Drupal core.
+- News aggregator:
+    * Added OPML import functionality for RSS feeds.
+    * Optionally, RSS feeds may be configured to not automatically generate feed blocks.
+- Search:
+    * Added support for language-aware searches.
+- Aggregator:
+    * Introduced architecture that allows pluggable parsers and processors for
+      syndicating RSS and Atom feeds.
+    * Added options to suspend updating specific feeds and never discard feeds
+      items.
+- Testing:
+    * Added test framework and tests.
+- Improved time zone support:
+    * Drupal now uses PHP's time zone database when rendering dates in local
+      time. Site-wide and user-configured time zone offsets have been converted
+      to time zone names, e.g. Africa/Abidjan.
+    * In some cases the upgrade and install scripts do not choose the preferred
+      site default time zone. The automatically-selected time zone can be
+      corrected at admin/config/regional/settings.
+    * If your site is being upgraded from Drupal 6 and you do not have the
+      contributed date or event modules installed, user time zone settings will
+      fallback to the system time zone and will have to be reconfigured by each user.
+    * User-configured time zones now serve as the default time zone for PHP
+      date/time functions.
+- Filter system:
+    * Revamped the filter API and text format storage.
+    * Added support for default text formats to be assigned on a per-role basis.
+    * Refactored the HTML corrector to take advantage of PHP 5 features.
+- User system:
+    * Added clean API functions for creating, loading, updating, and deleting
+      user roles and permissions.
+    * Refactored the "access rules" component of user module: The user module
+      now provides a simple interface for blocking single IP addresses. The
+      previous functionality in the user module for restricting certain e-mail
+      addresses and usernames is now available as a contributed module. Further,
+      IP address range blocking is no longer supported and should be implemented
+      at the operating system level.
+    * Removed per-user themes: Contributed modules with similar functionality
+      are available.
+- OpenID:
+    * Added support for Gmail and Google Apps for Domain identifiers. Users can
+      now login with their user@example.com identifier when example.com is powered
+      by Google.
+    * Made the OpenID module more pluggable.
+- Added code registry:
+    * Using the registry, modules declare their includable files via their .info file,
+      allowing Drupal to lazy-load classes and interfaces as needed.
+- Theme system:
+    * Removed the Bluemarine, Chameleon and Pushbutton themes. These themes live
+      on as contributed themes (http://drupal.org/project/bluemarine,
+      http://drupal.org/project/chameleon and http://drupal.org/project/pushbutton).
+    * Added Stark theme to make analyzing Drupal's default HTML and CSS easier.
+    * Added Seven as the default administration theme.
+    * Variable preprocessing of theme hooks prior to template rendering now goes
+      through two phases: a 'preprocess' phase and a new 'process' phase. See
+      http://api.drupal.org/api/function/theme/7 for details.
+    * Theme hooks implemented as functions (rather than as templates) can now
+      also have preprocess (and process) functions. See
+      http://api.drupal.org/api/function/theme/7 for details.
+    * Added Bartik as the default theme.
+- File handling:
+    * Files are now first class Drupal objects with file_load(), file_save(),
+      and file_validate() functions and corresponding hooks.
+    * The file_move(), file_copy() and file_delete() functions now operate on
+      file objects and invoke file hooks so that modules are notified and can
+      respond to changes.
+    * For the occasions when only basic file manipulation are needed--such as
+      uploading a site logo--that don't require the overhead of databases and
+      hooks, the current unmanaged copy, move and delete operations have been
+      preserved but renamed to file_unmanaged_*().
+    * Rewrote file handling to use PHP stream wrappers to enable support for
+      both public and private files and to support pluggable storage mechanisms
+      and access to remote resources (e.g. S3 storage or Flickr photos).
+    * The mime_extension_mapping variable has been removed. Modules that need to
+      alter the default MIME type extension mappings should implement
+      hook_file_mimetype_mapping_alter().
+    * Added the hook_file_url_alter() hook, which makes it possible to serve
+      files from a CDN.
+    * Added a field specifically for uploading files, previously provided by
+      the contributed module FileField.
+- Image handling:
+    * Improved image handling, including better support for add-on image
+      libraries.
+    * Added API and interface for creating advanced image thumbnails.
+    * Inclusion of additional effects such as rotate and desaturate.
+    * Added a field specifically for uploading images, previously provided by
+      the contributed module ImageField.
+- Added aliased multi-site support:
+    * Added support for mapping domain names to sites directories.
+- Added RDF support:
+    * Modules can declare RDF namespaces which are serialized in the <html> tag
+      for RDFa support.
+    * Modules can specify how their data structure maps to RDF.
+    * Added support for RDFa export of nodes, comments, terms, users, etc. and
+      their fields.
+- Search engine optimization and web linking:
+    * Added a rel="canonical" link on node and comment pages to prevent
+      duplicate content indexing by search engines.
+    * Added a default rel="shortlink" link on node and comment pages that
+      advertises a short link as an alternative URL to third-party services.
+    * Meta information is now alterable by all modules before rendering.
+- Field API:
+    * Custom data fields may be attached to nodes, users, comments and taxonomy
+      terms.
+    * Node bodies and teasers are now Field API fields instead of
+      being a hard-coded property of node objects.
+    * In addition, any other object type may register with Field API
+      and allow custom data fields to be attached to itself.
+    * Provides most of the features of the former Content Construction
+      Kit (CCK) module.
+    * Taxonomy terms are now Field API fields that can be added to any fieldable
+      object.
+- Installer:
+    * Refactored the installer into an API that allows Drupal to be installed
+      via a command line script.
+- Page organization
+    * Made the help text area a full featured region with blocks.
+    * Site mission is replaced with the highlighted content block region and
+      separate RSS feed description settings.
+    * The footer message setting was removed in favor of custom blocks.
+    * Made the main page content a block which can be moved and ordered
+      with other blocks in the same region.
+    * Blocks can now return structured arrays for later rendering just
+      like page callbacks.
+- Translation system
+    * The translation system now supports message context (msgctxt).
+    * Added support for translatable fields to Field API.
+- JavaScript changes
+    * Upgraded the core JavaScript library to jQuery version 1.4.4.
+    * Upgraded the jQuery Forms library to 2.52.
+    * Added jQuery UI 1.8.7, which allows improvements to Drupal's user
+      experience.
+- Better module version support
+    * Modules now can specify which version of another module they depend on.
+- Removed modules from core
+    * The following modules have been removed from core, because contributed
+      modules with similar functionality are available:
+      * Blog API module
+      * Ping module
+      * Throttle module
+- Improved node access control system.
+    * All modules may now influence the access to a node at runtime, not just
+      the module that defined a node.
+    * Users may now be allowed to bypass node access restrictions without giving
+      them complete access to the site.
+    * Access control affects both published and unpublished nodes.
+    * Numerous other improvements to the node access system.
+- Actions system
+    * Simplified definitions of actions and triggers.
+    * Removed dependency on the combination of hooks and operations. Triggers
+      now directly map to module hooks.
+- Task handling
+    * Added a queue API to process many or long-running tasks.
+    * Added queue API support to cron API.
+    * Added a locking framework to coordinate long-running operations across
+      requests.
+
+Drupal 6.23-dev, xxxx-xx-xx (development release)
+-----------------------
+
+Drupal 6.22, 2011-05-25
+-----------------------
+- Made Drupal 6 work better with IIS and Internet Explorer.
+- Fixed .po file imports to work better with custom textgroups.
+- Improved code documentation at various places.
+- Fixed a variety of other bugs.
+
+Drupal 6.21, 2011-05-25
+----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2011-001.
+
+Drupal 6.20, 2010-12-15
+----------------------
+- Fixed a variety of small bugs, improved code documentation.
+
+Drupal 6.19, 2010-08-11
+----------------------
+- Fixed a variety of small bugs, improved code documentation.
+
+Drupal 6.18, 2010-08-11
+----------------------
+- Fixed security issues (OpenID authentication bypass, File download access
+  bypass, Comment unpublishing bypass, Actions cross site scripting),
+  see SA-CORE-2010-002.
+
+Drupal 6.17, 2010-06-02
+----------------------
+- Improved PostgreSQL compatibility
+- Better PHP 5.3 and PHP 4 compatibility
+- Better browser compatibility of CSS and JS aggregation
+- Improved logging for login failures
+- Fixed an incompatibility with some contributed modules and the locking system
+- Fixed a variety of other bugs.
+
+Drupal 6.16, 2010-03-03
+----------------------
+- Fixed security issues (Installation cross site scripting, Open redirection,
+  Locale module cross site scripting, Blocked user session regeneration),
+  see SA-CORE-2010-001.
+- Better support for updated jQuery versions.
+- Reduced resource usage of update.module.
+- Fixed several issues relating to support of installation profiles and
+  distributions.
+- Added a locking framework to avoid data corruption on long operations.
+- Fixed a variety of other bugs.
+
+Drupal 6.15, 2009-12-16
+----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2009-009.
+- Fixed a variety of other bugs.
+
+Drupal 6.14, 2009-09-16
+----------------------
+- Fixed security issues (OpenID association cross site request forgeries,
+  OpenID impersonation and File upload), see SA-CORE-2009-008.
+- Changed the system modules page to not run all cache rebuilds; use the
+  button on the performance settings page to achieve the same effect.
+- Added support for PHP 5.3.0 out of the box.
+- Fixed a variety of small bugs.
+
+Drupal 6.13, 2009-07-01
+----------------------
+- Fixed security issues (Cross site scripting, Input format access bypass and
+  Password leakage in URL), see SA-CORE-2009-007.
+- Fixed a variety of small bugs.
+
+Drupal 6.12, 2009-05-13
+----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
+- Fixed a variety of small bugs.
+
+Drupal 6.11, 2009-04-29
+----------------------
+- Fixed security issues (Cross site scripting and limited information
+  disclosure), see SA-CORE-2009-005
+- Fixed performance issues with the menu router cache, the update
+  status cache and improved cache invalidation
+- Fixed a variety of small bugs.
+
+Drupal 6.10, 2009-02-25
+----------------------
+- Fixed a security issue, (Local file inclusion on Windows),
+  see SA-CORE-2009-003
+- Fixed node_feed() so custom fields can show up in RSS feeds.
+- Improved PostgreSQL compatibility.
+- Fixed a variety of small bugs.
+
+Drupal 6.9, 2009-01-14
+----------------------
+- Fixed security issues, (Access Bypass, Validation Bypass and Hardening
+  against SQL injection), see SA-CORE-2009-001
+- Made HTTP request checking more robust and informative.
+- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and
+  basic shell scripts.
+- Removed t() calls from all schema documentation. Suggested best practice
+  changed for contributed modules, see http://drupal.org/node/322731.
+- Fixed a variety of small bugs.
+
+Drupal 6.8, 2008-12-11
+----------------------
+- Removed a previous change incompatible with PHP 5.1.x and lower.
+
+Drupal 6.7, 2008-12-10
+----------------------
+- Fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
+- Updated robots.txt and .htaccess to match current file use.
+- Fixed a variety of small bugs.
+
+Drupal 6.6, 2008-10-22
+----------------------
+- Fixed security issues, (File inclusion, Cross site scripting), see SA-2008-067
+- Fixed a variety of small bugs.
+
+Drupal 6.5, 2008-10-08
+----------------------
+- Fixed security issues, (File upload access bypass, Access rules bypass,
+  BlogAPI access bypass), see SA-2008-060.
+- Fixed a variety of small bugs.
+
+Drupal 6.4, 2008-08-13
+----------------------
+- Fixed a security issue (Cross site scripting, Arbitrary file uploads via
+  BlogAPI, Cross site request forgeries and Various Upload module
+  vulnerabilities), see SA-2008-047.
+- Improved error messages during installation.
+- Fixed a bug that prevented AHAH handlers to be attached to radios widgets.
+- Fixed a variety of small bugs.
+
+Drupal 6.3, 2008-07-09
+----------------------
+- Fixed security issues, (Cross site scripting, cross site request forgery,
+  session fixation and SQL injection), see SA-2008-044.
+- Slightly modified installation process to prevent file ownership issues on
+  shared hosts.
+- Improved PostgreSQL compatibility (rewritten queries; custom blocks).
+- Upgraded to jQuery 1.2.6.
+- Performance improvements to search, menu handling and form API caches.
+- Fixed Views compatibility issues (Views for Drupal 6 requires Drupal 6.3+).
+- Fixed a variety of small bugs.
+
+Drupal 6.2, 2008-04-09
+----------------------
+- Fixed a variety of small bugs.
+- Fixed a security issue (Access bypasses), see SA-2008-026.
+
+Drupal 6.1, 2008-02-27
+----------------------
+- Fixed a variety of small bugs.
+- Fixed a security issue (Cross site scripting), see SA-2008-018.
+
+Drupal 6.0, 2008-02-13
+----------------------
+- New, faster and better menu system.
+- New watchdog as a hook functionality.
+   * New hook_watchdog that can be implemented by any module to route log
+     messages to various destinations.
+   * Expands the severity levels from 3 (Error, Warning, Notice) to the 8
+     levels defined in RFC 3164.
+   * The watchdog module is now called dblog, and is optional, but enabled by
+     default in the default installation profile.
+   * Extended the database log module so log messages can be filtered.
+   * Added syslog module: useful for monitoring large Drupal installations.
+- Added optional e-mail notifications when users are approved, blocked, or
+  deleted.
+- Drupal works with error reporting set to E_ALL.
+- Added scripts/drupal.sh to execute Drupal code from the command line. Useful
+  to use Drupal as a framework to build command-line tools.
+- Made signature support optional and made it possible to theme signatures.
+- Made it possible to filter the URL aliases on the URL alias administration
+  screen.
+- Language system improvements:
+    * Support for right to left languages.
+    * Language detection based on parts of the URL.
+    * Browser based language detection.
+    * Made it possible to specify a node's language.
+    * Support for translating posts on the site to different languages.
+    * Language dependent path aliases.
+    * Automatically import translations when adding a new language.
+    * JavaScript interface translation.
+    * Automatically import a module's translation upon enabling that module.
+- Moved "PHP input filter" to a standalone module so it can be deleted for
+  security reasons.
+- Usability:
+    * Improved handling of teasers in posts.
+    * Added sticky table headers.
+    * Check for clean URL support automatically with JavaScript.
+    * Removed default/settings.php. Instead the installer will create it from
+      default.settings.php.
+    * Made it possible to configure your own date formats.
+    * Remember anonymous comment posters.
+    * Only allow modules and themes to be enabled that have explicitly been
+      ported to the correct core API version.
+    * Can now specify the minimum PHP version required for a module within the
+      .info file.
+    * Drupal core no longer requires CREATE TEMPORARY TABLES or LOCK TABLES
+      database rights.
+    * Dynamically check password strength and confirmation.
+    * Refactored poll administration.
+    * Implemented drag-and-drop positioning for blocks, menu items, taxonomy
+      vocabularies and terms, forums, profile fields, and input format filters.
+- Theme system:
+    * Added .info files to themes and made it easier to specify regions and
+      features.
+    * Added theme registry: modules can directly provide .tpl.php files for
+      their themes without having to create theme_ functions.
+    * Used the Garland theme for the installation and maintenance pages.
+    * Added theme preprocess functions for themes that are templates.
+    * Added support for themeable functions in JavaScript.
+- Refactored update.php to a generic batch API to be able to run time-consuming
+  operations in multiple subsequent HTTP requests.
+- Installer:
+    * Themed the installer with the Garland theme.
+    * Added form to provide initial site information during installation.
+    * Added ability to provide extra installation steps programmatically.
+    * Made it possible to import interface translations during installation.
+- Added the HTML corrector filter:
+    * Fixes faulty and chopped off HTML in postings.
+    * Tags are now automatically closed at the end of the teaser.
+- Performance:
+    * Made it easier to conditionally load .include files and split up many core
+      modules.
+    * Added a JavaScript aggregator.
+    * Added block-level caching, improving performance for both authenticated
+      and anonymous users.
+    * Made Drupal work correctly when running behind a reverse proxy like
+      Squid or Pound.
+- File handling improvements:
+    * Entries in the files table are now keyed to a user instead of a node.
+    * Added reusable validation functions to check for uploaded file sizes,
+      extensions, and image resolution.
+    * Added ability to create and remove temporary files during a cron job.
+- Forum improvements:
+    * Any node type may now be posted in a forum.
+- Taxonomy improvements:
+    * Descriptions for terms are now shown on taxonomy/term pages as well
+      as RSS feeds.
+    * Added versioning support to categories by associating them with node
+      revisions.
+- Added support for OpenID.
+- Added support for triggering configurable actions.
+- Added the Update status module to automatically check for available updates
+  and warn sites if they are missing security updates or newer versions.
+  Sites deploying from CVS should use http://drupal.org/project/cvs_deploy.
+  Advanced settings provided by http://drupal.org/project/update_advanced.
+- Upgraded the core JavaScript library to jQuery version 1.2.3.
+- Added a new Schema API, which provides built-in support for core and
+  contributed modules to work with databases other than MySQL.
+- Removed drupal.module. The functionality lives on as the Site network
+  contributed module (http://drupal.org/project/site_network).
+- Removed old system updates. Updates from Drupal versions prior to 5.x will
+  require upgrading to 5.x before upgrading to 6.x.
+
+Drupal 5.23, 2010-08-11
+-----------------------
+- Fixed security issues (File download access bypass, Comment unpublishing
+  bypass), see SA-CORE-2010-002.
+
+Drupal 5.22, 2010-03-03
+-----------------------
+- Fixed security issues (Open redirection, Locale module cross site scripting,
+  Blocked user session regeneration), see SA-CORE-2010-001.
+
+Drupal 5.21, 2009-12-16
+-----------------------
+- Fixed a security issue (Cross site scripting), see SA-CORE-2009-009.
+- Fixed a variety of small bugs.
+
+Drupal 5.20, 2009-09-16
+-----------------------
+- Avoid security problems resulting from writing Drupal 6-style menu
+  declarations.
+- Fixed security issues (session fixation), see SA-CORE-2009-008.
+- Fixed a variety of small bugs.
+
+Drupal 5.19, 2009-07-01
+-----------------------
+- Fixed security issues (Cross site scripting and Password leakage in URL), see
+  SA-CORE-2009-007.          
+- Fixed a variety of small bugs.
+
+Drupal 5.18, 2009-05-13
+-----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
+- Fixed a variety of small bugs.
+
+Drupal 5.17, 2009-04-29
+-----------------------
+- Fixed security issues (Cross site scripting and limited information
+  disclosure) see SA-CORE-2009-005.
+- Fixed a variety of small bugs.
+
+Drupal 5.16, 2009-02-25
+-----------------------
+- Fixed a security issue, (Local file inclusion on Windows), see SA-CORE-2009-004.
+- Fixed a variety of small bugs.
+
+Drupal 5.15, 2009-01-14
+-----------------------
+- Fixed security issues, (Hardening against SQL injection), see
+  SA-CORE-2009-001
+- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and basic shell
+  scripts.
+- Fixed a variety of small bugs.
+
+Drupal 5.14, 2008-12-11
+-----------------------
+- removed a previous change incompatible with PHP 5.1.x and lower.
+
+Drupal 5.13, 2008-12-10
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
+- updated robots.txt and .htaccess to match current file use.
+
+Drupal 5.12, 2008-10-22
+-----------------------
+- fixed security issues, (File inclusion), see SA-2008-067
+
+Drupal 5.11, 2008-10-08
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (File upload access bypass, Access rules bypass,
+  BlogAPI access bypass, Node validation bypass), see SA-2008-060
+
+Drupal 5.10, 2008-08-13
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site scripting, Arbitrary file uploads via
+  BlogAPI and Cross site request forgery), see SA-2008-047
+
+Drupal 5.9, 2008-07-23
+----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Session fixation), see SA-2008-046
+
+Drupal 5.8, 2008-07-09
+----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site scripting, cross site request forgery, and
+  session fixation), see SA-2008-044
+
+Drupal 5.7, 2008-01-28
+----------------------
+- fixed the input format configuration page.
+- fixed a variety of small bugs.
+
+Drupal 5.6, 2008-01-10
+----------------------
+- fixed a variety of small bugs.
+- fixed a security issue (Cross site request forgery), see SA-2008-005
+- fixed a security issue (Cross site scripting, UTF8), see SA-2008-006
+- fixed a security issue (Cross site scripting, register_globals), see SA-2008-007
+
+Drupal 5.5, 2007-12-06
+----------------------
+- fixed missing missing brackets in a query in the user module.
+- fixed taxonomy feed bug introduced by SA-2007-031
+
+Drupal 5.4, 2007-12-05
+----------------------
+- fixed a variety of small bugs.
+- fixed a security issue (SQL injection), see SA-2007-031
+
+Drupal 5.3, 2007-10-17
+----------------------
+- fixed a variety of small bugs.
+- fixed a security issue (HTTP response splitting), see SA-2007-024
+- fixed a security issue (Arbitrary code execution via installer), see SA-2007-025
+- fixed a security issue (Cross site scripting via uploads), see SA-2007-026
+- fixed a security issue (User deletion cross site request forgery), see SA-2007-029
+- fixed a security issue (API handling of unpublished comment), see SA-2007-030
+
+Drupal 5.2, 2007-07-26
+----------------------
+- changed hook_link() $teaser argument to match documentation.
+- fixed a variety of small bugs.
+- fixed a security issue (cross-site request forgery), see SA-2007-017
+- fixed a security issue (cross-site scripting), see SA-2007-018
+
+Drupal 5.1, 2007-01-29
+----------------------
+- fixed security issue (code execution), see SA-2007-005
+- fixed a variety of small bugs.
+
+Drupal 5.0, 2007-01-15
+----------------------
+- Completely retooled the administration page
+    * /Admin now contains an administration page which may be themed
+    * Reorganised administration menu items by task and by module
+    * Added a status report page with detailed PHP/MySQL/Drupal information
+- Added web-based installer which can:
+    * Check installation and run-time requirements
+    * Automatically generate the database configuration file
+    * Install pre-made installation profiles or distributions
+    * Import the database structure with automatic table prefixing
+    * Be localized
+- Added new default Garland theme
+- Added color module to change some themes' color schemes
+- Included the jQuery JavaScript library 1.0.4 and converted all core JavaScript to use it
+- Introduced the ability to alter mail sent from system
+- Module system:
+    * Added .info files for module meta-data
+    * Added support for module dependencies
+    * Improved module installation screen
+    * Moved core modules to their own directories
+    * Added support for module uninstalling
+- Added support for different cache backends
+- Added support for a generic "sites/all" directory.
+- Usability:
+    * Added support for auto-complete forms (AJAX) to user profiles.
+    * Made it possible to instantly assign roles to newly created user accounts.
+    * Improved configurability of the contact forms.
+    * Reorganized the settings pages.
+    * Made it easy to investigate popular search terms.
+    * Added a 'select all' checkbox and a range select feature to administration tables.
+    * Simplified the 'break' tag to split teasers from body.
+    * Use proper capitalization for titles, menu items and operations.
+- Integrated urlfilter.module into filter.module
+- Block system:
+    * Extended the block visibility settings with a role specific setting.
+    * Made it possible to customize all block titles.
+- Poll module:
+    * Optionally allow people to inspect all votes.
+    * Optionally allow people to cancel their vote.
+- Distributed authentication:
+    * Added default server option.
+- Added default robots.txt to control crawlers.
+- Database API:
+    * Added db_table_exists().
+- Blogapi module:
+    * 'Blogapi new' and 'blogapi edit' nodeapi operations.
+- User module:
+    * Added hook_profile_alter().
+    * E-mail verification is made optional.
+    * Added mass editing and filtering on admin/user/user.
+- PHP Template engine:
+    * Add the ability to look for a series of suggested templates.
+    * Look for page templates based upon the path.
+    * Look for block templates based upon the region, module, and delta.
+- Content system:
+    * Made it easier for node access modules to work well with each other.
+    * Added configurable content types.
+    * Changed node rendering to work with structured arrays.
+- Performance:
+    * Improved session handling: reduces database overhead.
+    * Improved access checking: reduces database overhead.
+    * Made it possible to do memcached based session management.
+    * Omit sidebars when serving a '404 - Page not found': saves CPU cycles and bandwidth.
+    * Added an 'aggressive' caching policy.
+    * Added a CSS aggregator and compressor (up to 40% faster page loads).
+- Removed the archive module.
+- Upgrade system:
+    * Created space for update branches.
+- Form API:
+    * Made it possible to programmatically submit forms.
+    * Improved api for multistep forms.
+- Theme system:
+    * Split up and removed drupal.css.
+    * Added nested lists generation.
+    * Added a self-clearing block class.
+
+Drupal 4.7.11, 2008-01-10
+-------------------------
+- fixed a security issue (Cross site request forgery), see SA-2008-005
+- fixed a security issue (Cross site scripting, UTF8), see SA-2008-006
+- fixed a security issue (Cross site scripting, register_globals), see SA-2008-007
+
+Drupal 4.7.10, 2007-12-06
+-------------------------
+- fixed taxonomy feed bug introduced by SA-2007-031
+
+Drupal 4.7.9, 2007-12-05
+------------------------
+- fixed a security issue (SQL injection), see SA-2007-031
+
+Drupal 4.7.8, 2007-10-17
+----------------------
+- fixed a security issue (HTTP response splitting), see SA-2007-024
+- fixed a security issue (Cross site scripting via uploads), see SA-2007-026
+- fixed a security issue (API handling of unpublished comment), see SA-2007-030
+
+Drupal 4.7.7, 2007-07-26
+------------------------
+- fixed security issue (XSS), see SA-2007-018
+
+Drupal 4.7.6, 2007-01-29
+------------------------
+- fixed security issue (code execution), see SA-2007-005
+
+Drupal 4.7.5, 2007-01-05
+------------------------
+- Fixed security issue (XSS), see SA-2007-001
+- Fixed security issue (DoS), see SA-2007-002
+
+Drupal 4.7.4, 2006-10-18
+------------------------
+- Fixed security issue (XSS), see SA-2006-024
+- Fixed security issue (CSRF), see SA-2006-025
+- Fixed security issue (Form action attribute injection), see SA-2006-026
+
+Drupal 4.7.3, 2006-08-02
+------------------------
+- Fixed security issue (XSS), see SA-2006-011
+
+Drupal 4.7.2, 2006-06-01
+------------------------
+- Fixed critical upload issue, see SA-2006-007
+- Fixed taxonomy XSS issue, see SA-2006-008
+- Fixed a variety of small bugs.
+
+Drupal 4.7.1, 2006-05-24
+------------------------
+- Fixed critical SQL issue, see SA-2006-005
+- Fixed a serious upgrade related bug.
+- Fixed a variety of small bugs.
+
+Drupal 4.7.0, 2006-05-01
+------------------------
+- Added free tagging support.
+- Added a site-wide contact form.
+- Theme system:
+    * Added the PHPTemplate theme engine and removed the Xtemplate engine.
+    * Converted the bluemarine theme from XTemplate to PHPTemplate.
+    * Converted the pushbutton theme from XTemplate to PHPTemplate.
+- Usability:
+    * Reworked the 'request new password' functionality.
+    * Reworked the node and comment edit forms.
+    * Made it easy to add nodes to the navigation menu.
+    * Added site 'offline for maintenance' feature.
+    * Added support for auto-complete forms (AJAX).
+    * Added support for collapsible page sections (JS).
+    * Added support for resizable text fields (JS).
+    * Improved file upload functionality (AJAX).
+    * Reorganized some settings pages.
+    * Added friendly database error screens.
+    * Improved styling of update.php.
+- Refactored the forms API.
+    * Made it possible to alter, extend or theme forms.
+- Comment system:
+    * Added support for "mass comment operations" to ease repetitive tasks.
+    * Comment moderation has been removed.
+- Node system:
+    * Reworked the revision functionality.
+    * Removed the bookmarklet code. Third-party modules can now handle
+      This.
+- Upgrade system:
+    * Allows contributed modules to plug into the upgrade system.
+- Profiles:
+    * Added a block to display author information along with posts.
+    * Added support for private profile fields.
+- Statistics module:
+    * Added the ability to track page generation times.
+    * Made it possible to block certain IPs/hostnames.
+- Block system:
+    * Added support for theme-specific block regions.
+- Syndication:
+    * Made the aggregator module parse Atom feeds.
+    * Made the aggregator generate RSS feeds.
+    * Added RSS feed settings.
+- XML-RPC:
+    * Replaced the XML-RPC library by a better one.
+- Performance:
+    * Added 'loose caching' option for high-traffic sites.
+    * Improved performance of path aliasing.
+    * Added the ability to track page generation times.
+- Internationalization:
+    * Improved Unicode string handling API.
+    * Added support for PHP's multibyte string module.
+- Added support for PHP5's 'mysqli' extension.
+- Search module:
+    * Made indexer smarter and more robust.
+    * Added advanced search operators (e.g. phrase, node type, ...).
+    * Added customizable result ranking.
+- PostgreSQL support:
+    * Removed dependency on PL/pgSQL procedural language.
+- Menu system:
+    * Added support for external URLs.
+- Queue module:
+    * Removed from core.
+- HTTP handling:
+    * Added support for a tolerant Base URL.
+    * Output URIs relative to the root, without a base tag.
+
+Drupal 4.6.11, 2007-01-05
+-------------------------
+- Fixed security issue (XSS), see SA-2007-001
+- Fixed security issue (DoS), see SA-2007-002
+
+Drupal 4.6.10, 2006-10-18
+------------------------
+- Fixed security issue (XSS), see SA-2006-024
+- Fixed security issue (CSRF), see SA-2006-025
+- Fixed security issue (Form action attribute injection), see SA-2006-026
+
+Drupal 4.6.9, 2006-08-02
+------------------------
+- Fixed security issue (XSS), see SA-2006-011
+
+Drupal 4.6.8, 2006-06-01
+------------------------
+- Fixed critical upload issue, see SA-2006-007
+- Fixed taxonomy XSS issue, see SA-2006-008
+
+Drupal 4.6.7, 2006-05-24
+------------------------
+- Fixed critical SQL issue, see SA-2006-005
+
+Drupal 4.6.6, 2006-03-13
+------------------------
+- Fixed bugs, including 4 security vulnerabilities.
+
+Drupal 4.6.5, 2005-12-12
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.6.4, 2005-11-30
+------------------------
+- Fixed bugs, including 3 security vulnerabilities.
+
+Drupal 4.6.3, 2005-08-15
+------------------------
+- Fixed bugs, including a critical "arbitrary PHP code execution" bug.
+
+Drupal 4.6.2, 2005-06-29
+------------------------
+- Fixed bugs, including two critical "arbitrary PHP code execution" bugs.
+
+Drupal 4.6.1, 2005-06-01
+------------------------
+- Fixed bugs, including a critical input validation bug.
+
+Drupal 4.6.0, 2005-04-15
+------------------------
+- PHP5 compliance
+- Search:
+    * Added UTF-8 support to make it work with all languages.
+    * Improved search indexing algorithm.
+    * Improved search output.
+    * Impose a throttle on indexing of large sites.
+    * Added search block.
+- Syndication:
+    * Made the ping module ping pingomatic.com which, in turn, will ping all the major ping services.
+    * Made Drupal generate RSS 2.0 feeds.
+    * Made RSS feeds extensible.
+    * Added categories to RSS feeds.
+    * Added enclosures to RSS feeds.
+- Flood control mechanism:
+    * Added a mechanism to throttle certain operations.
+- Usability:
+    * Refactored the block configuration pages.
+    * Refactored the statistics pages.
+    * Refactored the watchdog pages.
+    * Refactored the throttle module configuration.
+    * Refactored the access rules page.
+    * Refactored the content administration page.
+    * Introduced forum configuration pages.
+    * Added a 'add child page' link to book pages.
+- Contact module:
+    * Added a simple contact module that allows users to contact each other using e-mail.
+- Multi-site configuration:
+    * Made it possible to run multiple sites from a single code base.
+- Added an image API: enables better image handling.
+- Block system:
+    * Extended the block visibility settings.
+- Theme system:
+    * Added new theme functions.
+- Database backend:
+    * The PEAR database backend is no longer supported.
+- Performance:
+    * Improved performance of the forum topics block.
+    * Improved performance of the tracker module.
+    * Improved performance of the node pages.
+- Documentation:
+    * Improved and extended PHPDoc/Doxygen comments.
+
+Drupal 4.5.8, 2006-03-13
+------------------------
+- Fixed bugs, including 3 security vulnerabilities.
+
+Drupal 4.5.7, 2005-12-12
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.5.6, 2005-11-30
+------------------------
+- Fixed bugs, including 3 security vulnerabilities.
+
+Drupal 4.5.5, 2005-08-15
+------------------------
+- Fixed bugs, including a critical "arbitrary PHP code execution" bug.
+
+Drupal 4.5.4, 2005-06-29
+------------------------
+- Fixed bugs, including two critical "arbitrary PHP code execution" bugs.
+
+Drupal 4.5.3, 2005-06-01
+------------------------
+- Fixed bugs, including a critical input validation bug.
+
+Drupal 4.5.2, 2005-01-15
+------------------------
+- Fixed bugs: a cross-site scripting (XSS) vulnerability has been fixed.
+
+Drupal 4.5.1, 2004-12-01
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.5.0, 2004-10-18
+------------------------
+- Navigation:
+    * Made it possible to add, delete, rename and move menu items.
+    * Introduced tabs and subtabs for local tasks.
+    * Reorganized the navigation menus.
+- User management:
+    * Added support for multiple roles per user.
+    * Made it possible to add custom profile fields.
+    * Made it possible to browse user profiles by field.
+- Node system:
+    * Added support for node-level permissions.
+- Comment module:
+    * Made it possible to leave contact information without having to register.
+- Upload module:
+    * Added support for uploading documents (includes images).
+- Forum module:
+    * Added support for sticky forum topics.
+    * Made it possible to track forum topics.
+- Syndication:
+    * Added support for RSS ping-notifications of http://technorati.com/.
+    * Refactored the categorization of syndicated news items.
+    * Added an URL alias for 'rss.xml'.
+    * Improved date parsing.
+- Database backend:
+    * Added support for multiple database connections.
+    * The PostgreSQL backend does no longer require PEAR.
+- Theme system:
+    * Changed all GIFs to PNGs.
+    * Reorganised the handling of themes, template engines, templates and styles.
+    * Unified and extended the available theme settings.
+    * Added theme screenshots.
+- Blocks:
+    * Added 'recent comments' block.
+    * Added 'categories' block.
+- Blogger API:
+    * Added support for auto-discovery of blogger API via RSD.
+- Performance:
+    * Added support for sending gzip compressed pages.
+    * Improved performance of the forum module.
+- Accessibility:
+    * Improved the accessibility of the archive module's calendar.
+    * Improved form handling and error reporting.
+    * Added HTTP redirects to prevent submitting twice when refreshing right after a form submission.
+- Refactored 403 (forbidden) handling and added support for custom 403 pages.
+- Documentation:
+    * Added PHPDoc/Doxygen comments.
+- Filter system:
+    * Added support for using multiple input formats on the site
+    * Expanded the embedded PHP-code feature so it can be used everywhere
+    * Added support for role-dependent filtering, through input formats
+- UI translation:
+    * Managing translations is now completely done through the administration interface
+    * Added support for importing/exporting gettext .po files
+
+Drupal 4.4.3, 2005-06-01
+------------------------
+- Fixed bugs, including a critical input validation bug.
+
+Drupal 4.4.2, 2004-07-04
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.4.1, 2004-05-01
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.4.0, 2004-04-01
+------------------------
+- Added support for the MetaWeblog API and MovableType extensions.
+- Added a file API: enables better document management.
+- Improved the watchdog and search module to log search keys.
+- News aggregator:
+    * Added support for conditional GET.
+    * Added OPML feed subscription list.
+    * Added support for <image>, <pubDate>, <dc:date>, <dcterms:created>, <dcterms:issued> and <dcterms:modified>.
+- Comment module:
+    * Made it possible to disable the "comment viewing controls".
+- Performance:
+    * Improved module loading when serving cached pages.
+    * Made it possible to automatically disable modules when under heavy load.
+    * Made it possible to automatically disable blocks when under heavy load.
+    * Improved performance and memory footprint of the locale module.
+- Theme system:
+    * Made all theme functions start with 'theme_'.
+    * Made all theme functions return their output.
+    * Migrated away from using the BaseTheme class.
+    * Added many new theme functions and refactored existing theme functions.
+    * Added avatar support to 'Xtemplate'.
+    * Replaced theme 'UnConeD' by 'Chameleon'.
+    * Replaced theme 'Marvin' by 'Pushbutton'.
+- Usability:
+    * Added breadcrumb navigation to all pages.
+    * Made it possible to add context-sensitive help to all pages.
+    * Replaced drop-down menus by radio buttons where appropriate.
+    * Removed the 'magic_quotes_gpc = 0' requirement.
+    * Added a 'book navigation' block.
+- Accessibility:
+    * Made themes degrade gracefully in absence of CSS.
+    * Grouped form elements using '<fieldset>' and '<legend>' tags.
+    * Added '<label>' tags to form elements.
+- Refactored 404 (file not found) handling and added support for custom 404 pages.
+- Improved the filter system to prevent conflicts between filters:
+    * Made it possible to change the order in which filters are applied.
+- Documentation:
+    * Added PHPDoc/Doxygen comments.
+
+Drupal 4.3.2, 2004-01-01
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.3.1, 2003-12-01
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.3.0, 2003-11-01
+------------------------
+- Added support for configurable URLs.
+- Added support for sortable table columns.
+- Database backend:
+    * Added support for selective database table prefixing.
+- Performance:
+    * Optimized many SQL queries for speed by converting left joins to inner joins.
+- Comment module:
+    * Rewrote the comment housekeeping code to be much more efficient and scalable.
+    * Changed the comment module to use the standard pager.
+- User module:
+    * Added support for multiple sessions per user.
+    * Added support for anonymous user sessions.
+- Forum module:
+    * Improved the forum views and the themability thereof.
+- Book module:
+    * Improved integration of non-book nodes in the book outline.
+- Usability:
+    * Added support for "mass node operations" to ease repetitive tasks.
+    * Added support for breadcrumb navigation to several modules' user pages.
+    * Integrated the administration pages with the normal user pages.
+
+Drupal 4.2.0, 2003-08-01
+------------------------
+- Added support for clean URLs.
+- Added textarea hook and support for onload attributes: enables integration of WYSIWYG editors.
+- Rewrote the RSS/RDF parser:
+    * It will now use PHP's built-in XML parser to parse news feeds.
+- Rewrote the administration pages:
+    * Improved the navigational elements and added breadcrumb navigation.
+    * Improved the look and feel.
+    * Added context-sensitive help.
+- Database backend:
+    * Fixed numerous SQL queries to make Drupal ANSI compliant.
+    * Added MSSQL database scheme.
+- Search module:
+    * Changed the search module to use implicit AND'ing instead of implicit OR'ing.
+- Node system:
+    * Replaced the "post content" permission by more fine-grained permissions.
+    * Improved content submission:
+        + Improved teasers: teasers are now optional, teaser length can be configured, teaser and body are edited in a single textarea, users will no longer be bothered with teasers when the post is too short for one.
+        + Added the ability to preview both the short and the full version of your posts.
+    * Extended the node API which allows for better integration.
+    * Added default node settings to control the behavior for promotion, moderation and other options.
+- Themes:
+    * Replaced theme "Goofy" by "Xtemplate", a template driven theme.
+- Removed the 'register_globals = on' requirement.
+- Added better installation instructions.
+
+Drupal 4.1.0, 2003-02-01
+------------------------
+- Collaboratively revised and expanded the Drupal documentation.
+- Rewrote comment.module:
+    * Reintroduced comment rating/moderation.
+    * Added support for comment paging.
+    * Performance improvements: improved comment caching, faster SQL queries, etc.
+- Rewrote block.module:
+    * Performance improvements: blocks are no longer rendered when not displayed.
+- Rewrote forum.module:
+    * Added a lot of features one can find in stand-alone forum software including but not limited to support for topic paging, added support for icons, rewrote the statistics module, etc.
+- Rewrote statistics.module:
+    * Collects access counts for each node, referrer logs, number of users/guests.
+    * Export blocks displaying top viewed nodes over last 24 hour period, top viewed nodes over all time, last nodes viewed, how many users/guest online.
+- Added throttle.module:
+    * Auto-throttle congestion control mechanism: Drupal can adapt itself based on the server load.
+- Added profile.module:
+    * Enables to extend the user and registration page.
+- Added pager support to the main page.
+- Replaced weblogs.module by ping.module:
+    * Added support for normal and RSS notifications of http://blo.gs/.
+    * Added support for RSS ping-notifications of http://weblogs.com/.
+- Removed the rating module
+- Themes:
+    * Removed a significant portion of hard-coded mark-up.
+
+Drupal 4.0.0, 2002-06-15
+------------------------
+- Added tracker.module:
+    * Replaces the previous "your [site]" links (recent comments and nodes).
+- Added weblogs.module:
+    * This will ping weblogs.com when new content is promoted.
+- Added taxonomy module which replaces the meta module.
+    * Supports relations, hierarchies and synonyms.
+- Added a caching system:
+    * Speeds up pages for anonymous users and reduces system load.
+- Added support for external SMTP libraries.
+- Added an archive extension to the calendar.
+- Added support for the Blogger API.
+- Themes:
+    * Cleaned up the theme system.
+    * Moved themes that are not maintained to contributions CVS repository.
+- Database backend:
+    * Changed to PEAR database abstraction layer.
+    * Using ANSI SQL queries to be more portable.
+- Rewrote the user system:
+    * Added support for Drupal authentication through XML-RPC and through a Jabber server.
+    * Added support for modules to add more user data.
+    * Users may delete their own account.
+    * Added who's new and who's online blocks.
+- Changed block system:
+    * Various hard coded blocks are now dynamic.
+    * Blocks can now be enabled and/or be set by the user.
+    * Blocks can be set to only show up on some pages.
+    * Merged box module with block module.
+- Node system:
+    * Blogs can be updated.
+    * Teasers (abstracts) on all node types.
+    * Improved error checking.
+    * Content versioning support.
+    * Usability improvements.
+- Improved book module to support text, HTML and PHP pages.
+- Improved comment module to mark new comments.
+- Added a general outliner which will let any node type be linked to a book.
+- Added an update script that lets you upgrade from previous releases or on a day to day basis when using the development tree.
+- Search module:
+    * Improved the search system by making it context sensitive.
+    * Added indexing.
+- Various updates:
+    * Changed output to valid XHTML.
+    * Improved multiple sites using the same Drupal database support.
+    * Added support for session IDs in URLs instead of cookies.
+    * Made the type of content on the front page configurable.
+    * Made each cloud site have its own settings.
+    * Modules and themes can now be enabled/disabled using the administration pages.
+    * Added URL abstraction for links.
+    * Usability changes (renamed links, better UI, etc).
+- Collaboratively revised and expanded the Drupal documentation.
+
+Drupal 3.0.1, 2001-10-15
+------------------------
+- Various updates:
+    * Added missing translations
+    * Updated the themes: tidied up some HTML code and added new Drupal logos.
+
+Drupal 3.0.0, 2001-09-15
+------------------------
+- Major overhaul of the entire underlying design:
+    * Everything is based on nodes: nodes are a conceptual "black box" to couple and manage different types of content and that promotes reusing existing code, thus reducing the complexity and size of Drupal as well as improving long-term stability.
+- Rewrote submission/moderation queue and renamed it to queue.module.
+- Removed FAQ and documentation module and merged them into a "book module".
+- Removed ban module and integrated it into account.module as "access control":
+    * Access control is based on much more powerful regular expressions (regex) now rather than on MySQL pattern matching.
+- Rewrote watchdog and submission throttle:
+    * Improved watchdog messages and added watchdog filter.
+- Rewrote headline code and renamed it to import.module and export.module:
+    * Added various improvements, including a better parser, bundles and better control over individual feeds.
+- Rewrote section code and renamed it to meta.module:
+    * Supports unlimited amount of nested topics. Topics can be nested to create a multi-level hierarchy.
+- Rewrote configuration file resolving:
+    * Drupal tries to locate a configuration file that matches your domain name or uses conf.php if the former failed. Note also that the configuration files got renamed from .conf to .php for security's sake on mal-configured Drupal sites.
+- Added caching support which makes Drupal extremely scalable.
+- Added access.module:
+    * Allows you to set up 'roles' (groups) and to bind a set of permissions to each group.
+- Added blog.module.
+- Added poll.module.
+- Added system.module:
+    * Moved most of the configuration options from hostname.conf to the new administration section.
+    * Added support for custom "filters".
+- Added statistics.module
+- Added moderate.module:
+    * Allows to assign users editorial/moderator rights to certain nodes or topics.
+- Added page.module:
+    * Allows creation of static (and dynamic) pages through the administration interface.
+- Added help.module:
+    * Groups all available module documentation on a single page.
+- Added forum.module:
+    * Added an integrated forum.
+- Added cvs.module and cvs-to-sql.pl:
+    * Allows to display and mail CVS log messages as daily digests.
+- Added book.module:
+    * Allows collaborative handbook writing: primary used for Drupal documentation.
+- Removed cron.module and integrated it into conf.module.
+- Removed module.module as it was no longer needed.
+- Various updates:
+    * Added "auto-post new submissions" feature versus "moderate new submissions".
+    * Introduced links/Drupal tags: [[link]]
+    * Added preview functionality when submitting new content (such as a story) from the administration pages.
+    * Made the administration section only show those links a user has access to.
+    * Made all modules use specific form_* functions to guarantee a rock-solid forms and more consistent layout.
+    * Improved scheduler:
+        + Content can be scheduled to be 'posted', 'queued' and 'hidden'.
+    * Improved account module:
+        + Added "access control" to allow/deny certain usernames/e-mail addresses/hostnames.
+    * Improved locale module:
+        + Added new overview to easy the translation process.
+    * Improved comment module:
+        + Made it possible to permanently delete comments.
+    * Improved rating module
+    * Improved story module:
+        + Added preview functionality for administrators.
+        + Made it possible to permanently delete stories.
+    * Improved themes:
+        + W3C validation on a best effort basis.
+        + Removed $theme->control() from themes.
+        + Added theme "goofy".
+- Collaboratively revised and expanded the Drupal documentation.
+
+Drupal 2.0.0, 2001-03-15
+------------------------
+- Rewrote the comment/discussion code:
+    * Comment navigation should be less confusing now.
+    * Additional/alternative display and order methods have been added.
+    * Modules can be extended with a "comment system": modules can embed the existing comment system without having to write their own, duplicate comment system.
+- Added sections and section manager:
+    * Story sections can be maintained from the administration pages.
+    * Story sections make the open submission more adaptive in that you can set individual post, dump and expiration thresholds for each section according to the story type and urgency level: stories in certain sections do not "expire" and might stay interesting and active as time passes by, whereas news-related stories are only considered "hot" over a short period of time.
+- Multiple vhosts + multiple directories:
+    * You can set up multiple Drupal sites on top of the same physical source tree either by using vhosts or sub-directories.
+- Added "user ratings" similar to SlashCode's Karma or Scoop's Mojo:
+    * All rating logic is packed into a module to ease experimenting with different rating heuristics/algorithms.
+- Added "search infrastructure":
+    * Improved search page and integrated search functionality in the administration pages.
+- Added translation / localization / internationalization support:
+    * Because many people would love to see their website showing a lot less of English, and far more of their own language, Drupal provides a framework to set up a multi-lingual website or to overwrite the default English text in English.
+- Added fine-grained user permission (or group) system:
+    * Users can be granted access to specific administration sections. Example: a FAQ maintainer can be given access to maintain the FAQ and translators can be given access to the translation pages.
+- Added FAQ module
+- Changed the "open submission queue" into a (optional) module.
+- Various updates:
+    * Improved account module:
+        + User accounts can be deleted.
+        + Added fine-grained permission support.
+    * Improved block module
+    * Improved diary module:
+        + Diary entries can be deleted
+    * Improved headline module:
+        + Improved parser to support more "generic" RDF/RSS/XML backend.
+    * Improved module module
+    * Improved watchdog module
+    * Improved database abstraction layer
+    * Improved themes:
+        + W3C validation on a best effort basis
+        + Added theme "example" (alias "Stone Age")
+    * Added new scripts to directory "scripts"
+    * Added directory "misc"
+    * Added CREDITS file
+- Revised documentation
+
+Drupal 1.0.0, 2001-01-15
+------------------------
+- Initial release

+ 44 - 0
COPYRIGHT.txt

@@ -0,0 +1,44 @@
+All Drupal code is Copyright 2001 - 2012 by the original authors.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or (at
+your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program as the file LICENSE.txt; if not, please see
+http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+Drupal is a registered trademark of Dries Buytaert.
+
+Drupal includes works under other copyright notices and distributed
+according to the terms of the GNU General Public License or a compatible
+license, including:
+
+Javascript
+
+  Farbtastic - Copyright (c) 2010 Matt Farina
+
+  jQuery - Copyright (c) 2010 John Resig
+
+  jQuery BBQ - Copyright (c) 2010 "Cowboy" Ben Alman
+
+  jQuery Cookie - Copyright (c) 2006 Klaus Hartl
+
+  jQuery Form - Copyright (c) 2010 Mike Alsup
+
+  jQuery Once - Copyright (c) 2009 Konstantin K�fer
+
+  jQuery UI - Copyright (c) 2010 by the original authors
+    (http://jqueryui.com/about)
+
+  Sizzle.js - Copyright (c) 2010 The Dojo Foundation (http://sizzlejs.com/)
+
+PHP
+
+  ArchiveTar - Copyright (c) 1997 - 2008 Vincent Blavet

+ 42 - 0
INSTALL.mysql.txt

@@ -0,0 +1,42 @@
+
+CREATE THE MySQL DATABASE
+--------------------------
+
+This step is only necessary if you don't already have a database set up (e.g.,
+by your host). In the following examples, 'username' is an example MySQL user
+which has the CREATE and GRANT privileges. Use the appropriate user name for
+your system.
+
+First, you must create a new database for your Drupal site (here, 'databasename'
+is the name of the new database):
+
+  mysqladmin -u username -p create databasename
+
+MySQL will prompt for the 'username' database password and then create the
+initial database files. Next you must log in and set the access database rights:
+
+  mysql -u username -p
+
+Again, you will be asked for the 'username' database password. At the MySQL
+prompt, enter the following command:
+
+  GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER
+  ON databasename.*
+  TO 'username'@'localhost' IDENTIFIED BY 'password';
+
+where
+
+ 'databasename' is the name of your database
+ 'username@localhost' is the username of your MySQL account
+ 'password' is the password required for that username
+
+Note: Unless your database user has all of the privileges listed above, you will
+not be able to run Drupal.
+
+If successful, MySQL will reply with:
+
+  Query OK, 0 rows affected
+
+If the InnoDB storage engine is available, it will be used for all database
+tables. InnoDB provides features over MyISAM such as transaction support,
+row-level locks, and consistent non-locking reads.

+ 44 - 0
INSTALL.pgsql.txt

@@ -0,0 +1,44 @@
+
+CREATE THE PostgreSQL DATABASE
+------------------------------
+
+Note that the database must be created with UTF-8 (Unicode) encoding.
+
+1. CREATE DATABASE USER
+
+   This step is only necessary if you don't already have a user set up (e.g., by
+   your host), or want to create a new user for use with Drupal only. The
+   following command creates a new user named 'username' and asks for a password
+   for that user:
+
+     createuser --pwprompt --encrypted --no-createrole --no-createdb username
+
+   If there are no errors, then the command was successful.
+
+2. CREATE DRUPAL DATABASE
+
+   This step is only necessary if you don't already have a database set up
+   (e.g., by your host) or want to create a new database for use with Drupal
+   only. The following command creates a new database named 'databasename',
+   which is owned by the previously created 'username':
+
+     createdb --encoding=UTF8 --owner=username databasename
+
+   If there are no errors, then the command was successful.
+
+3. CREATE SCHEMA OR SCHEMAS (Optional advanced step)
+
+   Drupal will run across different schemas within your database if you so wish.
+   By default, Drupal runs inside the 'public' schema but you can use $db_prefix
+   inside settings.php to define a schema for Drupal to run inside of, or
+   specify tables that are shared inside of a separate schema. Drupal will not
+   create schemas for you. In fact, the user that Drupal runs as should not be
+   allowed to do this. You'll need to execute the SQL below as a superuser,
+   replace 'username' with the username that Drupal uses to connect to
+   PostgreSQL, and replace 'schema_name' with a schema name you wish to use,
+   such as 'shared':
+
+     CREATE SCHEMA schema_name AUTHORIZATION username;
+
+   Do this for as many schemas as you need. See default.settings.php for
+   instructions on how to set which tables use which schemas.

+ 31 - 0
INSTALL.sqlite.txt

@@ -0,0 +1,31 @@
+
+SQLITE REQUIREMENTS
+-------------------
+
+To use SQLite with your Drupal installation, the following requirements must be
+met: Server has PHP 5.2 or later with PDO, and the PDO SQLite driver must be
+enabled.
+
+SQLITE DATABASE CREATION
+------------------------
+
+The Drupal installer will create the SQLite database for you. The only
+requirement is that the installer must have write permissions to the directory
+where the database file resides. This directory (not just the database file) also
+has to remain writeable by the web server going forward for SQLite to continue to
+be able to operate.
+
+On the "Database configuration" form in the "Database file" field, you must
+supply the exact path to where you wish your database file to reside. It is
+strongly suggested that you choose a path that is outside of the webroot, yet
+ensure that the directory is writeable by the web server.
+
+If you must place your database file in your webroot, you could try using the
+following in your "Database file" field:
+
+  sites/default/files/.ht.sqlite
+
+Note: The .ht in the name will tell Apache to prevent the database from being
+downloaded. Please check that the file is, indeed, protected by your webserver.
+If not, please consult the documentation of your webserver on how to protect a
+file from downloading.

+ 398 - 0
INSTALL.txt

@@ -0,0 +1,398 @@
+
+CONTENTS OF THIS FILE
+---------------------
+
+ * Requirements and notes
+ * Optional server requirements
+ * Installation
+ * Building and customizing your site
+ * Multisite configuration
+ * More information
+
+REQUIREMENTS AND NOTES
+----------------------
+
+Drupal requires:
+
+- A web server. Apache (version 2.0 or greater) is recommended.
+- PHP 5.2.4 (or greater) (http://www.php.net/).
+- One of the following databases:
+  - MySQL 5.0.15 (or greater) (http://www.mysql.com/).
+  - MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully
+    compatible drop-in replacement for MySQL.
+  - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/).
+  - SQLite 3.4.2 (or greater) (http://www.sqlite.org/).
+
+For more detailed information about Drupal requirements, including a list of
+PHP extensions and configurations that are required, see "System requirements"
+(http://drupal.org/requirements) in the Drupal.org online documentation.
+
+For detailed information on how to configure a test server environment using a
+variety of operating systems and web servers, see "Local server setup"
+(http://drupal.org/node/157602) in the Drupal.org online documentation.
+
+Note that all directories mentioned in this document are always relative to the
+directory of your Drupal installation, and commands are meant to be run from
+this directory (except for the initial commands that create that directory).
+
+OPTIONAL SERVER REQUIREMENTS
+----------------------------
+
+- If you want to use Drupal's "Clean URLs" feature on an Apache web server, you
+  will need the mod_rewrite module and the ability to use local .htaccess
+  files. For Clean URLs support on IIS, see "Clean URLs with IIS"
+  (http://drupal.org/node/3854) in the Drupal.org online documentation.
+
+- If you plan to use XML-based services such as RSS aggregation, you will need
+  PHP's XML extension. This extension is enabled by default on most PHP
+  installations.
+
+- To serve gzip compressed CSS and JS files on an Apache web server, you will
+  need the mod_headers module and the ability to use local .htaccess files.
+
+- Some Drupal functionality (e.g., checking whether Drupal and contributed
+  modules need updates, RSS aggregation, etc.) require that the web server be
+  able to go out to the web and download information. If you want to use this
+  functionality, you need to verify that your hosting provider or server
+  configuration allows the web server to initiate outbound connections. Most web
+  hosting setups allow this.
+
+INSTALLATION
+------------
+
+1. Download and extract Drupal.
+
+   You can obtain the latest Drupal release from http://drupal.org -- the files
+   are available in .tar.gz and .zip formats and can be extracted using most
+   compression tools.
+
+   To download and extract the files, on a typical Unix/Linux command line, use
+   the following commands (assuming you want version x.y of Drupal in .tar.gz
+   format):
+
+     wget http://drupal.org/files/projects/drupal-x.y.tar.gz
+     tar -zxvf drupal-x.y.tar.gz
+
+   This will create a new directory drupal-x.y/ containing all Drupal files and
+   directories. Then, to move the contents of that directory into a directory
+   within your web server's document root or your public HTML directory,
+   continue with this command:
+
+     mv drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
+
+2. Optionally, download a translation.
+
+   By default, Drupal is installed in English, and further languages may be
+   installed later. If you prefer to install Drupal in another language
+   initially:
+
+   - Download a translation file for the correct Drupal version and language
+     from the translation server: http://localize.drupal.org/translate/downloads
+
+   - Place the file into your installation profile's translations directory.
+     For instance, if you are using the Standard installation profile,
+     move the .po file into the directory:
+
+       profiles/standard/translations/
+
+   For detailed instructions, visit http://drupal.org/localize
+
+3. Create the Drupal database.
+
+   Because Drupal stores all site information in a database, you must create
+   this database in order to install Drupal, and grant Drupal certain database
+   privileges (such as the ability to create tables). For details, consult
+   INSTALL.mysql.txt, INSTALL.pgsql.txt, or INSTALL.sqlite.txt. You may also
+   need to consult your web hosting provider for instructions specific to your
+   web host.
+
+   Take note of the username, password, database name, and hostname as you
+   create the database. You will enter this information during the install.
+
+4. Run the install script.
+
+   To run the install script, point your browser to the base URL of your
+   website (e.g., http://www.example.com).
+
+   You will be guided through several screens to set up the database, add the
+   site maintenance account (the first user, also known as user/1), and provide
+   basic web site settings.
+
+   During installation, several files and directories need to be created, which
+   the install script will try to do automatically. However, on some hosting
+   environments, manual steps are required, and the install script will tell
+   you that it cannot proceed until you fix certain issues. This is normal and
+   does not indicate a problem with your server.
+
+   The most common steps you may need to perform are:
+
+   a. Missing files directory.
+
+      The install script will attempt to create a file storage directory in
+      the default location at sites/default/files (the location of the files
+      directory may be changed after Drupal is installed).
+
+      If auto-creation fails, you can make it work by changing permissions on
+      the sites/default directory so that the web server can create the files
+      directory within it for you. (If you are creating a multisite
+      installation, substitute the correct sites directory for sites/default;
+      see the Multisite Configuration section of this file, below.)
+
+      For example, on a Unix/Linux command line, you can grant everyone
+      (including the web server) permission to write to the sites/default
+      directory with this command:
+
+        chmod a+w sites/default
+
+      Be sure to set the permissions back after the installation is finished!
+      Sample command:
+
+        chmod go-w sites/default
+
+      Alternatively, instead of allowing the web server to create the files
+      directory for you as described above, you can create it yourself. Sample
+      commands from a Unix/Linux command line:
+
+        mkdir sites/default/files
+        chmod a+w sites/default/files
+
+   b. Missing settings file.
+
+      Drupal will try to automatically create a settings.php configuration file,
+      which is normally in the directory sites/default (to avoid problems when
+      upgrading, Drupal is not packaged with this file). If auto-creation fails,
+      you will need to create this file yourself, using the file
+      sites/default/default.settings.php as a template.
+
+      For example, on a Unix/Linux command line, you can make a copy of the
+      default.settings.php file with the command:
+
+        cp sites/default/default.settings.php sites/default/settings.php
+
+      Next, grant write privileges to the file to everyone (including the web
+      server) with the command:
+
+        chmod a+w sites/default/settings.php
+
+      Be sure to set the permissions back after the installation is finished!
+      Sample command:
+
+        chmod go-w sites/default/settings.php
+
+   c. Write permissions after install.
+
+      The install script will attempt to write-protect the settings.php file and
+      the sites/default directory after saving your configuration. If this
+      fails, you will be notified, and you can do it manually. Sample commands
+      from a Unix/Linux command line:
+
+        chmod go-w sites/default/settings.php
+        chmod go-w sites/default
+
+5. Verify that the site is working.
+
+   When the install script finishes, you will be logged in with the site
+   maintenance account on a "Welcome" page. If the default Drupal theme is not
+   displaying properly and links on the page result in "Page Not Found" errors,
+   you may be experiencing problems with clean URLs. Visit
+   http://drupal.org/getting-started/clean-urls to troubleshoot.
+
+6. Change file system storage settings (optional).
+
+   The files directory created in step 4 is the default file system path used to
+   store all uploaded files, as well as some temporary files created by
+   Drupal. After installation, you can modify the file system path to store
+   uploaded files in a different location.
+
+   It is not necessary to modify this path, but you may wish to change it if:
+
+   - Your site runs multiple Drupal installations from a single codebase (modify
+     the file system path of each installation to a different directory so that
+     uploads do not overlap between installations).
+
+   - Your site runs on a number of web servers behind a load balancer or reverse
+     proxy (modify the file system path on each server to point to a shared file
+     repository).
+
+   - You want to restrict access to uploaded files.
+
+   To modify the file system path:
+
+   a. Ensure that the new location for the path exists and is writable by the
+      web server. For example, to create a new directory named uploads and grant
+      write permissions, use the following commands on a Unix/Linux command
+      line:
+
+        mkdir uploads
+        chmod a+w uploads
+
+   b. Navigate to Administration > Configuration > Media > File system, and
+      enter the desired path. Note that if you want to use private file storage,
+      you need to first enter the path for private files and save the
+      configuration, and then change the "Default download method" setting and
+      save again.
+
+   Changing the file system path after files have been uploaded may cause
+   unexpected problems on an existing site. If you modify the file system path
+   on an existing site, remember to copy all files from the original location
+   to the new location.
+
+7. Revoke documentation file permissions (optional).
+
+   Some administrators suggest making the documentation files, especially
+   CHANGELOG.txt, non-readable so that the exact version of Drupal you are
+   running is slightly more difficult to determine. If you wish to implement
+   this optional security measure, from a Unix/Linux command line you can use
+   the following command:
+
+     chmod a-r CHANGELOG.txt
+
+   Note that the example only affects CHANGELOG.txt. To completely hide all
+   documentation files from public view, repeat this command for each of the
+   Drupal documentation files in the installation directory, substituting the
+   name of each file for CHANGELOG.txt in the example.
+
+   For more information on setting file permissions, see "Modifying Linux,
+   Unix, and Mac file permissions" (http://drupal.org/node/202483) or
+   "Modifying Windows file permissions" (http://drupal.org/node/202491) in the
+   Drupal.org online documentation.
+
+8. Set up independent "cron" maintenance jobs.
+
+   Many Drupal modules have tasks that must be run periodically, including the
+   Search module (building and updating the index used for keyword searching),
+   the Aggregator module (retrieving feeds from other sites), and the System
+   module (performing routine maintenance and pruning of database tables). These
+   tasks are known as "cron maintenance tasks", named after the Unix/Linux
+   "cron" utility.
+
+   When you install Drupal, its built-in cron feature is enabled, which
+   automatically runs the cron tasks periodically, triggered by people visiting
+   pages of your site. You can configure the built-in cron feature by navigating
+   to Administration > Configuration > System > Cron.
+
+   It is also possible to run the cron tasks independent of site visits; this is
+   recommended for most sites. To do this, you will need to set up an automated
+   process to visit the page cron.php on your site, which executes the cron
+   tasks.
+
+   The URL of the cron.php page requires a "cron key" to protect against
+   unauthorized access. Your site's cron key is automatically generated during
+   installation and is specific to your site. The full URL of the page, with the
+   cron key, is available in the "Cron maintenance tasks" section of the Status
+   report page at Administration > Reports > Status report.
+
+   As an example for how to set up this automated process, you can use the
+   crontab utility on Unix/Linux systems. The following crontab line uses the
+   wget command to visit the cron.php page, and runs each hour, on the hour:
+
+   0 * * * * wget -O - -q -t 1 http://example.com/cron.php?cron_key=YOURKEY
+
+   Replace the text "http://example.com/cron.php?cron_key=YOURKEY" in the
+   example with the full URL displayed under "Cron maintenance tasks" on the
+   "Status report" page.
+
+   More information about cron maintenance tasks is available at
+   http://drupal.org/cron, and sample cron shell scripts can be found in the
+   scripts/ directory. (Note that these scripts must be customized like the
+   above example, to add your site-specific cron key and domain name.)
+
+BUILDING AND CUSTOMIZING YOUR SITE
+----------------------------------
+
+A new installation of Drupal defaults to a very basic configuration. To extend
+your site, you use "modules" and "themes". A module is a plugin that adds
+functionality to Drupal, while a theme changes the look of your site. The core
+of Drupal provides several optional modules and themes, and you can download
+more at http://drupal.org/project/modules and http://drupal.org/project/themes
+
+Do not mix downloaded or custom modules and themes with Drupal's core modules
+and themes. Drupal's modules and themes are located in the top-level modules and
+themes directories, while the modules and themes you add to Drupal are normally
+placed in the sites/all/modules and sites/all/themes directories. If you run a
+multisite installation, you can also place modules and themes in the
+site-specific directories -- see the Multisite Configuration section, below.
+
+Never edit Drupal's core modules and themes; instead, use the hooks available in
+the Drupal API. To modify the behavior of Drupal, develop a module as described
+at http://drupal.org/developing/modules. To modify the look of Drupal, create a
+subtheme as described at http://drupal.org/node/225125, or a completely new
+theme as described at http://drupal.org/documentation/theme
+
+MULTISITE CONFIGURATION
+-----------------------
+
+A single Drupal installation can host several Drupal-powered sites, each with
+its own individual configuration.
+
+Additional site configurations are created in subdirectories within the 'sites'
+directory. Each subdirectory must have a 'settings.php' file, which specifies
+the configuration settings. The easiest way to create additional sites is to
+copy the 'default' directory and modify the 'settings.php' file as appropriate.
+The new directory name is constructed from the site's URL. The configuration for
+www.example.com could be in 'sites/example.com/settings.php' (note that 'www.'
+should be omitted if users can access your site at http://example.com/).
+
+Sites do not have to have a different domain. You can also use subdomains and
+subdirectories for Drupal sites. For example, example.com, sub.example.com, and
+sub.example.com/site3 can all be defined as independent Drupal sites. The setup
+for a configuration such as this would look like the following:
+
+  sites/default/settings.php
+  sites/example.com/settings.php
+  sites/sub.example.com/settings.php
+  sites/sub.example.com.site3/settings.php
+
+When searching for a site configuration (for example www.sub.example.com/site3),
+Drupal will search for configuration files in the following order, using the
+first configuration it finds:
+
+  sites/www.sub.example.com.site3/settings.php
+  sites/sub.example.com.site3/settings.php
+  sites/example.com.site3/settings.php
+  sites/www.sub.example.com/settings.php
+  sites/sub.example.com/settings.php
+  sites/example.com/settings.php
+  sites/default/settings.php
+
+If you are installing on a non-standard port, the port number is treated as the
+deepest subdomain. For example: http://www.example.com:8080/ could be loaded
+from sites/8080.www.example.com/. The port number will be removed according to
+the pattern above if no port-specific configuration is found, just like a real
+subdomain.
+
+Each site configuration can have its own site-specific modules and themes in
+addition to those installed in the standard 'modules' and 'themes' directories.
+To use site-specific modules or themes, simply create a 'modules' or 'themes'
+directory within the site configuration directory. For example, if
+sub.example.com has a custom theme and a custom module that should not be
+accessible to other sites, the setup would look like this:
+
+  sites/sub.example.com/
+    settings.php
+    themes/custom_theme
+    modules/custom_module
+
+NOTE: for more information about multiple virtual hosts or the configuration
+settings, consult http://drupal.org/getting-started/6/install/multi-site
+
+For more information on configuring Drupal's file system path in a multisite
+configuration, see step 6 above.
+
+MORE INFORMATION
+----------------
+
+- See the Drupal.org online documentation:
+  http://drupal.org/documentation
+
+- For a list of security announcements, see the "Security advisories" page at
+  http://drupal.org/security (available as an RSS feed). This page also
+  describes how to subscribe to these announcements via e-mail.
+
+- For information about the Drupal security process, or to find out how to
+  report a potential security issue to the Drupal security team, see the
+  "Security team" page at http://drupal.org/security-team
+
+- For information about the wide range of available support options, visit
+  http://drupal.org and click on Community and Support in the top or bottom
+  navigation.

+ 339 - 0
LICENSE.txt

@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.

+ 303 - 0
MAINTAINERS.txt

@@ -0,0 +1,303 @@
+
+Drupal core is built and maintained by the Drupal project community. Everyone is
+encouraged to submit issues and changes (patches) to improve Drupal, and to
+contribute in other ways -- see http://drupal.org/contribute to find out how.
+
+Branch maintainers
+------------------
+
+The Drupal Core branch maintainers oversee the development of Drupal as a whole.
+The branch maintainers for Drupal 7 are:
+
+- Dries Buytaert 'dries' http://drupal.org/user/1
+- Angela Byron 'webchick' http://drupal.org/user/24967
+- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+
+
+Component maintainers
+---------------------
+
+The Drupal Core component maintainers oversee the development of Drupal
+subsystems. See http://drupal.org/contribute/core-maintainers for more
+information on their responsibilities, and to find out how to become a component
+maintainer. Current component maintainers for Drupal 7:
+
+Ajax system
+- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
+- Earl Miles 'merlinofchaos' http://drupal.org/user/26979
+
+Base system
+- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Damien Tournoud 'DamZ' http://drupal.org/user/22211
+- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
+
+Batch system
+- Yves Chedemois 'yched' http://drupal.org/user/39567
+
+Cache system
+- Damien Tournoud 'DamZ' http://drupal.org/user/22211
+- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+
+Cron system
+- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Derek Wright 'dww' http://drupal.org/user/46549
+
+Database system
+- Larry Garfield 'Crell' http://drupal.org/user/26398
+
+  - MySQL driver
+    - Larry Garfield 'Crell' http://drupal.org/user/26398
+    - David Strauss 'David Strauss' http://drupal.org/user/93254
+
+  - PostgreSQL driver
+    - Damien Tournoud 'DamZ' http://drupal.org/user/22211
+    - Josh Waihi 'fiasco' http://drupal.org/user/188162
+
+  - Sqlite driver
+    - Damien Tournoud 'DamZ' http://drupal.org/user/22211
+    - Károly Négyesi 'chx' http://drupal.org/user/9446
+
+Database update system
+- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Ashok Modi 'BTMash' http://drupal.org/user/60422
+
+Entity system
+- Wolfgang Ziegler 'fago' http://drupal.org/user/16747
+- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Franz Heinzmann 'Frando' http://drupal.org/user/21850
+
+File system
+- Andrew Morton 'drewish' http://drupal.org/user/34869
+- Aaron Winborn 'aaron' http://drupal.org/user/33420
+
+Form system
+- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
+- Wolfgang Ziegler 'fago' http://drupal.org/user/16747
+- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Franz Heinzmann 'Frando' http://drupal.org/user/21850
+
+Image system
+- Andrew Morton 'drewish' http://drupal.org/user/34869
+- Nathan Haug 'quicksketch' http://drupal.org/user/35821
+
+Install system
+- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+
+JavaScript
+- Théodore Biadala 'nod_' http://drupal.org/user/598310
+- Steve De Jonghe 'seutje' http://drupal.org/user/264148
+- Jesse Renée Beach 'jessebeach' http://drupal.org/user/748566
+
+Language system
+- Francesco Placella 'plach' http://drupal.org/user/183211
+- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+
+Lock system
+- Damien Tournoud 'DamZ' http://drupal.org/user/22211
+
+Mail system
+- ?
+
+Markup
+- Jacine Luisi 'Jacine' http://drupal.org/user/88931
+- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+
+Menu system
+- Peter Wolanin 'pwolanin' http://drupal.org/user/49851
+- Károly Négyesi 'chx' http://drupal.org/user/9446
+
+Path system
+- Dave Reid 'davereid' http://drupal.org/user/53892
+- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+
+Render system
+- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
+- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
+- Franz Heinzmann 'Frando' http://drupal.org/user/21850
+
+Theme system
+- Earl Miles 'merlinofchaos' http://drupal.org/user/26979
+- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
+- Joon Park 'dvessel' http://drupal.org/user/56782
+- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+
+Token system
+- Dave Reid 'davereid' http://drupal.org/user/53892
+
+XML-RPC system
+- Frederic G. Marand 'fgm' http://drupal.org/user/27985
+
+
+Topic coordinators
+------------------
+
+Accessibility
+- Everett Zufelt 'Everett Zufelt' http://drupal.org/user/406552
+- Brandon Bowersox-Johnson 'bowersox' http://drupal.org/user/186415
+
+Documentation
+- Jennifer Hodgdon 'jhodgdon' http://drupal.org/user/155601
+
+Security
+- Greg Knaddison 'greggles' http://drupal.org/user/36762
+
+Translations
+- Gerhard Killesreiter 'killes' http://drupal.org/user/83
+
+User experience and usability
+- Roy Scholten 'yoroy' http://drupal.org/user/41502
+- Bojhan Somers 'Bojhan' http://drupal.org/user/87969
+
+Node Access
+- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
+- Ken Rickard 'agentrickard' http://drupal.org/user/20975
+- Jess Myrbo 'xjm' http://drupal.org/user/65776
+
+Module maintainers
+------------------
+
+Aggregator module
+- ?
+
+Block module
+- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+
+Blog module
+- ?
+
+Book module
+- Peter Wolanin 'pwolanin' http://drupal.org/user/49851
+
+Color module
+- ?
+
+Comment module
+- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+
+Contact module
+- Dave Reid 'davereid' http://drupal.org/user/53892
+
+Contextual module
+- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+
+Dashboard module
+- ?
+
+Database logging module
+- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063
+
+Field module
+- Yves Chedemois 'yched' http://drupal.org/user/39567
+- Barry Jaspan 'bjaspan' http://drupal.org/user/46413
+
+Field UI module
+- Yves Chedemois 'yched' http://drupal.org/user/39567
+
+File module
+- Aaron Winborn 'aaron' http://drupal.org/user/33420
+
+Filter module
+- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+
+Forum module
+- Lee Rowlands 'larowlan' http://drupal.org/user/395439
+
+Help module
+- ?
+
+Image module
+- Nathan Haug 'quicksketch' http://drupal.org/user/35821
+
+Locale module
+- Gábor Hojtsy 'Gábor Hojtsy' http://drupal.org/user/4166
+
+Menu module
+- ?
+
+Node module
+- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
+- David Strauss 'David Strauss' http://drupal.org/user/93254
+
+OpenID module
+- Vojtech Kusy 'wojtha' http://drupal.org/user/56154
+- Christian Schmidt 'c960657' http://drupal.org/user/216078
+- Damien Tournoud 'DamZ' http://drupal.org/user/22211
+
+Overlay module
+- Katherine Senzee 'ksenzee' http://drupal.org/user/139855
+
+Path module
+- Dave Reid 'davereid' http://drupal.org/user/53892
+
+PHP module
+- ?
+
+Poll module
+- Andrei Mateescu 'amateescu' http://drupal.org/user/729614
+
+Profile module
+- ?
+
+RDF module
+- Stéphane Corlosquet 'scor' http://drupal.org/user/52142
+
+Search module
+- Doug Green 'douggreen' http://drupal.org/user/29191
+
+Shortcut module
+- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+
+Simpletest module
+- Jimmy Berry 'boombatower' http://drupal.org/user/214218
+- Károly Négyesi 'chx' http://drupal.org/user/9446
+
+Statistics module
+- Tim Millwood 'timmillwood' http://drupal.org/user/227849
+
+Syslog module
+- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063
+
+System module
+- ?
+
+Taxonomy module
+- Jess Myrbo 'xjm' http://drupal.org/user/65776
+- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Benjamin Doherty 'bangpound' http://drupal.org/user/100456
+
+Toolbar module
+- ?
+
+Tracker module
+- David Strauss 'David Strauss' http://drupal.org/user/93254
+
+Translation module
+- Francesco Placella 'plach' http://drupal.org/user/183211
+
+Trigger module
+- ?
+
+Update module
+- Derek Wright 'dww' http://drupal.org/user/46549
+
+User module
+- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
+- David Strauss 'David Strauss' http://drupal.org/user/93254
+
+
+Theme maintainers
+-----------------
+
+Bartik theme
+- Jen Simmons 'jensimmons' http://drupal.org/user/140882
+- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393
+
+Garland theme
+- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+
+Seven theme
+- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393
+
+Stark theme
+- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095

+ 123 - 0
README.txt

@@ -0,0 +1,123 @@
+
+CONTENTS OF THIS FILE
+---------------------
+
+ * About Drupal
+ * Configuration and features
+ * Installation profiles
+ * Appearance
+ * Developing for Drupal
+
+ABOUT DRUPAL
+------------
+
+Drupal is an open source content management platform supporting a variety of
+websites ranging from personal weblogs to large community-driven websites. For
+more information, see the Drupal website at http://drupal.org/, and join the
+Drupal community at http://drupal.org/community.
+
+Legal information about Drupal:
+ * Know your rights when using Drupal:
+   See LICENSE.txt in the same directory as this document.
+ * Learn about the Drupal trademark and logo policy:
+   http://drupal.com/trademark
+
+CONFIGURATION AND FEATURES
+--------------------------
+
+Drupal core (what you get when you download and extract a drupal-x.y.tar.gz or
+drupal-x.y.zip file from http://drupal.org/project/drupal) has what you need to
+get started with your website. It includes several modules (extensions that add
+functionality) for common website features, such as managing content, user
+accounts, image uploading, and search. Core comes with many options that allow
+site-specific configuration. In addition to the core modules, there are
+thousands of contributed modules (for functionality not included with Drupal
+core) available for download.
+
+More about configuration:
+ * Install, upgrade, and maintain Drupal:
+   See INSTALL.txt and UPGRADE.txt in the same directory as this document.
+ * Learn about how to use Drupal to create your site:
+   http://drupal.org/documentation
+ * Download contributed modules to sites/all/modules to extend Drupal's
+   functionality:
+   http://drupal.org/project/modules
+ * See also: "Developing for Drupal" for writing your own modules, below.
+
+INSTALLATION PROFILES
+---------------------
+
+Installation profiles define additional steps (such as enabling modules,
+defining content types, etc.) that run after the base installation provided
+by core when Drupal is first installed. There are two basic installation
+profiles provided with Drupal core.
+
+Installation profiles from the Drupal community modify the installation process
+to provide a website for a specific use case, such as a CMS for media
+publishers, a web-based project tracking tool, or a full-fledged CRM for
+non-profit organizations raising money and accepting donations. They can be
+distributed as bare installation profiles or as "distributions". Distributions
+include Drupal core, the installation profile, and all other required
+extensions, such as contributed and custom modules, themes, and third-party
+libraries. Bare installation profiles require you to download Drupal Core and
+the required extensions separately; place the downloaded profile in the
+/profiles directory before you start the installation process. Note that the
+contents of this directory may be overwritten during updates of Drupal core;
+it is advised to keep code backups or use a version control system.
+
+Additionally, modules and themes may be placed inside subdirectories in a
+specific installation profile such as profiles/your_site_profile/modules and
+profiles/your_site_profile/themes respectively to restrict their usage to only
+sites that were installed with that specific profile.
+
+More about installation profiles and distributions:
+* Read about the difference between installation profiles and distributions:
+  http://drupal.org/node/1089736
+* Download contributed installation profiles and distributions:
+  http://drupal.org/project/distributions
+* Develop your own installation profile or distribution:
+  http://drupal.org/developing/distributions
+
+APPEARANCE
+----------
+
+In Drupal, the appearance of your site is set by the theme (themes are
+extensions that set fonts, colors, and layout). Drupal core comes with several
+themes. More themes are available for download, and you can also create your own
+custom theme.
+
+More about themes:
+ * Download contributed themes to sites/all/themes to modify Drupal's
+   appearance:
+   http://drupal.org/project/themes
+ * Develop your own theme:
+   http://drupal.org/documentation/theme
+
+DEVELOPING FOR DRUPAL
+---------------------
+
+Drupal contains an extensive API that allows you to add to and modify the
+functionality of your site. The API consists of "hooks", which allow modules to
+react to system events and customize Drupal's behavior, and functions that
+standardize common operations such as database queries and form generation. The
+flexible hook architecture means that you should never need to directly modify
+the files that come with Drupal core to achieve the functionality you want;
+instead, functionality modifications take the form of modules.
+
+When you need new functionality for your Drupal site, search for existing
+contributed modules. If you find a module that matches except for a bug or an
+additional needed feature, change the module and contribute your improvements
+back to the project in the form of a "patch". Create new custom modules only
+when nothing existing comes close to what you need.
+
+More about developing:
+ * Search for existing contributed modules:
+   http://drupal.org/project/modules
+ * Contribute a patch:
+   http://drupal.org/patch/submit
+ * Develop your own module:
+   http://drupal.org/developing/modules
+ * Follow best practices:
+   http://drupal.org/best-practices
+ * Refer to the API documentation:
+   http://api.drupal.org/api/drupal/7

+ 236 - 0
UPGRADE.txt

@@ -0,0 +1,236 @@
+INTRODUCTION
+------------
+This document describes how to:
+
+  * Update your Drupal site from one minor 7.x version to another minor 7.x
+    version; for example, from 7.8 to 7.9, or from 7.6 to 7.10.
+
+  * Upgrade your Drupal site's major version from 6.x to 7.x.
+
+First steps and definitions:
+
+  * If you are upgrading to Drupal version x.y, then x is known as the major
+    version number, and y is known as the minor version number. The download
+    file will be named drupal-x.y.tar.gz (or drupal-x.y.zip).
+
+  * All directories mentioned in this document are relative to the directory of
+    your Drupal installation.
+
+  * Make a full backup of all files, directories, and your database(s) before
+    starting, and save it outside your Drupal installation directory.
+    Instructions may be found at http://drupal.org/upgrade/backing-up-the-db
+
+  * It is wise to try an update or upgrade on a test copy of your site before
+    applying it to your live site. Even minor updates can cause your site's
+    behavior to change.
+
+  * Each new release of Drupal has release notes, which explain the changes made
+    since the previous version and any special instructions needed to update or
+    upgrade to the new version. You can find a link to the release notes for the
+    version you are upgrading or updating to on the Drupal project page
+    (http://drupal.org/project/drupal).
+
+UPGRADE PROBLEMS
+----------------
+If you encounter errors during this process,
+
+  * Note any error messages you see.
+
+  * Restore your site to its previous state, using the file and database backups
+    you created before you started the upgrade process. Do not attempt to do
+    further upgrades on a site that had update problems.
+
+  * Consult one of the support options listed on http://drupal.org/support
+
+More in-depth information on upgrading can be found at http://drupal.org/upgrade
+
+MINOR VERSION UPDATES
+---------------------
+To update from one minor 7.x version of Drupal to any later 7.x version, after
+following the instructions in the INTRODUCTION section at the top of this file:
+
+1. Log in as a user with the permission "Administer software updates".
+
+2. Go to Administration > Configuration > Development > Maintenance mode.
+   Enable the "Put site into maintenance mode" checkbox and save the
+   configuration.
+
+3. Remove all old core files and directories, except for the 'sites' directory
+   and any custom files you added elsewhere.
+
+   If you made modifications to files like .htaccess or robots.txt, you will
+   need to re-apply them from your backup, after the new files are in place.
+
+   Sometimes an update includes changes to default.settings.php (this will be
+   noted in the release notes). If that's the case, follow these steps:
+
+   - Make a backup copy of your settings.php file, with a different file name.
+
+   - Make a copy of the new default.settings.php file, and name the copy
+     settings.php (overwriting your previous settings.php file).
+
+   - Copy the custom and site-specific entries from the backup you made into the
+     new settings.php file. You will definitely need the lines giving the
+     database information, and you will also want to copy in any other
+     customizations you have added.
+
+4. Download the latest Drupal 7.x release from http://drupal.org to a
+   directory outside of your web root. Extract the archive and copy the files
+   into your Drupal directory.
+
+   On a typical Unix/Linux command line, use the following commands to download
+   and extract:
+
+     wget http://drupal.org/files/projects/drupal-x.y.tar.gz
+     tar -zxvf drupal-x.y.tar.gz
+
+   This creates a new directory drupal-x.y/ containing all Drupal files and
+   directories. Copy the files into your Drupal installation directory:
+
+     cp -R drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
+
+   If you do not have command line access to your server, download the archive
+   from http://drupal.org using your web browser, extract it, and then use an
+   FTP client to upload the files to your web root.
+
+5. Re-apply any modifications to files such as .htaccess or robots.txt.
+
+6. Run update.php by visiting http://www.example.com/update.php (replace
+   www.example.com with your domain name). This will update the core database
+   tables.
+
+   If you are unable to access update.php do the following:
+
+   - Open settings.php with a text editor.
+
+   - Find the line that says:
+     $update_free_access = FALSE;
+
+   - Change it into:
+     $update_free_access = TRUE;
+
+   - Once the upgrade is done, $update_free_access must be reverted to FALSE.
+
+7. Go to Administration > Reports > Status report. Verify that everything is
+   working as expected.
+
+8. Ensure that $update_free_access is FALSE in settings.php.
+
+9. Go to Administration > Configuration > Development > Maintenance mode.
+   Disable the "Put site into maintenance mode" checkbox and save the
+   configuration.
+
+MAJOR VERSION UPGRADE
+---------------------
+To upgrade from a previous major version of Drupal to Drupal 7.x, after
+following the instructions in the INTRODUCTION section at the top of this file:
+
+1. Check on the Drupal 7 status of your contributed and custom modules and
+   themes. See http://drupal.org/node/948216 for information on upgrading
+   contributed modules and themes. See http://drupal.org/node/895314 for a list
+   of modules that have been moved into core for Drupal 7, and instructions on
+   how to update them. See http://drupal.org/update/modules for information on
+   how to update your custom modules, and http://drupal.org/update/theme for
+   custom themes.
+
+   You may decide at this point that you cannot upgrade your site, because
+   needed modules or themes are not ready for Drupal 7.
+
+2. Update to the latest available version of Drupal 6.x (if your current version
+   is Drupal 5.x, you have to upgrade to 6.x first). If you need to update,
+   download Drupal 6.x and follow the instructions in its UPGRADE.txt. This
+   document only applies for upgrades from 6.x to 7.x.
+
+3. In addition to updating to the latest available version of Drupal 6.x core,
+   you must also upgrade all of your contributed modules for Drupal to their
+   latest Drupal 6.x versions.
+
+4. Log in as user ID 1 (the site maintenance user).
+
+5. Go to Administer > Site configuration > Site maintenance. Select
+   "Off-line" and save the configuration.
+
+6. Go to Administer > Site building > Themes. Enable "Garland" and select it as
+   the default theme.
+
+7. Go to Administer > Site building > Modules. Disable all modules that are not
+   listed under "Core - required" or "Core - optional". It is possible that some
+   modules cannot be disabled, because others depend on them. Repeat this step
+   until all non-core modules are disabled.
+
+   If you know that you will not re-enable some modules for Drupal 7.x and you
+   no longer need their data, then you can uninstall them under the Uninstall
+   tab after disabling them.
+
+8. On the command line or in your FTP client, remove the file
+
+     sites/default/default.settings.php
+
+9. Remove all old core files and directories, except for the 'sites' directory
+   and any custom files you added elsewhere.
+
+   If you made modifications to files like .htaccess or robots.txt, you will
+   need to re-apply them from your backup, after the new files are in place.
+
+10. If you uninstalled any modules, remove them from the sites/all/modules and
+   other sites/*/modules directories. Leave other modules in place, even though
+   they are incompatible with Drupal 7.x.
+
+11. Download the latest Drupal 7.x release from http://drupal.org to a
+   directory outside of your web root. Extract the archive and copy the files
+   into your Drupal directory.
+
+   On a typical Unix/Linux command line, use the following commands to download
+   and extract:
+
+     wget http://drupal.org/files/projects/drupal-x.y.tar.gz
+     tar -zxvf drupal-x.y.tar.gz
+
+   This creates a new directory drupal-x.y/ containing all Drupal files and
+   directories. Copy the files into your Drupal installation directory:
+
+     cp -R drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
+
+   If you do not have command line access to your server, download the archive
+   from http://drupal.org using your web browser, extract it, and then use an
+   FTP client to upload the files to your web root.
+
+12. Re-apply any modifications to files such as .htaccess or robots.txt.
+
+13. Make your settings.php file writeable, so that the update process can
+   convert it to the format of Drupal 7.x. settings.php is usually located in
+
+     sites/default/settings.php
+
+14. Run update.php by visiting http://www.example.com/update.php (replace
+   www.example.com with your domain name). This will update the core database
+   tables.
+
+   If you are unable to access update.php do the following:
+
+   - Open settings.php with a text editor.
+
+   - Find the line that says:
+     $update_free_access = FALSE;
+
+   - Change it into:
+     $update_free_access = TRUE;
+
+   - Once the upgrade is done, $update_free_access must be reverted to FALSE.
+
+15. Backup your database after the core upgrade has run.
+
+16. Replace and update your non-core modules and themes, following the
+   procedures at http://drupal.org/node/948216
+
+17. Go to Administration > Reports > Status report. Verify that everything is
+   working as expected.
+
+18. Ensure that $update_free_access is FALSE in settings.php.
+
+19. Go to Administration > Configuration > Development > Maintenance mode.
+   Disable the "Put site into maintenance mode" checkbox and save the
+   configuration.
+
+To get started with Drupal 7 administration, visit
+http://drupal.org/getting-started/7/admin

+ 174 - 0
authorize.php

@@ -0,0 +1,174 @@
+<?php
+
+/**
+ * @file
+ * Administrative script for running authorized file operations.
+ *
+ * Using this script, the site owner (the user actually owning the files on the
+ * webserver) can authorize certain file-related operations to proceed with
+ * elevated privileges, for example to deploy and upgrade modules or themes.
+ * Users should not visit this page directly, but instead use an administrative
+ * user interface which knows how to redirect the user to this script as part of
+ * a multistep process. This script actually performs the selected operations
+ * without loading all of Drupal, to be able to more gracefully recover from
+ * errors. Access to the script is controlled by a global killswitch in
+ * settings.php ('allow_authorize_operations') and via the 'administer software
+ * updates' permission.
+ *
+ * There are helper functions for setting up an operation to run via this
+ * system in modules/system/system.module. For more information, see:
+ * @link authorize Authorized operation helper functions @endlink
+ */
+
+/**
+ * Defines the root directory of the Drupal installation.
+ */
+define('DRUPAL_ROOT', getcwd());
+
+/**
+ * Global flag to identify update.php and authorize.php runs.
+ *
+ * Identifies update.php and authorize.php runs, avoiding unwanted operations
+ * such as hook_init() and hook_exit() invokes, css/js preprocessing and
+ * translation, and solves some theming issues. The flag is checked in other
+ * places in Drupal code (not just authorize.php).
+ */
+define('MAINTENANCE_MODE', 'update');
+
+/**
+ * Renders a 403 access denied page for authorize.php.
+ */
+function authorize_access_denied_page() {
+  drupal_add_http_header('Status', '403 Forbidden');
+  watchdog('access denied', 'authorize.php', NULL, WATCHDOG_WARNING);
+  drupal_set_title('Access denied');
+  return t('You are not allowed to access this page.');
+}
+
+/**
+ * Determines if the current user is allowed to run authorize.php.
+ *
+ * The killswitch in settings.php overrides all else, otherwise, the user must
+ * have access to the 'administer software updates' permission.
+ *
+ * @return
+ *   TRUE if the current user can run authorize.php, and FALSE if not.
+ */
+function authorize_access_allowed() {
+  return variable_get('allow_authorize_operations', TRUE) && user_access('administer software updates');
+}
+
+// *** Real work of the script begins here. ***
+
+require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+require_once DRUPAL_ROOT . '/includes/common.inc';
+require_once DRUPAL_ROOT . '/includes/file.inc';
+require_once DRUPAL_ROOT . '/includes/module.inc';
+require_once DRUPAL_ROOT . '/includes/ajax.inc';
+
+// We prepare only a minimal bootstrap. This includes the database and
+// variables, however, so we have access to the class autoloader registry.
+drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
+
+// This must go after drupal_bootstrap(), which unsets globals!
+global $conf;
+
+// We have to enable the user and system modules, even to check access and
+// display errors via the maintenance theme.
+$module_list['system']['filename'] = 'modules/system/system.module';
+$module_list['user']['filename'] = 'modules/user/user.module';
+module_list(TRUE, FALSE, FALSE, $module_list);
+drupal_load('module', 'system');
+drupal_load('module', 'user');
+
+// We also want to have the language system available, but we do *NOT* want to
+// actually call drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE), since that would
+// also force us through the DRUPAL_BOOTSTRAP_PAGE_HEADER phase, which loads
+// all the modules, and that's exactly what we're trying to avoid.
+drupal_language_initialize();
+
+// Initialize the maintenance theme for this administrative script.
+drupal_maintenance_theme();
+
+$output = '';
+$show_messages = TRUE;
+
+if (authorize_access_allowed()) {
+  // Load both the Form API and Batch API.
+  require_once DRUPAL_ROOT . '/includes/form.inc';
+  require_once DRUPAL_ROOT . '/includes/batch.inc';
+  // Load the code that drives the authorize process.
+  require_once DRUPAL_ROOT . '/includes/authorize.inc';
+
+  // For the sake of Batch API and a few other low-level functions, we need to
+  // initialize the URL path into $_GET['q']. However, we do not want to raise
+  // our bootstrap level, nor do we want to call drupal_initialize_path(),
+  // since that is assuming that modules are loaded and invoking hooks.
+  // However, all we really care is if we're in the middle of a batch, in which
+  // case $_GET['q'] will already be set, we just initialize it to an empty
+  // string if it's not already defined.
+  if (!isset($_GET['q'])) {
+    $_GET['q'] = '';
+  }
+
+  if (isset($_SESSION['authorize_operation']['page_title'])) {
+    drupal_set_title($_SESSION['authorize_operation']['page_title']);
+  }
+  else {
+    drupal_set_title(t('Authorize file system changes'));
+  }
+
+  // See if we've run the operation and need to display a report.
+  if (isset($_SESSION['authorize_results']) && $results = $_SESSION['authorize_results']) {
+
+    // Clear the session out.
+    unset($_SESSION['authorize_results']);
+    unset($_SESSION['authorize_operation']);
+    unset($_SESSION['authorize_filetransfer_info']);
+
+    if (!empty($results['page_title'])) {
+      drupal_set_title($results['page_title']);
+    }
+    if (!empty($results['page_message'])) {
+      drupal_set_message($results['page_message']['message'], $results['page_message']['type']);
+    }
+
+    $output = theme('authorize_report', array('messages' => $results['messages']));
+
+    $links = array();
+    if (is_array($results['tasks'])) {
+      $links += $results['tasks'];
+    }
+    else {
+      $links = array_merge($links, array(
+        l(t('Administration pages'), 'admin'),
+        l(t('Front page'), '<front>'),
+      ));
+    }
+
+    $output .= theme('item_list', array('items' => $links, 'title' => t('Next steps')));
+  }
+  // If a batch is running, let it run.
+  elseif (isset($_GET['batch'])) {
+    $output = _batch_page();
+  }
+  else {
+    if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_info'])) {
+      $output = t('It appears you have reached this page in error.');
+    }
+    elseif (!$batch = batch_get()) {
+      // We have a batch to process, show the filetransfer form.
+      $elements = drupal_get_form('authorize_filetransfer_form');
+      $output = drupal_render($elements);
+    }
+  }
+  // We defer the display of messages until all operations are done.
+  $show_messages = !(($batch = batch_get()) && isset($batch['running']));
+}
+else {
+  $output = authorize_access_denied_page();
+}
+
+if (!empty($output)) {
+  print theme('update_page', array('content' => $output, 'show_messages' => $show_messages));
+}

+ 26 - 0
cron.php

@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Handles incoming requests to fire off regularly-scheduled tasks (cron jobs).
+ */
+
+/**
+ * Root directory of Drupal installation.
+ */
+define('DRUPAL_ROOT', getcwd());
+
+include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+
+if (!isset($_GET['cron_key']) || variable_get('cron_key', 'drupal') != $_GET['cron_key']) {
+  watchdog('cron', 'Cron could not run because an invalid key was used.', array(), WATCHDOG_NOTICE);
+  drupal_access_denied();
+}
+elseif (variable_get('maintenance_mode', 0)) {
+  watchdog('cron', 'Cron could not run because the site is in maintenance mode.', array(), WATCHDOG_NOTICE);
+  drupal_access_denied();
+}
+else {
+  drupal_cron_run();
+}

BIN
favicon.ico


+ 388 - 0
includes/actions.inc

@@ -0,0 +1,388 @@
+<?php
+
+/**
+ * @file
+ * This is the actions engine for executing stored actions.
+ */
+
+/**
+ * @defgroup actions Actions
+ * @{
+ * Functions that perform an action on a certain system object.
+ *
+ * Action functions are declared by modules by implementing hook_action_info().
+ * Modules can cause action functions to run by calling actions_do(), and
+ * trigger.module provides a user interface that lets administrators define
+ * events that cause action functions to run.
+ *
+ * Each action function takes two to four arguments:
+ * - $entity: The object that the action acts on, such as a node, comment, or
+ *   user.
+ * - $context: Array of additional information about what triggered the action.
+ * - $a1, $a2: Optional additional information, which can be passed into
+ *   actions_do() and will be passed along to the action function.
+ *
+ * @}
+ */
+
+/**
+ * Performs a given list of actions by executing their callback functions.
+ *
+ * Given the IDs of actions to perform, this function finds out what the
+ * callback functions for the actions are by querying the database. Then
+ * it calls each callback using the function call $function($object, $context,
+ * $a1, $a2), passing the input arguments of this function (see below) to the
+ * action function.
+ *
+ * @param $action_ids
+ *   The IDs of the actions to perform. Can be a single action ID or an array
+ *   of IDs. IDs of configurable actions must be given as numeric action IDs;
+ *   IDs of non-configurable actions may be given as action function names.
+ * @param $object
+ *   The object that the action will act on: a node, user, or comment object.
+ * @param $context
+ *   Associative array containing extra information about what triggered
+ *   the action call, with $context['hook'] giving the name of the hook
+ *   that resulted in this call to actions_do().
+ * @param $a1
+ *   Passed along to the callback.
+ * @param $a2
+ *   Passed along to the callback.
+ *
+ * @return
+ *   An associative array containing the results of the functions that
+ *   perform the actions, keyed on action ID.
+ *
+ * @ingroup actions
+ */
+function actions_do($action_ids, $object = NULL, $context = NULL, $a1 = NULL, $a2 = NULL) {
+  // $stack tracks the number of recursive calls.
+  static $stack;
+  $stack++;
+  if ($stack > variable_get('actions_max_stack', 35)) {
+    watchdog('actions', 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.', array(), WATCHDOG_ERROR);
+    return;
+  }
+  $actions = array();
+  $available_actions = actions_list();
+  $actions_result = array();
+  if (is_array($action_ids)) {
+    $conditions = array();
+    foreach ($action_ids as $action_id) {
+      if (is_numeric($action_id)) {
+        $conditions[] = $action_id;
+      }
+      elseif (isset($available_actions[$action_id])) {
+        $actions[$action_id] = $available_actions[$action_id];
+      }
+    }
+
+    // When we have action instances we must go to the database to retrieve
+    // instance data.
+    if (!empty($conditions)) {
+      $query = db_select('actions');
+      $query->addField('actions', 'aid');
+      $query->addField('actions', 'type');
+      $query->addField('actions', 'callback');
+      $query->addField('actions', 'parameters');
+      $query->condition('aid', $conditions, 'IN');
+      $result = $query->execute();
+      foreach ($result as $action) {
+        $actions[$action->aid] = $action->parameters ? unserialize($action->parameters) : array();
+        $actions[$action->aid]['callback'] = $action->callback;
+        $actions[$action->aid]['type'] = $action->type;
+      }
+    }
+
+    // Fire actions, in no particular order.
+    foreach ($actions as $action_id => $params) {
+      // Configurable actions need parameters.
+      if (is_numeric($action_id)) {
+        $function = $params['callback'];
+        if (function_exists($function)) {
+          $context = array_merge($context, $params);
+          $actions_result[$action_id] = $function($object, $context, $a1, $a2);
+        }
+        else {
+          $actions_result[$action_id] = FALSE;
+        }
+      }
+      // Singleton action; $action_id is the function name.
+      else {
+        $actions_result[$action_id] = $action_id($object, $context, $a1, $a2);
+      }
+    }
+  }
+  // Optimized execution of a single action.
+  else {
+    // If it's a configurable action, retrieve stored parameters.
+    if (is_numeric($action_ids)) {
+      $action = db_query("SELECT callback, parameters FROM {actions} WHERE aid = :aid", array(':aid' => $action_ids))->fetchObject();
+      $function = $action->callback;
+      if (function_exists($function)) {
+        $context = array_merge($context, unserialize($action->parameters));
+        $actions_result[$action_ids] = $function($object, $context, $a1, $a2);
+      }
+      else {
+        $actions_result[$action_ids] = FALSE;
+      }
+    }
+    // Singleton action; $action_ids is the function name.
+    else {
+      if (function_exists($action_ids)) {
+        $actions_result[$action_ids] = $action_ids($object, $context, $a1, $a2);
+      }
+      else {
+        // Set to avoid undefined index error messages later.
+        $actions_result[$action_ids] = FALSE;
+      }
+    }
+  }
+  $stack--;
+  return $actions_result;
+}
+
+/**
+ * Discovers all available actions by invoking hook_action_info().
+ *
+ * This function contrasts with actions_get_all_actions(); see the
+ * documentation of actions_get_all_actions() for an explanation.
+ *
+ * @param $reset
+ *   Reset the action info static cache.
+ *
+ * @return
+ *   An associative array keyed on action function name, with the same format
+ *   as the return value of hook_action_info(), containing all
+ *   modules' hook_action_info() return values as modified by any
+ *   hook_action_info_alter() implementations.
+ *
+ * @see hook_action_info()
+ */
+function actions_list($reset = FALSE) {
+  $actions = &drupal_static(__FUNCTION__);
+  if (!isset($actions) || $reset) {
+    $actions = module_invoke_all('action_info');
+    drupal_alter('action_info', $actions);
+  }
+
+  // See module_implements() for an explanation of this cast.
+  return (array) $actions;
+}
+
+/**
+ * Retrieves all action instances from the database.
+ *
+ * This function differs from the actions_list() function, which gathers
+ * actions by invoking hook_action_info(). The actions returned by this
+ * function and the actions returned by actions_list() are partially
+ * synchronized. Non-configurable actions from hook_action_info()
+ * implementations are put into the database when actions_synchronize() is
+ * called, which happens when admin/config/system/actions is visited.
+ * Configurable actions are not added to the database until they are configured
+ * in the user interface, in which case a database row is created for each
+ * configuration of each action.
+ *
+ * @return
+ *   Associative array keyed by numeric action ID. Each value is an associative
+ *   array with keys 'callback', 'label', 'type' and 'configurable'.
+ */
+function actions_get_all_actions() {
+  $actions = db_query("SELECT aid, type, callback, parameters, label FROM {actions}")->fetchAllAssoc('aid', PDO::FETCH_ASSOC);
+  foreach ($actions as &$action) {
+    $action['configurable'] = (bool) $action['parameters'];
+    unset($action['parameters']);
+    unset($action['aid']);
+  }
+  return $actions;
+}
+
+/**
+ * Creates an associative array keyed by hashes of function names or IDs.
+ *
+ * Hashes are used to prevent actual function names from going out into HTML
+ * forms and coming back.
+ *
+ * @param $actions
+ *   An associative array with function names or action IDs as keys
+ *   and associative arrays with keys 'label', 'type', etc. as values.
+ *   This is usually the output of actions_list() or actions_get_all_actions().
+ *
+ * @return
+ *   An associative array whose keys are hashes of the input array keys, and
+ *   whose corresponding values are associative arrays with components
+ *   'callback', 'label', 'type', and 'configurable' from the input array.
+ */
+function actions_actions_map($actions) {
+  $actions_map = array();
+  foreach ($actions as $callback => $array) {
+    $key = drupal_hash_base64($callback);
+    $actions_map[$key]['callback']     = isset($array['callback']) ? $array['callback'] : $callback;
+    $actions_map[$key]['label']        = $array['label'];
+    $actions_map[$key]['type']         = $array['type'];
+    $actions_map[$key]['configurable'] = $array['configurable'];
+  }
+  return $actions_map;
+}
+
+/**
+ * Returns an action array key (function or ID), given its hash.
+ *
+ * Faster than actions_actions_map() when you only need the function name or ID.
+ *
+ * @param $hash
+ *   Hash of a function name or action ID array key. The array key
+ *   is a key into the return value of actions_list() (array key is the action
+ *   function name) or actions_get_all_actions() (array key is the action ID).
+ *
+ * @return
+ *   The corresponding array key, or FALSE if no match is found.
+ */
+function actions_function_lookup($hash) {
+  // Check for a function name match.
+  $actions_list = actions_list();
+  foreach ($actions_list as $function => $array) {
+    if (drupal_hash_base64($function) == $hash) {
+      return $function;
+    }
+  }
+  $aid = FALSE;
+  // Must be a configurable action; check database.
+  $result = db_query("SELECT aid FROM {actions} WHERE parameters <> ''")->fetchAll(PDO::FETCH_ASSOC);
+  foreach ($result as $row) {
+    if (drupal_hash_base64($row['aid']) == $hash) {
+      $aid = $row['aid'];
+      break;
+    }
+  }
+  return $aid;
+}
+
+/**
+ * Synchronizes actions that are provided by modules in hook_action_info().
+ *
+ * Actions provided by modules in hook_action_info() implementations are
+ * synchronized with actions that are stored in the actions database table.
+ * This is necessary so that actions that do not require configuration can
+ * receive action IDs.
+ *
+ * @param $delete_orphans
+ *   If TRUE, any actions that exist in the database but are no longer
+ *   found in the code (for example, because the module that provides them has
+ *   been disabled) will be deleted.
+ */
+function actions_synchronize($delete_orphans = FALSE) {
+  $actions_in_code = actions_list(TRUE);
+  $actions_in_db = db_query("SELECT aid, callback, label FROM {actions} WHERE parameters = ''")->fetchAllAssoc('callback', PDO::FETCH_ASSOC);
+
+  // Go through all the actions provided by modules.
+  foreach ($actions_in_code as $callback => $array) {
+    // Ignore configurable actions since their instances get put in when the
+    // user adds the action.
+    if (!$array['configurable']) {
+      // If we already have an action ID for this action, no need to assign aid.
+      if (isset($actions_in_db[$callback])) {
+        unset($actions_in_db[$callback]);
+      }
+      else {
+        // This is a new singleton that we don't have an aid for; assign one.
+        db_insert('actions')
+          ->fields(array(
+            'aid' => $callback,
+            'type' => $array['type'],
+            'callback' => $callback,
+            'parameters' => '',
+            'label' => $array['label'],
+            ))
+          ->execute();
+        watchdog('actions', "Action '%action' added.", array('%action' => $array['label']));
+      }
+    }
+  }
+
+  // Any actions that we have left in $actions_in_db are orphaned.
+  if ($actions_in_db) {
+    $orphaned = array_keys($actions_in_db);
+
+    if ($delete_orphans) {
+      $actions = db_query('SELECT aid, label FROM {actions} WHERE callback IN (:orphaned)', array(':orphaned' => $orphaned))->fetchAll();
+      foreach ($actions as $action) {
+        actions_delete($action->aid);
+        watchdog('actions', "Removed orphaned action '%action' from database.", array('%action' => $action->label));
+      }
+    }
+    else {
+      $link = l(t('Remove orphaned actions'), 'admin/config/system/actions/orphan');
+      $count = count($actions_in_db);
+      $orphans = implode(', ', $orphaned);
+      watchdog('actions', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $count, '%orphans' => $orphans, '!link' => $link), WATCHDOG_INFO);
+    }
+  }
+}
+
+/**
+ * Saves an action and its user-supplied parameter values to the database.
+ *
+ * @param $function
+ *   The name of the function to be called when this action is performed.
+ * @param $type
+ *   The type of action, to describe grouping and/or context, e.g., 'node',
+ *   'user', 'comment', or 'system'.
+ * @param $params
+ *   An associative array with parameter names as keys and parameter values as
+ *   values.
+ * @param $label
+ *   A user-supplied label of this particular action, e.g., 'Send e-mail
+ *   to Jim'.
+ * @param $aid
+ *   The ID of this action. If omitted, a new action is created.
+ *
+ * @return
+ *   The ID of the action.
+ */
+function actions_save($function, $type, $params, $label, $aid = NULL) {
+  // aid is the callback for singleton actions so we need to keep a separate
+  // table for numeric aids.
+  if (!$aid) {
+    $aid = db_next_id();
+  }
+
+  db_merge('actions')
+    ->key(array('aid' => $aid))
+    ->fields(array(
+      'callback' => $function,
+      'type' => $type,
+      'parameters' => serialize($params),
+      'label' => $label,
+    ))
+    ->execute();
+
+  watchdog('actions', 'Action %action saved.', array('%action' => $label));
+  return $aid;
+}
+
+/**
+ * Retrieves a single action from the database.
+ *
+ * @param $aid
+ *   The ID of the action to retrieve.
+ *
+ * @return
+ *   The appropriate action row from the database as an object.
+ */
+function actions_load($aid) {
+  return db_query("SELECT aid, type, callback, parameters, label FROM {actions} WHERE aid = :aid", array(':aid' => $aid))->fetchObject();
+}
+
+/**
+ * Deletes a single action from the database.
+ *
+ * @param $aid
+ *   The ID of the action to delete.
+ */
+function actions_delete($aid) {
+  db_delete('actions')
+    ->condition('aid', $aid)
+    ->execute();
+  module_invoke_all('actions_delete', $aid);
+}

+ 1212 - 0
includes/ajax.inc

@@ -0,0 +1,1212 @@
+<?php
+
+/**
+ * @file
+ * Functions for use with Drupal's Ajax framework.
+ */
+
+/**
+ * @defgroup ajax Ajax framework
+ * @{
+ * Functions for Drupal's Ajax framework.
+ *
+ * Drupal's Ajax framework is used to dynamically update parts of a page's HTML
+ * based on data from the server. Upon a specified event, such as a button
+ * click, a callback function is triggered which performs server-side logic and
+ * may return updated markup, which is then replaced on-the-fly with no page
+ * refresh necessary.
+ *
+ * This framework creates a PHP macro language that allows the server to
+ * instruct JavaScript to perform actions on the client browser. When using
+ * forms, it can be used with the #ajax property.
+ * The #ajax property can be used to bind events to the Ajax framework. By
+ * default, #ajax uses 'system/ajax' as its path for submission and thus calls
+ * ajax_form_callback() and a defined #ajax['callback'] function.
+ * However, you may optionally specify a different path to request or a
+ * different callback function to invoke, which can return updated HTML or can
+ * also return a richer set of
+ * @link ajax_commands Ajax framework commands @endlink.
+ *
+ * Standard form handling is as follows:
+ *   - A form element has a #ajax property that includes #ajax['callback'] and
+ *     omits #ajax['path']. See below about using #ajax['path'] to implement
+ *     advanced use-cases that require something other than standard form
+ *     handling.
+ *   - On the specified element, Ajax processing is triggered by a change to
+ *     that element.
+ *   - The browser submits an HTTP POST request to the 'system/ajax' Drupal
+ *     path.
+ *   - The menu page callback for 'system/ajax', ajax_form_callback(), calls
+ *     drupal_process_form() to process the form submission and rebuild the
+ *     form if necessary. The form is processed in much the same way as if it
+ *     were submitted without Ajax, with the same #process functions and
+ *     validation and submission handlers called in either case, making it easy
+ *     to create Ajax-enabled forms that degrade gracefully when JavaScript is
+ *     disabled.
+ *   - After form processing is complete, ajax_form_callback() calls the
+ *     function named by #ajax['callback'], which returns the form element that
+ *     has been updated and needs to be returned to the browser, or
+ *     alternatively, an array of custom Ajax commands.
+ *   - The page delivery callback for 'system/ajax', ajax_deliver(), renders the
+ *     element returned by #ajax['callback'], and returns the JSON string
+ *     created by ajax_render() to the browser.
+ *   - The browser unserializes the returned JSON string into an array of
+ *     command objects and executes each command, resulting in the old page
+ *     content within and including the HTML element specified by
+ *     #ajax['wrapper'] being replaced by the new content returned by
+ *     #ajax['callback'], using a JavaScript animation effect specified by
+ *     #ajax['effect'].
+ *
+ * A simple example of basic Ajax use from the
+ * @link http://drupal.org/project/examples Examples module @endlink follows:
+ * @code
+ * function main_page() {
+ *   return drupal_get_form('ajax_example_simplest');
+ * }
+ *
+ * function ajax_example_simplest($form, &$form_state) {
+ *   $form = array();
+ *   $form['changethis'] = array(
+ *     '#type' => 'select',
+ *     '#options' => array(
+ *       'one' => 'one',
+ *       'two' => 'two',
+ *       'three' => 'three',
+ *     ),
+ *     '#ajax' => array(
+ *       'callback' => 'ajax_example_simplest_callback',
+ *       'wrapper' => 'replace_textfield_div',
+ *      ),
+ *   );
+
+ *   // This entire form element will be replaced with an updated value.
+ *   $form['replace_textfield'] = array(
+ *     '#type' => 'textfield',
+ *     '#title' => t("The default value will be changed"),
+ *     '#description' => t("Say something about why you chose") . "'" .
+ *       (!empty($form_state['values']['changethis'])
+ *       ? $form_state['values']['changethis'] : t("Not changed yet")) . "'",
+ *     '#prefix' => '<div id="replace_textfield_div">',
+ *     '#suffix' => '</div>',
+ *   );
+ *   return $form;
+ * }
+ *
+ * function ajax_example_simplest_callback($form, $form_state) {
+ *   // The form has already been submitted and updated. We can return the replaced
+ *   // item as it is.
+ *   return $form['replace_textfield'];
+ * }
+ * @endcode
+ *
+ * In the above example, the 'changethis' element is Ajax-enabled. The default
+ * #ajax['event'] is 'change', so when the 'changethis' element changes,
+ * an Ajax call is made. The form is submitted and reprocessed, and then the
+ * callback is called. In this case, the form has been automatically
+ * built changing $form['replace_textfield']['#description'], so the callback
+ * just returns that part of the form.
+ *
+ * To implement Ajax handling in a form, add '#ajax' to the form
+ * definition of a field. That field will trigger an Ajax event when it is
+ * clicked (or changed, depending on the kind of field). #ajax supports
+ * the following parameters (either 'path' or 'callback' is required at least):
+ * - #ajax['callback']: The callback to invoke to handle the server side of the
+ *   Ajax event, which will receive a $form and $form_state as arguments, and
+ *   returns a renderable array (most often a form or form fragment), an HTML
+ *   string, or an array of Ajax commands. If returning a renderable array or
+ *   a string, the value will replace the original element named in
+ *   #ajax['wrapper'], and
+ *   theme_status_messages()
+ *   will be prepended to that
+ *   element. (If the status messages are not wanted, return an array
+ *   of Ajax commands instead.)
+ *   #ajax['wrapper']. If an array of Ajax commands is returned, it will be
+ *   executed by the calling code.
+ * - #ajax['path']: The menu path to use for the request. This is often omitted
+ *   and the default is used. This path should map
+ *   to a menu page callback that returns data using ajax_render(). Defaults to
+ *   'system/ajax', which invokes ajax_form_callback(), eventually calling
+ *   the function named in #ajax['callback']. If you use a custom
+ *   path, you must set up the menu entry and handle the entire callback in your
+ *   own code.
+ * - #ajax['wrapper']: The CSS ID of the area to be replaced by the content
+ *   returned by the #ajax['callback'] function. The content returned from
+ *   the callback will replace the entire element named by #ajax['wrapper'].
+ *   The wrapper is usually created using #prefix and #suffix properties in the
+ *   form. Note that this is the wrapper ID, not a CSS selector. So to replace
+ *   the element referred to by the CSS selector #some-selector on the page,
+ *   use #ajax['wrapper'] = 'some-selector', not '#some-selector'.
+ * - #ajax['effect']: The jQuery effect to use when placing the new HTML.
+ *   Defaults to no effect. Valid options are 'none', 'slide', or 'fade'.
+ * - #ajax['speed']: The effect speed to use. Defaults to 'slow'. May be
+ *   'slow', 'fast' or a number in milliseconds which represents the length
+ *   of time the effect should run.
+ * - #ajax['event']: The JavaScript event to respond to. This is normally
+ *   selected automatically for the type of form widget being used, and
+ *   is only needed if you need to override the default behavior.
+ * - #ajax['prevent']: A JavaScript event to prevent when 'event' is triggered.
+ *   Defaults to 'click' for #ajax on #type 'submit', 'button', and
+ *   'image_button'. Multiple events may be specified separated by spaces.
+ *   For example, when binding #ajax behaviors to form buttons, pressing the
+ *   ENTER key within a textfield triggers the 'click' event of the form's first
+ *   submit button. Triggering Ajax in this situation leads to problems, like
+ *   breaking autocomplete textfields. Because of that, Ajax behaviors are bound
+ *   to the 'mousedown' event on form buttons by default. However, binding to
+ *   'mousedown' rather than 'click' means that it is possible to trigger a
+ *   click by pressing the mouse, holding the mouse button down until the Ajax
+ *   request is complete and the button is re-enabled, and then releasing the
+ *   mouse button. For this case, 'prevent' can be set to 'click', so an
+ *   additional event handler is bound to prevent such a click from triggering a
+ *   non-Ajax form submission. This also prevents a textfield's ENTER press
+ *   triggering a button's non-Ajax form submission behavior.
+ * - #ajax['method']: The jQuery method to use to place the new HTML.
+ *   Defaults to 'replaceWith'. May be: 'replaceWith', 'append', 'prepend',
+ *   'before', 'after', or 'html'. See the
+ *   @link http://api.jquery.com/category/manipulation/ jQuery manipulators documentation @endlink
+ *   for more information on these methods.
+ * - #ajax['progress']: Choose either a throbber or progress bar that is
+ *   displayed while awaiting a response from the callback, and add an optional
+ *   message. Possible keys: 'type', 'message', 'url', 'interval'.
+ *   More information is available in the
+ *   @link forms_api_reference.html Form API Reference @endlink
+ *
+ * In addition to using Form API for doing in-form modification, Ajax may be
+ * enabled by adding classes to buttons and links. By adding the 'use-ajax'
+ * class to a link, the link will be loaded via an Ajax call. When using this
+ * method, the href of the link can contain '/nojs/' as part of the path. When
+ * the Ajax framework makes the request, it will convert this to '/ajax/'.
+ * The server is then able to easily tell if this request was made through an
+ * actual Ajax request or in a degraded state, and respond appropriately.
+ *
+ * Similarly, submit buttons can be given the class 'use-ajax-submit'. The
+ * form will then be submitted via Ajax to the path specified in the #action.
+ * Like the ajax-submit class above, this path will have '/nojs/' replaced with
+ * '/ajax/' so that the submit handler can tell if the form was submitted
+ * in a degraded state or not.
+ *
+ * When responding to Ajax requests, the server should do what it needs to do
+ * for that request, then create a commands array. This commands array will
+ * be converted to a JSON object and returned to the client, which will then
+ * iterate over the array and process it like a macro language.
+ *
+ * Each command item is an associative array which will be converted to a
+ * command object on the JavaScript side. $command_item['command'] is the type
+ * of command, e.g. 'alert' or 'replace', and will correspond to a method in the
+ * Drupal.ajax[command] space. The command array may contain any other data that
+ * the command needs to process, e.g. 'method', 'selector', 'settings', etc.
+ *
+ * Commands are usually created with a couple of helper functions, so they
+ * look like this:
+ * @code
+ *   $commands = array();
+ *   // Replace the content of '#object-1' on the page with 'some html here'.
+ *   $commands[] = ajax_command_replace('#object-1', 'some html here');
+ *   // Add a visual "changed" marker to the '#object-1' element.
+ *   $commands[] = ajax_command_changed('#object-1');
+ *   // Menu 'page callback' and #ajax['callback'] functions are supposed to
+ *   // return render arrays. If returning an Ajax commands array, it must be
+ *   // encapsulated in a render array structure.
+ *   return array('#type' => 'ajax', '#commands' => $commands);
+ * @endcode
+ *
+ * When returning an Ajax command array, it is often useful to have
+ * status messages rendered along with other tasks in the command array.
+ * In that case the the Ajax commands array may be constructed like this:
+ * @code
+ *   $commands = array();
+ *   $commands[] = ajax_command_replace(NULL, $output);
+ *   $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
+ *   return array('#type' => 'ajax', '#commands' => $commands);
+ * @endcode
+ *
+ * See @link ajax_commands Ajax framework commands @endlink
+ */
+
+/**
+ * Renders a commands array into JSON.
+ *
+ * @param $commands
+ *   A list of macro commands generated by the use of ajax_command_*()
+ *   functions.
+ */
+function ajax_render($commands = array()) {
+  // Ajax responses aren't rendered with html.tpl.php, so we have to call
+  // drupal_get_css() and drupal_get_js() here, in order to have new files added
+  // during this request to be loaded by the page. We only want to send back
+  // files that the page hasn't already loaded, so we implement simple diffing
+  // logic using array_diff_key().
+  foreach (array('css', 'js') as $type) {
+    // It is highly suspicious if $_POST['ajax_page_state'][$type] is empty,
+    // since the base page ought to have at least one JS file and one CSS file
+    // loaded. It probably indicates an error, and rather than making the page
+    // reload all of the files, instead we return no new files.
+    if (empty($_POST['ajax_page_state'][$type])) {
+      $items[$type] = array();
+    }
+    else {
+      $function = 'drupal_add_' . $type;
+      $items[$type] = $function();
+      drupal_alter($type, $items[$type]);
+      // @todo Inline CSS and JS items are indexed numerically. These can't be
+      //   reliably diffed with array_diff_key(), since the number can change
+      //   due to factors unrelated to the inline content, so for now, we strip
+      //   the inline items from Ajax responses, and can add support for them
+      //   when drupal_add_css() and drupal_add_js() are changed to use a hash
+      //   of the inline content as the array key.
+      foreach ($items[$type] as $key => $item) {
+        if (is_numeric($key)) {
+          unset($items[$type][$key]);
+        }
+      }
+      // Ensure that the page doesn't reload what it already has.
+      $items[$type] = array_diff_key($items[$type], $_POST['ajax_page_state'][$type]);
+    }
+  }
+
+  // Render the HTML to load these files, and add AJAX commands to insert this
+  // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
+  // data from being altered again, as we already altered it above. Settings are
+  // handled separately, afterwards.
+  if (isset($items['js']['settings'])) {
+    unset($items['js']['settings']);
+  }
+  $styles = drupal_get_css($items['css'], TRUE);
+  $scripts_footer = drupal_get_js('footer', $items['js'], TRUE);
+  $scripts_header = drupal_get_js('header', $items['js'], TRUE);
+
+  $extra_commands = array();
+  if (!empty($styles)) {
+    $extra_commands[] = ajax_command_prepend('head', $styles);
+  }
+  if (!empty($scripts_header)) {
+    $extra_commands[] = ajax_command_prepend('head', $scripts_header);
+  }
+  if (!empty($scripts_footer)) {
+    $extra_commands[] = ajax_command_append('body', $scripts_footer);
+  }
+  if (!empty($extra_commands)) {
+    $commands = array_merge($extra_commands, $commands);
+  }
+
+  // Now add a command to merge changes and additions to Drupal.settings.
+  $scripts = drupal_add_js();
+  if (!empty($scripts['settings'])) {
+    $settings = $scripts['settings'];
+    array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $settings['data']), TRUE));
+  }
+
+  // Allow modules to alter any Ajax response.
+  drupal_alter('ajax_render', $commands);
+
+  return drupal_json_encode($commands);
+}
+
+/**
+ * Gets a form submitted via #ajax during an Ajax callback.
+ *
+ * This will load a form from the form cache used during Ajax operations. It
+ * pulls the form info from $_POST.
+ *
+ * @return
+ *   An array containing the $form and $form_state. Use the list() function
+ *   to break these apart:
+ *   @code
+ *     list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
+ *   @endcode
+ */
+function ajax_get_form() {
+  $form_state = form_state_defaults();
+
+  $form_build_id = $_POST['form_build_id'];
+
+  // Get the form from the cache.
+  $form = form_get_cache($form_build_id, $form_state);
+  if (!$form) {
+    // If $form cannot be loaded from the cache, the form_build_id in $_POST
+    // must be invalid, which means that someone performed a POST request onto
+    // system/ajax without actually viewing the concerned form in the browser.
+    // This is likely a hacking attempt as it never happens under normal
+    // circumstances, so we just do nothing.
+    watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
+    drupal_exit();
+  }
+
+  // Since some of the submit handlers are run, redirects need to be disabled.
+  $form_state['no_redirect'] = TRUE;
+
+  // When a form is rebuilt after Ajax processing, its #build_id and #action
+  // should not change.
+  // @see drupal_rebuild_form()
+  $form_state['rebuild_info']['copy']['#build_id'] = TRUE;
+  $form_state['rebuild_info']['copy']['#action'] = TRUE;
+
+  // The form needs to be processed; prepare for that by setting a few internal
+  // variables.
+  $form_state['input'] = $_POST;
+  $form_id = $form['#form_id'];
+
+  return array($form, $form_state, $form_id, $form_build_id);
+}
+
+/**
+ * Menu callback; handles Ajax requests for the #ajax Form API property.
+ *
+ * This rebuilds the form from cache and invokes the defined #ajax['callback']
+ * to return an Ajax command structure for JavaScript. In case no 'callback' has
+ * been defined, nothing will happen.
+ *
+ * The Form API #ajax property can be set both for buttons and other input
+ * elements.
+ *
+ * This function is also the canonical example of how to implement
+ * #ajax['path']. If processing is required that cannot be accomplished with
+ * a callback, re-implement this function and set #ajax['path'] to the
+ * enhanced function.
+ *
+ * @see system_menu()
+ */
+function ajax_form_callback() {
+  list($form, $form_state) = ajax_get_form();
+  drupal_process_form($form['#form_id'], $form, $form_state);
+
+  // We need to return the part of the form (or some other content) that needs
+  // to be re-rendered so the browser can update the page with changed content.
+  // Since this is the generic menu callback used by many Ajax elements, it is
+  // up to the #ajax['callback'] function of the element (may or may not be a
+  // button) that triggered the Ajax request to determine what needs to be
+  // rendered.
+  if (!empty($form_state['triggering_element'])) {
+    $callback = $form_state['triggering_element']['#ajax']['callback'];
+  }
+  if (!empty($callback) && function_exists($callback)) {
+    return $callback($form, $form_state);
+  }
+}
+
+/**
+ * Theme callback for Ajax requests.
+ *
+ * Many different pages can invoke an Ajax request to system/ajax or another
+ * generic Ajax path. It is almost always desired for an Ajax response to be
+ * rendered using the same theme as the base page, because most themes are built
+ * with the assumption that they control the entire page, so if the CSS for two
+ * themes are both loaded for a given page, they may conflict with each other.
+ * For example, Bartik is Drupal's default theme, and Seven is Drupal's default
+ * administration theme. Depending on whether the "Use the administration theme
+ * when editing or creating content" checkbox is checked, the node edit form may
+ * be displayed in either theme, but the Ajax response to the Field module's
+ * "Add another item" button should be rendered using the same theme as the rest
+ * of the page. Therefore, system_menu() sets the 'theme callback' for
+ * 'system/ajax' to this function, and it is recommended that modules
+ * implementing other generic Ajax paths do the same.
+ *
+ * @see system_menu()
+ * @see file_menu()
+ */
+function ajax_base_page_theme() {
+  if (!empty($_POST['ajax_page_state']['theme']) && !empty($_POST['ajax_page_state']['theme_token'])) {
+    $theme = $_POST['ajax_page_state']['theme'];
+    $token = $_POST['ajax_page_state']['theme_token'];
+
+    // Prevent a request forgery from giving a person access to a theme they
+    // shouldn't be otherwise allowed to see. However, since everyone is allowed
+    // to see the default theme, token validation isn't required for that, and
+    // bypassing it allows most use-cases to work even when accessed from the
+    // page cache.
+    if ($theme === variable_get('theme_default', 'bartik') || drupal_valid_token($token, $theme)) {
+      return $theme;
+    }
+  }
+}
+
+/**
+ * Packages and sends the result of a page callback as an Ajax response.
+ *
+ * This function is the equivalent of drupal_deliver_html_page(), but for Ajax
+ * requests. Like that function, it:
+ * - Adds needed HTTP headers.
+ * - Prints rendered output.
+ * - Performs end-of-request tasks.
+ *
+ * @param $page_callback_result
+ *   The result of a page callback. Can be one of:
+ *   - NULL: to indicate no content.
+ *   - An integer menu status constant: to indicate an error condition.
+ *   - A string of HTML content.
+ *   - A renderable array of content.
+ *
+ * @see drupal_deliver_html_page()
+ */
+function ajax_deliver($page_callback_result) {
+  // Browsers do not allow JavaScript to read the contents of a user's local
+  // files. To work around that, the jQuery Form plugin submits forms containing
+  // a file input element to an IFRAME, instead of using XHR. Browsers do not
+  // normally expect JSON strings as content within an IFRAME, so the response
+  // must be customized accordingly.
+  // @see http://malsup.com/jquery/form/#file-upload
+  // @see Drupal.ajax.prototype.beforeSend()
+  $iframe_upload = !empty($_POST['ajax_iframe_upload']);
+
+  // Emit a Content-Type HTTP header if none has been added by the page callback
+  // or by a wrapping delivery callback.
+  if (is_null(drupal_get_http_header('Content-Type'))) {
+    if (!$iframe_upload) {
+      // Standard JSON can be returned to a browser's XHR object, and to
+      // non-browser user agents.
+      // @see http://www.ietf.org/rfc/rfc4627.txt?number=4627
+      drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
+    }
+    else {
+      // Browser IFRAMEs expect HTML. With most other content types, Internet
+      // Explorer presents the user with a download prompt.
+      drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
+    }
+  }
+
+  // Print the response.
+  $commands = ajax_prepare_response($page_callback_result);
+  $json = ajax_render($commands);
+  if (!$iframe_upload) {
+    // Standard JSON can be returned to a browser's XHR object, and to
+    // non-browser user agents.
+    print $json;
+  }
+  else {
+    // Browser IFRAMEs expect HTML. Browser extensions, such as Linkification
+    // and Skype's Browser Highlighter, convert URLs, phone numbers, etc. into
+    // links. This corrupts the JSON response. Protect the integrity of the
+    // JSON data by making it the value of a textarea.
+    // @see http://malsup.com/jquery/form/#file-upload
+    // @see http://drupal.org/node/1009382
+    print '<textarea>' . $json . '</textarea>';
+  }
+
+  // Perform end-of-request tasks.
+  ajax_footer();
+}
+
+/**
+ * Converts the return value of a page callback into an Ajax commands array.
+ *
+ * @param $page_callback_result
+ *   The result of a page callback. Can be one of:
+ *   - NULL: to indicate no content.
+ *   - An integer menu status constant: to indicate an error condition.
+ *   - A string of HTML content.
+ *   - A renderable array of content.
+ *
+ * @return
+ *   An Ajax commands array that can be passed to ajax_render().
+ */
+function ajax_prepare_response($page_callback_result) {
+  $commands = array();
+  if (!isset($page_callback_result)) {
+    // Simply delivering an empty commands array is sufficient. This results
+    // in the Ajax request being completed, but nothing being done to the page.
+  }
+  elseif (is_int($page_callback_result)) {
+    switch ($page_callback_result) {
+      case MENU_NOT_FOUND:
+        $commands[] = ajax_command_alert(t('The requested page could not be found.'));
+        break;
+
+      case MENU_ACCESS_DENIED:
+        $commands[] = ajax_command_alert(t('You are not authorized to access this page.'));
+        break;
+
+      case MENU_SITE_OFFLINE:
+        $commands[] = ajax_command_alert(filter_xss_admin(variable_get('maintenance_mode_message',
+          t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))))));
+        break;
+    }
+  }
+  elseif (is_array($page_callback_result) && isset($page_callback_result['#type']) && ($page_callback_result['#type'] == 'ajax')) {
+    // Complex Ajax callbacks can return a result that contains an error message
+    // or a specific set of commands to send to the browser.
+    $page_callback_result += element_info('ajax');
+    $error = $page_callback_result['#error'];
+    if (isset($error) && $error !== FALSE) {
+      if ((empty($error) || $error === TRUE)) {
+        $error = t('An error occurred while handling the request: The server received invalid input.');
+      }
+      $commands[] = ajax_command_alert($error);
+    }
+    else {
+      $commands = $page_callback_result['#commands'];
+    }
+  }
+  else {
+    // Like normal page callbacks, simple Ajax callbacks can return HTML
+    // content, as a string or render array. This HTML is inserted in some
+    // relationship to #ajax['wrapper'], as determined by which jQuery DOM
+    // manipulation method is used. The method used is specified by
+    // #ajax['method']. The default method is 'replaceWith', which completely
+    // replaces the old wrapper element and its content with the new HTML.
+    $html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result);
+    $commands[] = ajax_command_insert(NULL, $html);
+    // Add the status messages inside the new content's wrapper element, so that
+    // on subsequent Ajax requests, it is treated as old content.
+    $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
+  }
+
+  return $commands;
+}
+
+/**
+ * Performs end-of-Ajax-request tasks.
+ *
+ * This function is the equivalent of drupal_page_footer(), but for Ajax
+ * requests.
+ *
+ * @see drupal_page_footer()
+ */
+function ajax_footer() {
+  // Even for Ajax requests, invoke hook_exit() implementations. There may be
+  // modules that need very fast Ajax responses, and therefore, run Ajax
+  // requests with an early bootstrap.
+  if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')) {
+    module_invoke_all('exit');
+  }
+
+  // Commit the user session. See above comment about the possibility of this
+  // function running without session.inc loaded.
+  if (function_exists('drupal_session_commit')) {
+    drupal_session_commit();
+  }
+}
+
+/**
+ * Form element processing handler for the #ajax form property.
+ *
+ * @param $element
+ *   An associative array containing the properties of the element.
+ *
+ * @return
+ *   The processed element.
+ *
+ * @see ajax_pre_render_element()
+ */
+function ajax_process_form($element, &$form_state) {
+  $element = ajax_pre_render_element($element);
+  if (!empty($element['#ajax_processed'])) {
+    $form_state['cache'] = TRUE;
+  }
+  return $element;
+}
+
+/**
+ * Adds Ajax information about an element to communicate with JavaScript.
+ *
+ * If #ajax['path'] is set on an element, this additional JavaScript is added
+ * to the page header to attach the Ajax behaviors. See ajax.js for more
+ * information.
+ *
+ * @param $element
+ *   An associative array containing the properties of the element.
+ *   Properties used:
+ *   - #ajax['event']
+ *   - #ajax['prevent']
+ *   - #ajax['path']
+ *   - #ajax['options']
+ *   - #ajax['wrapper']
+ *   - #ajax['parameters']
+ *   - #ajax['effect']
+ *
+ * @return
+ *   The processed element with the necessary JavaScript attached to it.
+ */
+function ajax_pre_render_element($element) {
+  // Skip already processed elements.
+  if (isset($element['#ajax_processed'])) {
+    return $element;
+  }
+  // Initialize #ajax_processed, so we do not process this element again.
+  $element['#ajax_processed'] = FALSE;
+
+  // Nothing to do if there is neither a callback nor a path.
+  if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']))) {
+    return $element;
+  }
+
+  // Add a reasonable default event handler if none was specified.
+  if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
+    switch ($element['#type']) {
+      case 'submit':
+      case 'button':
+      case 'image_button':
+        // Pressing the ENTER key within a textfield triggers the click event of
+        // the form's first submit button. Triggering Ajax in this situation
+        // leads to problems, like breaking autocomplete textfields, so we bind
+        // to mousedown instead of click.
+        // @see http://drupal.org/node/216059
+        $element['#ajax']['event'] = 'mousedown';
+        // Retain keyboard accessibility by setting 'keypress'. This causes
+        // ajax.js to trigger 'event' when SPACE or ENTER are pressed while the
+        // button has focus.
+        $element['#ajax']['keypress'] = TRUE;
+        // Binding to mousedown rather than click means that it is possible to
+        // trigger a click by pressing the mouse, holding the mouse button down
+        // until the Ajax request is complete and the button is re-enabled, and
+        // then releasing the mouse button. Set 'prevent' so that ajax.js binds
+        // an additional handler to prevent such a click from triggering a
+        // non-Ajax form submission. This also prevents a textfield's ENTER
+        // press triggering this button's non-Ajax form submission behavior.
+        if (!isset($element['#ajax']['prevent'])) {
+          $element['#ajax']['prevent'] = 'click';
+        }
+        break;
+
+      case 'password':
+      case 'textfield':
+      case 'textarea':
+        $element['#ajax']['event'] = 'blur';
+        break;
+
+      case 'radio':
+      case 'checkbox':
+      case 'select':
+        $element['#ajax']['event'] = 'change';
+        break;
+
+      case 'link':
+        $element['#ajax']['event'] = 'click';
+        break;
+
+      default:
+        return $element;
+    }
+  }
+
+  // Attach JavaScript settings to the element.
+  if (isset($element['#ajax']['event'])) {
+    $element['#attached']['library'][] = array('system', 'jquery.form');
+    $element['#attached']['library'][] = array('system', 'drupal.ajax');
+
+    $settings = $element['#ajax'];
+
+    // Assign default settings.
+    $settings += array(
+      'path' => 'system/ajax',
+      'options' => array(),
+    );
+
+    // @todo Legacy support. Remove in Drupal 8.
+    if (isset($settings['method']) && $settings['method'] == 'replace') {
+      $settings['method'] = 'replaceWith';
+    }
+
+    // Change path to URL.
+    $settings['url'] = url($settings['path'], $settings['options']);
+    unset($settings['path'], $settings['options']);
+
+    // Add special data to $settings['submit'] so that when this element
+    // triggers an Ajax submission, Drupal's form processing can determine which
+    // element triggered it.
+    // @see _form_element_triggered_scripted_submission()
+    if (isset($settings['trigger_as'])) {
+      // An element can add a 'trigger_as' key within #ajax to make the element
+      // submit as though another one (for example, a non-button can use this
+      // to submit the form as though a button were clicked). When using this,
+      // the 'name' key is always required to identify the element to trigger
+      // as. The 'value' key is optional, and only needed when multiple elements
+      // share the same name, which is commonly the case for buttons.
+      $settings['submit']['_triggering_element_name'] = $settings['trigger_as']['name'];
+      if (isset($settings['trigger_as']['value'])) {
+        $settings['submit']['_triggering_element_value'] = $settings['trigger_as']['value'];
+      }
+      unset($settings['trigger_as']);
+    }
+    elseif (isset($element['#name'])) {
+      // Most of the time, elements can submit as themselves, in which case the
+      // 'trigger_as' key isn't needed, and the element's name is used.
+      $settings['submit']['_triggering_element_name'] = $element['#name'];
+      // If the element is a (non-image) button, its name may not identify it
+      // uniquely, in which case a match on value is also needed.
+      // @see _form_button_was_clicked()
+      if (isset($element['#button_type']) && empty($element['#has_garbage_value'])) {
+        $settings['submit']['_triggering_element_value'] = $element['#value'];
+      }
+    }
+
+    // Convert a simple #ajax['progress'] string into an array.
+    if (isset($settings['progress']) && is_string($settings['progress'])) {
+      $settings['progress'] = array('type' => $settings['progress']);
+    }
+    // Change progress path to a full URL.
+    if (isset($settings['progress']['path'])) {
+      $settings['progress']['url'] = url($settings['progress']['path']);
+      unset($settings['progress']['path']);
+    }
+
+    $element['#attached']['js'][] = array(
+      'type' => 'setting',
+      'data' => array('ajax' => array($element['#id'] => $settings)),
+    );
+
+    // Indicate that Ajax processing was successful.
+    $element['#ajax_processed'] = TRUE;
+  }
+  return $element;
+}
+
+/**
+ * @} End of "defgroup ajax".
+ */
+
+/**
+ * @defgroup ajax_commands Ajax framework commands
+ * @{
+ * Functions to create various Ajax commands.
+ *
+ * These functions can be used to create arrays for use with the
+ * ajax_render() function.
+ */
+
+/**
+ * Creates a Drupal Ajax 'alert' command.
+ *
+ * The 'alert' command instructs the client to display a JavaScript alert
+ * dialog box.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.alert()
+ * defined in misc/ajax.js.
+ *
+ * @param $text
+ *   The message string to display to the user.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_alert($text) {
+  return array(
+    'command' => 'alert',
+    'text' => $text,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'insert' command using the method in #ajax['method'].
+ *
+ * This command instructs the client to insert the given HTML using whichever
+ * jQuery DOM manipulation method has been specified in the #ajax['method']
+ * variable of the element that triggered the request.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $html
+ *   The data to use with the jQuery method.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_insert($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => NULL,
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'insert/replaceWith' command.
+ *
+ * The 'insert/replaceWith' command instructs the client to use jQuery's
+ * replaceWith() method to replace each element matched matched by the given
+ * selector with the given HTML.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $html
+ *   The data to use with the jQuery replaceWith() method.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * See
+ * @link http://docs.jquery.com/Manipulation/replaceWith#content jQuery replaceWith command @endlink
+ */
+function ajax_command_replace($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'replaceWith',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'insert/html' command.
+ *
+ * The 'insert/html' command instructs the client to use jQuery's html()
+ * method to set the HTML content of each element matched by the given
+ * selector while leaving the outer tags intact.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $html
+ *   The data to use with the jQuery html() method.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/Attributes/html#val
+ */
+function ajax_command_html($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'html',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'insert/prepend' command.
+ *
+ * The 'insert/prepend' command instructs the client to use jQuery's prepend()
+ * method to prepend the given HTML content to the inside each element matched
+ * by the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $html
+ *   The data to use with the jQuery prepend() method.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/Manipulation/prepend#content
+ */
+function ajax_command_prepend($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'prepend',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'insert/append' command.
+ *
+ * The 'insert/append' command instructs the client to use jQuery's append()
+ * method to append the given HTML content to the inside of each element matched
+ * by the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $html
+ *   The data to use with the jQuery append() method.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/Manipulation/append#content
+ */
+function ajax_command_append($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'append',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'insert/after' command.
+ *
+ * The 'insert/after' command instructs the client to use jQuery's after()
+ * method to insert the given HTML content after each element matched by
+ * the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $html
+ *   The data to use with the jQuery after() method.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/Manipulation/after#content
+ */
+function ajax_command_after($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'after',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'insert/before' command.
+ *
+ * The 'insert/before' command instructs the client to use jQuery's before()
+ * method to insert the given HTML content before each of elements matched by
+ * the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $html
+ *   The data to use with the jQuery before() method.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/Manipulation/before#content
+ */
+function ajax_command_before($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'before',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'remove' command.
+ *
+ * The 'remove' command instructs the client to use jQuery's remove() method
+ * to remove each of elements matched by the given selector, and everything
+ * within them.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.remove()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/Manipulation/remove#expr
+ */
+function ajax_command_remove($selector) {
+  return array(
+    'command' => 'remove',
+    'selector' => $selector,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'changed' command.
+ *
+ * This command instructs the client to mark each of the elements matched by the
+ * given selector as 'ajax-changed'.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.changed()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $asterisk
+ *   An optional CSS selector which must be inside $selector. If specified,
+ *   an asterisk will be appended to the HTML inside the $asterisk selector.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_changed($selector, $asterisk = '') {
+  return array(
+    'command' => 'changed',
+    'selector' => $selector,
+    'asterisk' => $asterisk,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'css' command.
+ *
+ * The 'css' command will instruct the client to use the jQuery css() method
+ * to apply the CSS arguments to elements matched by the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.css()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $argument
+ *   An array of key/value pairs to set in the CSS for the selector.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/CSS/css#properties
+ */
+function ajax_command_css($selector, $argument) {
+  return array(
+    'command' => 'css',
+    'selector' => $selector,
+    'argument' => $argument,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'settings' command.
+ *
+ * The 'settings' command instructs the client either to use the given array as
+ * the settings for ajax-loaded content or to extend Drupal.settings with the
+ * given array, depending on the value of the $merge parameter.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.settings()
+ * defined in misc/ajax.js.
+ *
+ * @param $argument
+ *   An array of key/value pairs to add to the settings. This will be utilized
+ *   for all commands after this if they do not include their own settings
+ *   array.
+ * @param $merge
+ *   Whether or not the passed settings in $argument should be merged into the
+ *   global Drupal.settings on the page. By default (FALSE), the settings that
+ *   are passed to Drupal.attachBehaviors will not include the global
+ *   Drupal.settings.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_settings($argument, $merge = FALSE) {
+  return array(
+    'command' => 'settings',
+    'settings' => $argument,
+    'merge' => $merge,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'data' command.
+ *
+ * The 'data' command instructs the client to attach the name=value pair of
+ * data to the selector via jQuery's data cache.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.data()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $name
+ *   The name or key (in the key value pair) of the data attached to this
+ *   selector.
+ * @param $value
+ *   The value of the data. Not just limited to strings can be any format.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see http://docs.jquery.com/Core/data#namevalue
+ */
+function ajax_command_data($selector, $name, $value) {
+  return array(
+    'command' => 'data',
+    'selector' => $selector,
+    'name' => $name,
+    'value' => $value,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'invoke' command.
+ *
+ * The 'invoke' command will instruct the client to invoke the given jQuery
+ * method with the supplied arguments on the elements matched by the given
+ * selector. Intended for simple jQuery commands, such as attr(), addClass(),
+ * removeClass(), toggleClass(), etc.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.invoke()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string. If the command is a response to a request from
+ *   an #ajax form element then this value can be NULL.
+ * @param $method
+ *   The jQuery method to invoke.
+ * @param $arguments
+ *   (optional) A list of arguments to the jQuery $method, if any.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_invoke($selector, $method, array $arguments = array()) {
+  return array(
+    'command' => 'invoke',
+    'selector' => $selector,
+    'method' => $method,
+    'arguments' => $arguments,
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'restripe' command.
+ *
+ * The 'restripe' command instructs the client to restripe a table. This is
+ * usually used after a table has been modified by a replace or append command.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.restripe()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   A jQuery selector string.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_restripe($selector) {
+  return array(
+    'command' => 'restripe',
+    'selector' => $selector,
+  );
+}

+ 68 - 0
includes/archiver.inc

@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Shared classes and interfaces for the archiver system.
+ */
+
+/**
+ * Defines the common interface for all Archiver classes.
+ */
+interface ArchiverInterface {
+
+  /**
+   * Constructs a new archiver instance.
+   *
+   * @param $file_path
+   *   The full system path of the archive to manipulate. Only local files
+   *   are supported. If the file does not yet exist, it will be created if
+   *   appropriate.
+   */
+  public function __construct($file_path);
+
+  /**
+   * Adds the specified file or directory to the archive.
+   *
+   * @param $file_path
+   *   The full system path of the file or directory to add. Only local files
+   *   and directories are supported.
+   *
+   * @return ArchiverInterface
+   *   The called object.
+   */
+  public function add($file_path);
+
+  /**
+   * Removes the specified file from the archive.
+   *
+   * @param $path
+   *   The file name relative to the root of the archive to remove.
+   *
+   * @return ArchiverInterface
+   *   The called object.
+   */
+  public function remove($path);
+
+  /**
+   * Extracts multiple files in the archive to the specified path.
+   *
+   * @param $path
+   *   A full system path of the directory to which to extract files.
+   * @param $files
+   *   Optionally specify a list of files to be extracted. Files are
+   *   relative to the root of the archive. If not specified, all files
+   *   in the archive will be extracted.
+   *
+   * @return ArchiverInterface
+   *   The called object.
+   */
+  public function extract($path, array $files = array());
+
+  /**
+   * Lists all files in the archive.
+   *
+   * @return
+   *   An array of file names relative to the root of the archive.
+   */
+  public function listContents();
+}

+ 334 - 0
includes/authorize.inc

@@ -0,0 +1,334 @@
+<?php
+
+/**
+ * @file
+ * Helper functions and form handlers used for the authorize.php script.
+ */
+
+/**
+ * Form constructor for the file transfer authorization form.
+ *
+ * Allows the user to choose a FileTransfer type and supply credentials.
+ *
+ * @see authorize_filetransfer_form_validate()
+ * @see authorize_filetransfer_form_submit()
+ * @ingroup forms
+ */
+function authorize_filetransfer_form($form, &$form_state) {
+  global $base_url, $is_https;
+  $form = array();
+
+  // If possible, we want to post this form securely via HTTPS.
+  $form['#https'] = TRUE;
+
+  // CSS we depend on lives in modules/system/maintenance.css, which is loaded
+  // via the default maintenance theme.
+  $form['#attached']['js'][] = $base_url . '/misc/authorize.js';
+
+  // Get all the available ways to transfer files.
+  if (empty($_SESSION['authorize_filetransfer_info'])) {
+    drupal_set_message(t('Unable to continue, no available methods of file transfer'), 'error');
+    return array();
+  }
+  $available_backends = $_SESSION['authorize_filetransfer_info'];
+
+  if (!$is_https) {
+    $form['information']['https_warning'] = array(
+      '#prefix' => '<div class="messages error">',
+      '#markup' => t('WARNING: You are not using an encrypted connection, so your password will be sent in plain text. <a href="@https-link">Learn more</a>.', array('@https-link' => 'http://drupal.org/https-information')),
+      '#suffix' => '</div>',
+    );
+  }
+
+  // Decide on a default backend.
+  if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default'])) {
+    $authorize_filetransfer_default = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
+  }
+  elseif ($authorize_filetransfer_default = variable_get('authorize_filetransfer_default', NULL));
+  else {
+    $authorize_filetransfer_default = key($available_backends);
+  }
+
+  $form['information']['main_header'] = array(
+    '#prefix' => '<h3>',
+    '#markup' => t('To continue, provide your server connection details'),
+    '#suffix' => '</h3>',
+  );
+
+  $form['connection_settings']['#tree'] = TRUE;
+  $form['connection_settings']['authorize_filetransfer_default'] = array(
+    '#type' => 'select',
+    '#title' => t('Connection method'),
+    '#default_value' => $authorize_filetransfer_default,
+    '#weight' => -10,
+  );
+
+  /*
+   * Here we create two submit buttons. For a JS enabled client, they will
+   * only ever see submit_process. However, if a client doesn't have JS
+   * enabled, they will see submit_connection on the first form (when picking
+   * what filetransfer type to use, and submit_process on the second one (which
+   * leads to the actual operation).
+   */
+  $form['submit_connection'] = array(
+    '#prefix' => "<br style='clear:both'/>",
+    '#name' => 'enter_connection_settings',
+    '#type' => 'submit',
+    '#value' => t('Enter connection settings'),
+    '#weight' => 100,
+  );
+
+  $form['submit_process'] = array(
+    '#name' => 'process_updates',
+    '#type' => 'submit',
+    '#value' => t('Continue'),
+    '#weight' => 100,
+    '#attributes' => array('style' => 'display:none'),
+  );
+
+  // Build a container for each connection type.
+  foreach ($available_backends as $name => $backend) {
+    $form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title'];
+    $form['connection_settings'][$name] = array(
+      '#type' => 'container',
+      '#attributes' => array('class' => array("filetransfer-$name", 'filetransfer')),
+    );
+    // We can't use #prefix on the container itself since then the header won't
+    // be hidden and shown when the containers are being manipulated via JS.
+    $form['connection_settings'][$name]['header'] = array(
+      '#markup' => '<h4>' . t('@backend connection settings', array('@backend' => $backend['title'])) . '</h4>',
+    );
+
+    $form['connection_settings'][$name] += _authorize_filetransfer_connection_settings($name);
+
+    // Start non-JS code.
+    if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default']) && $form_state['values']['connection_settings']['authorize_filetransfer_default'] == $name) {
+
+      // If the user switches from JS to non-JS, Drupal (and Batch API) will
+      // barf. This is a known bug: http://drupal.org/node/229825.
+      setcookie('has_js', '', time() - 3600, '/');
+      unset($_COOKIE['has_js']);
+
+      // Change the submit button to the submit_process one.
+      $form['submit_process']['#attributes'] = array();
+      unset($form['submit_connection']);
+
+      // Activate the proper filetransfer settings form.
+      $form['connection_settings'][$name]['#attributes']['style'] = 'display:block';
+      // Disable the select box.
+      $form['connection_settings']['authorize_filetransfer_default']['#disabled'] = TRUE;
+
+      // Create a button for changing the type of connection.
+      $form['connection_settings']['change_connection_type'] = array(
+        '#name' => 'change_connection_type',
+        '#type' => 'submit',
+        '#value' => t('Change connection type'),
+        '#weight' => -5,
+        '#attributes' => array('class' => array('filetransfer-change-connection-type')),
+      );
+    }
+    // End non-JS code.
+  }
+  return $form;
+}
+
+/**
+ * Generates the Form API array for a given connection backend's settings.
+ *
+ * @param $backend
+ *   The name of the backend (e.g. 'ftp', 'ssh', etc).
+ *
+ * @return
+ *   Form API array of connection settings for the given backend.
+ *
+ * @see hook_filetransfer_backends()
+ */
+function _authorize_filetransfer_connection_settings($backend) {
+  $defaults = variable_get('authorize_filetransfer_connection_settings_' . $backend, array());
+  $form = array();
+
+  // Create an instance of the file transfer class to get its settings form.
+  $filetransfer = authorize_get_filetransfer($backend);
+  if ($filetransfer) {
+    $form = $filetransfer->getSettingsForm();
+  }
+  // Fill in the defaults based on the saved settings, if any.
+  _authorize_filetransfer_connection_settings_set_defaults($form, NULL, $defaults);
+  return $form;
+}
+
+/**
+ * Sets the default settings on a file transfer connection form recursively.
+ *
+ * The default settings for the file transfer connection forms are saved in
+ * the database. The settings are stored as a nested array in the case of a
+ * settings form that has fieldsets or otherwise uses a nested structure.
+ * Therefore, to properly add defaults, we need to walk through all the
+ * children form elements and process those defaults recursively.
+ *
+ * @param $element
+ *   Reference to the Form API form element we're operating on.
+ * @param $key
+ *   The key for our current form element, if any.
+ * @param array $defaults
+ *   The default settings for the file transfer backend we're operating on.
+ */
+function _authorize_filetransfer_connection_settings_set_defaults(&$element, $key, array $defaults) {
+  // If we're operating on a form element which isn't a fieldset, and we have
+  // a default setting saved, stash it in #default_value.
+  if (!empty($key) && isset($defaults[$key]) && isset($element['#type']) && $element['#type'] != 'fieldset') {
+    $element['#default_value'] = $defaults[$key];
+  }
+  // Now, we walk through all the child elements, and recursively invoke
+  // ourself on each one. Since the $defaults settings array can be nested
+  // (because of #tree, any values inside fieldsets will be nested), if
+  // there's a subarray of settings for the form key we're currently
+  // processing, pass in that subarray to the recursive call. Otherwise, just
+  // pass on the whole $defaults array.
+  foreach (element_children($element) as $child_key) {
+    _authorize_filetransfer_connection_settings_set_defaults($element[$child_key], $child_key, ((isset($defaults[$key]) && is_array($defaults[$key])) ? $defaults[$key] : $defaults));
+  }
+}
+
+/**
+ * Form validation handler for authorize_filetransfer_form().
+ *
+ * @see authorize_filetransfer_form()
+ * @see authorize_filetransfer_submit()
+ */
+function authorize_filetransfer_form_validate($form, &$form_state) {
+  // Only validate the form if we have collected all of the user input and are
+  // ready to proceed with updating or installing.
+  if ($form_state['triggering_element']['#name'] != 'process_updates') {
+    return;
+  }
+
+  if (isset($form_state['values']['connection_settings'])) {
+    $backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
+    $filetransfer = authorize_get_filetransfer($backend, $form_state['values']['connection_settings'][$backend]);
+    try {
+      if (!$filetransfer) {
+        throw new Exception(t('Error, this type of connection protocol (%backend) does not exist.', array('%backend' => $backend)));
+      }
+      $filetransfer->connect();
+    }
+    catch (Exception $e) {
+      // The format of this error message is similar to that used on the
+      // database connection form in the installer.
+      form_set_error('connection_settings', t('Failed to connect to the server. The server reports the following message: !message For more help installing or updating code on your server, see the <a href="@handbook_url">handbook</a>.', array(
+        '!message' => '<p class="error">' . $e->getMessage()  . '</p>',
+        '@handbook_url' => 'http://drupal.org/documentation/install/modules-themes',
+      )));
+    }
+  }
+}
+
+/**
+ * Form submission handler for authorize_filetransfer_form().
+ *
+ * @see authorize_filetransfer_form()
+ * @see authorize_filetransfer_validate()
+ */
+function authorize_filetransfer_form_submit($form, &$form_state) {
+  global $base_url;
+  switch ($form_state['triggering_element']['#name']) {
+    case 'process_updates':
+
+      // Save the connection settings to the DB.
+      $filetransfer_backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
+
+      // If the database is available then try to save our settings. We have
+      // to make sure it is available since this code could potentially (will
+      // likely) be called during the installation process, before the
+      // database is set up.
+      try {
+        $connection_settings = array();
+        foreach ($form_state['values']['connection_settings'][$filetransfer_backend] as $key => $value) {
+          // We do *not* want to store passwords in the database, unless the
+          // backend explicitly says so via the magic #filetransfer_save form
+          // property. Otherwise, we store everything that's not explicitly
+          // marked with #filetransfer_save set to FALSE.
+          if (!isset($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save'])) {
+            if ($form['connection_settings'][$filetransfer_backend][$key]['#type'] != 'password') {
+              $connection_settings[$key] = $value;
+            }
+          }
+          // The attribute is defined, so only save if set to TRUE.
+          elseif ($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save']) {
+            $connection_settings[$key] = $value;
+          }
+        }
+        // Set this one as the default authorize method.
+        variable_set('authorize_filetransfer_default', $filetransfer_backend);
+        // Save the connection settings minus the password.
+        variable_set('authorize_filetransfer_connection_settings_' . $filetransfer_backend, $connection_settings);
+
+        $filetransfer = authorize_get_filetransfer($filetransfer_backend, $form_state['values']['connection_settings'][$filetransfer_backend]);
+
+        // Now run the operation.
+        authorize_run_operation($filetransfer);
+      }
+      catch (Exception $e) {
+        // If there is no database available, we don't care and just skip
+        // this part entirely.
+      }
+
+      break;
+
+    case 'enter_connection_settings':
+      $form_state['rebuild'] = TRUE;
+      break;
+
+    case 'change_connection_type':
+      $form_state['rebuild'] = TRUE;
+      unset($form_state['values']['connection_settings']['authorize_filetransfer_default']);
+      break;
+  }
+}
+
+/**
+ * Runs the operation specified in $_SESSION['authorize_operation'].
+ *
+ * @param $filetransfer
+ *   The FileTransfer object to use for running the operation.
+ */
+function authorize_run_operation($filetransfer) {
+  $operation = $_SESSION['authorize_operation'];
+  unset($_SESSION['authorize_operation']);
+
+  if (!empty($operation['page_title'])) {
+    drupal_set_title($operation['page_title']);
+  }
+
+  require_once DRUPAL_ROOT . '/' . $operation['file'];
+  call_user_func_array($operation['callback'], array_merge(array($filetransfer), $operation['arguments']));
+}
+
+/**
+ * Gets a FileTransfer class for a specific transfer method and settings.
+ *
+ * @param $backend
+ *   The FileTransfer backend to get the class for.
+ * @param $settings
+ *   Array of settings for the FileTransfer.
+ *
+ * @return
+ *   An instantiated FileTransfer object for the requested method and settings,
+ *   or FALSE if there was an error finding or instantiating it.
+ */
+function authorize_get_filetransfer($backend, $settings = array()) {
+  $filetransfer = FALSE;
+  if (!empty($_SESSION['authorize_filetransfer_info'][$backend])) {
+    $backend_info = $_SESSION['authorize_filetransfer_info'][$backend];
+    if (!empty($backend_info['file'])) {
+      $file = $backend_info['file path'] . '/' . $backend_info['file'];
+      require_once $file;
+    }
+    if (class_exists($backend_info['class'])) {
+      // PHP 5.2 doesn't support $class::factory() syntax, so we have to
+      // use call_user_func_array() until we can require PHP 5.3.
+      $filetransfer = call_user_func_array(array($backend_info['class'], 'factory'), array(DRUPAL_ROOT, $settings));
+    }
+  }
+  return $filetransfer;
+}

+ 539 - 0
includes/batch.inc

@@ -0,0 +1,539 @@
+<?php
+
+/**
+ * @file
+ * Batch processing API for processes to run in multiple HTTP requests.
+ *
+ * Note that batches are usually invoked by form submissions, which is
+ * why the core interaction functions of the batch processing API live in
+ * form.inc.
+ *
+ * @see form.inc
+ * @see batch_set()
+ * @see batch_process()
+ * @see batch_get()
+ */
+
+/**
+ * Loads a batch from the database.
+ *
+ * @param $id
+ *   The ID of the batch to load. When a progressive batch is being processed,
+ *   the relevant ID is found in $_REQUEST['id'].
+ *
+ * @return
+ *   An array representing the batch, or FALSE if no batch was found.
+ */
+function batch_load($id) {
+  $batch = db_query("SELECT batch FROM {batch} WHERE bid = :bid AND token = :token", array(
+    ':bid' => $id,
+    ':token' => drupal_get_token($id),
+  ))->fetchField();
+  if ($batch) {
+    return unserialize($batch);
+  }
+  return FALSE;
+}
+
+/**
+ * Renders the batch processing page based on the current state of the batch.
+ *
+ * @see _batch_shutdown()
+ */
+function _batch_page() {
+  $batch = &batch_get();
+
+  if (!isset($_REQUEST['id'])) {
+    return FALSE;
+  }
+
+  // Retrieve the current state of the batch.
+  if (!$batch) {
+    $batch = batch_load($_REQUEST['id']);
+    if (!$batch) {
+      drupal_set_message(t('No active batch.'), 'error');
+      drupal_goto();
+    }
+  }
+
+  // Register database update for the end of processing.
+  drupal_register_shutdown_function('_batch_shutdown');
+
+  // Add batch-specific CSS.
+  foreach ($batch['sets'] as $batch_set) {
+    if (isset($batch_set['css'])) {
+      foreach ($batch_set['css'] as $css) {
+        drupal_add_css($css);
+      }
+    }
+  }
+
+  $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
+  $output = NULL;
+  switch ($op) {
+    case 'start':
+      $output = _batch_start();
+      break;
+
+    case 'do':
+      // JavaScript-based progress page callback.
+      _batch_do();
+      break;
+
+    case 'do_nojs':
+      // Non-JavaScript-based progress page.
+      $output = _batch_progress_page_nojs();
+      break;
+
+    case 'finished':
+      $output = _batch_finished();
+      break;
+  }
+
+  return $output;
+}
+
+/**
+ * Initializes the batch processing.
+ *
+ * JavaScript-enabled clients are identified by the 'has_js' cookie set in
+ * drupal.js. If no JavaScript-enabled page has been visited during the current
+ * user's browser session, the non-JavaScript version is returned.
+ */
+function _batch_start() {
+  if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
+    return _batch_progress_page_js();
+  }
+  else {
+    return _batch_progress_page_nojs();
+  }
+}
+
+/**
+ * Outputs a batch processing page with JavaScript support.
+ *
+ * This initializes the batch and error messages. Note that in JavaScript-based
+ * processing, the batch processing page is displayed only once and updated via
+ * AHAH requests, so only the first batch set gets to define the page title.
+ * Titles specified by subsequent batch sets are not displayed.
+ *
+ * @see batch_set()
+ * @see _batch_do()
+ */
+function _batch_progress_page_js() {
+  $batch = batch_get();
+
+  $current_set = _batch_current_set();
+  drupal_set_title($current_set['title'], PASS_THROUGH);
+
+  // Merge required query parameters for batch processing into those provided by
+  // batch_set() or hook_batch_alter().
+  $batch['url_options']['query']['id'] = $batch['id'];
+
+  $js_setting = array(
+    'batch' => array(
+      'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
+      'initMessage' => $current_set['init_message'],
+      'uri' => url($batch['url'], $batch['url_options']),
+    ),
+  );
+  drupal_add_js($js_setting, 'setting');
+  drupal_add_library('system', 'drupal.batch');
+
+  return '<div id="progress"></div>';
+}
+
+/**
+ * Does one execution pass with JavaScript and returns progress to the browser.
+ *
+ * @see _batch_progress_page_js()
+ * @see _batch_process()
+ */
+function _batch_do() {
+  // HTTP POST required.
+  if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+    drupal_set_message(t('HTTP POST is required.'), 'error');
+    drupal_set_title(t('Error'));
+    return '';
+  }
+
+  // Perform actual processing.
+  list($percentage, $message) = _batch_process();
+
+  drupal_json_output(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
+}
+
+/**
+ * Outputs a batch processing page without JavaScript support.
+ *
+ * @see _batch_process()
+ */
+function _batch_progress_page_nojs() {
+  $batch = &batch_get();
+
+  $current_set = _batch_current_set();
+  drupal_set_title($current_set['title'], PASS_THROUGH);
+
+  $new_op = 'do_nojs';
+
+  if (!isset($batch['running'])) {
+    // This is the first page so we return some output immediately.
+    $percentage       = 0;
+    $message          = $current_set['init_message'];
+    $batch['running'] = TRUE;
+  }
+  else {
+    // This is one of the later requests; do some processing first.
+
+    // Error handling: if PHP dies due to a fatal error (e.g. a nonexistent
+    // function), it will output whatever is in the output buffer, followed by
+    // the error message.
+    ob_start();
+    $fallback = $current_set['error_message'] . '<br />' . $batch['error_message'];
+    $fallback = theme('maintenance_page', array('content' => $fallback, 'show_messages' => FALSE));
+
+    // We strip the end of the page using a marker in the template, so any
+    // additional HTML output by PHP shows up inside the page rather than below
+    // it. While this causes invalid HTML, the same would be true if we didn't,
+    // as content is not allowed to appear after </html> anyway.
+    list($fallback) = explode('<!--partial-->', $fallback);
+    print $fallback;
+
+    // Perform actual processing.
+    list($percentage, $message) = _batch_process($batch);
+    if ($percentage == 100) {
+      $new_op = 'finished';
+    }
+
+    // PHP did not die; remove the fallback output.
+    ob_end_clean();
+  }
+
+  // Merge required query parameters for batch processing into those provided by
+  // batch_set() or hook_batch_alter().
+  $batch['url_options']['query']['id'] = $batch['id'];
+  $batch['url_options']['query']['op'] = $new_op;
+
+  $url = url($batch['url'], $batch['url_options']);
+  $element = array(
+    '#tag' => 'meta',
+    '#attributes' => array(
+      'http-equiv' => 'Refresh',
+      'content' => '0; URL=' . $url,
+    ),
+  );
+  drupal_add_html_head($element, 'batch_progress_meta_refresh');
+
+  return theme('progress_bar', array('percent' => $percentage, 'message' => $message));
+}
+
+/**
+ * Processes sets in a batch.
+ *
+ * If the batch was marked for progressive execution (default), this executes as
+ * many operations in batch sets until an execution time of 1 second has been
+ * exceeded. It will continue with the next operation of the same batch set in
+ * the next request.
+ *
+ * @return
+ *   An array containing a completion value (in percent) and a status message.
+ */
+function _batch_process() {
+  $batch       = &batch_get();
+  $current_set = &_batch_current_set();
+  // Indicate that this batch set needs to be initialized.
+  $set_changed = TRUE;
+
+  // If this batch was marked for progressive execution (e.g. forms submitted by
+  // drupal_form_submit()), initialize a timer to determine whether we need to
+  // proceed with the same batch phase when a processing time of 1 second has
+  // been exceeded.
+  if ($batch['progressive']) {
+    timer_start('batch_processing');
+  }
+
+  if (empty($current_set['start'])) {
+    $current_set['start'] = microtime(TRUE);
+  }
+
+  $queue = _batch_queue($current_set);
+
+  while (!$current_set['success']) {
+    // If this is the first time we iterate this batch set in the current
+    // request, we check if it requires an additional file for functions
+    // definitions.
+    if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
+      include_once DRUPAL_ROOT . '/' . $current_set['file'];
+    }
+
+    $task_message = '';
+    // Assume a single pass operation and set the completion level to 1 by
+    // default.
+    $finished = 1;
+
+    if ($item = $queue->claimItem()) {
+      list($function, $args) = $item->data;
+
+      // Build the 'context' array and execute the function call.
+      $batch_context = array(
+        'sandbox'  => &$current_set['sandbox'],
+        'results'  => &$current_set['results'],
+        'finished' => &$finished,
+        'message'  => &$task_message,
+      );
+      call_user_func_array($function, array_merge($args, array(&$batch_context)));
+
+      if ($finished >= 1) {
+        // Make sure this step is not counted twice when computing $current.
+        $finished = 0;
+        // Remove the processed operation and clear the sandbox.
+        $queue->deleteItem($item);
+        $current_set['count']--;
+        $current_set['sandbox'] = array();
+      }
+    }
+
+    // When all operations in the current batch set are completed, browse
+    // through the remaining sets, marking them 'successfully processed'
+    // along the way, until we find a set that contains operations.
+    // _batch_next_set() executes form submit handlers stored in 'control'
+    // sets (see form_execute_handlers()), which can in turn add new sets to
+    // the batch.
+    $set_changed = FALSE;
+    $old_set = $current_set;
+    while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
+      $current_set = &_batch_current_set();
+      $current_set['start'] = microtime(TRUE);
+      $set_changed = TRUE;
+    }
+
+    // At this point, either $current_set contains operations that need to be
+    // processed or all sets have been completed.
+    $queue = _batch_queue($current_set);
+
+    // If we are in progressive mode, break processing after 1 second.
+    if ($batch['progressive'] && timer_read('batch_processing') > 1000) {
+      // Record elapsed wall clock time.
+      $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
+      break;
+    }
+  }
+
+  if ($batch['progressive']) {
+    // Gather progress information.
+
+    // Reporting 100% progress will cause the whole batch to be considered
+    // processed. If processing was paused right after moving to a new set,
+    // we have to use the info from the new (unprocessed) set.
+    if ($set_changed && isset($current_set['queue'])) {
+      // Processing will continue with a fresh batch set.
+      $remaining        = $current_set['count'];
+      $total            = $current_set['total'];
+      $progress_message = $current_set['init_message'];
+      $task_message     = '';
+    }
+    else {
+      // Processing will continue with the current batch set.
+      $remaining        = $old_set['count'];
+      $total            = $old_set['total'];
+      $progress_message = $old_set['progress_message'];
+    }
+
+    // Total progress is the number of operations that have fully run plus the
+    // completion level of the current operation.
+    $current    = $total - $remaining + $finished;
+    $percentage = _batch_api_percentage($total, $current);
+    $elapsed    = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
+    $values     = array(
+      '@remaining'  => $remaining,
+      '@total'      => $total,
+      '@current'    => floor($current),
+      '@percentage' => $percentage,
+      '@elapsed'    => format_interval($elapsed / 1000),
+      // If possible, estimate remaining processing time.
+      '@estimate'   => ($current > 0) ? format_interval(($elapsed * ($total - $current) / $current) / 1000) : '-',
+    );
+    $message = strtr($progress_message, $values);
+    if (!empty($message)) {
+      $message .= '<br />';
+    }
+    if (!empty($task_message)) {
+      $message .= $task_message;
+    }
+
+    return array($percentage, $message);
+  }
+  else {
+    // If we are not in progressive mode, the entire batch has been processed.
+    return _batch_finished();
+  }
+}
+
+/**
+ * Formats the percent completion for a batch set.
+ *
+ * @param $total
+ *   The total number of operations.
+ * @param $current
+ *   The number of the current operation. This may be a floating point number
+ *   rather than an integer in the case of a multi-step operation that is not
+ *   yet complete; in that case, the fractional part of $current represents the
+ *   fraction of the operation that has been completed.
+ *
+ * @return
+ *   The properly formatted percentage, as a string. We output percentages
+ *   using the correct number of decimal places so that we never print "100%"
+ *   until we are finished, but we also never print more decimal places than
+ *   are meaningful.
+ *
+ * @see _batch_process()
+ */
+function _batch_api_percentage($total, $current) {
+  if (!$total || $total == $current) {
+    // If $total doesn't evaluate as true or is equal to the current set, then
+    // we're finished, and we can return "100".
+    $percentage = "100";
+  }
+  else {
+    // We add a new digit at 200, 2000, etc. (since, for example, 199/200
+    // would round up to 100% if we didn't).
+    $decimal_places = max(0, floor(log10($total / 2.0)) - 1);
+    do {
+      // Calculate the percentage to the specified number of decimal places.
+      $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
+      // When $current is an integer, the above calculation will always be
+      // correct. However, if $current is a floating point number (in the case
+      // of a multi-step batch operation that is not yet complete), $percentage
+      // may be erroneously rounded up to 100%. To prevent that, we add one
+      // more decimal place and try again.
+      $decimal_places++;
+    } while ($percentage == '100');
+  }
+  return $percentage;
+}
+
+/**
+ * Returns the batch set being currently processed.
+ */
+function &_batch_current_set() {
+  $batch = &batch_get();
+  return $batch['sets'][$batch['current_set']];
+}
+
+/**
+ * Retrieves the next set in a batch.
+ *
+ * If there is a subsequent set in this batch, assign it as the new set to
+ * process and execute its form submit handler (if defined), which may add
+ * further sets to this batch.
+ *
+ * @return
+ *   TRUE if a subsequent set was found in the batch.
+ */
+function _batch_next_set() {
+  $batch = &batch_get();
+  if (isset($batch['sets'][$batch['current_set'] + 1])) {
+    $batch['current_set']++;
+    $current_set = &_batch_current_set();
+    if (isset($current_set['form_submit']) && ($function = $current_set['form_submit']) && function_exists($function)) {
+      // We use our stored copies of $form and $form_state to account for
+      // possible alterations by previous form submit handlers.
+      $function($batch['form_state']['complete form'], $batch['form_state']);
+    }
+    return TRUE;
+  }
+}
+
+/**
+ * Ends the batch processing.
+ *
+ * Call the 'finished' callback of each batch set to allow custom handling of
+ * the results and resolve page redirection.
+ */
+function _batch_finished() {
+  $batch = &batch_get();
+
+  // Execute the 'finished' callbacks for each batch set, if defined.
+  foreach ($batch['sets'] as $batch_set) {
+    if (isset($batch_set['finished'])) {
+      // Check if the set requires an additional file for function definitions.
+      if (isset($batch_set['file']) && is_file($batch_set['file'])) {
+        include_once DRUPAL_ROOT . '/' . $batch_set['file'];
+      }
+      if (function_exists($batch_set['finished'])) {
+        $queue = _batch_queue($batch_set);
+        $operations = $queue->getAllItems();
+        $batch_set['finished']($batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
+      }
+    }
+  }
+
+  // Clean up the batch table and unset the static $batch variable.
+  if ($batch['progressive']) {
+    db_delete('batch')
+      ->condition('bid', $batch['id'])
+      ->execute();
+    foreach ($batch['sets'] as $batch_set) {
+      if ($queue = _batch_queue($batch_set)) {
+        $queue->deleteQueue();
+      }
+    }
+  }
+  $_batch = $batch;
+  $batch = NULL;
+
+  // Clean-up the session. Not needed for CLI updates.
+  if (isset($_SESSION)) {
+    unset($_SESSION['batches'][$batch['id']]);
+    if (empty($_SESSION['batches'])) {
+      unset($_SESSION['batches']);
+    }
+  }
+
+  // Redirect if needed.
+  if ($_batch['progressive']) {
+    // Revert the 'destination' that was saved in batch_process().
+    if (isset($_batch['destination'])) {
+      $_GET['destination'] = $_batch['destination'];
+    }
+
+    // Determine the target path to redirect to.
+    if (!isset($_batch['form_state']['redirect'])) {
+      if (isset($_batch['redirect'])) {
+        $_batch['form_state']['redirect'] = $_batch['redirect'];
+      }
+      else {
+        $_batch['form_state']['redirect'] = $_batch['source_url'];
+      }
+    }
+
+    // Use drupal_redirect_form() to handle the redirection logic.
+    drupal_redirect_form($_batch['form_state']);
+
+    // If no redirection happened, redirect to the originating page. In case the
+    // form needs to be rebuilt, save the final $form_state for
+    // drupal_build_form().
+    if (!empty($_batch['form_state']['rebuild'])) {
+      $_SESSION['batch_form_state'] = $_batch['form_state'];
+    }
+    $function = $_batch['redirect_callback'];
+    if (function_exists($function)) {
+      $function($_batch['source_url'], array('query' => array('op' => 'finish', 'id' => $_batch['id'])));
+    }
+  }
+}
+
+/**
+ * Shutdown function: Stores the current batch data for the next request.
+ *
+ * @see _batch_page()
+ * @see drupal_register_shutdown_function()
+ */
+function _batch_shutdown() {
+  if ($batch = batch_get()) {
+    db_update('batch')
+      ->fields(array('batch' => serialize($batch)))
+      ->condition('bid', $batch['id'])
+      ->execute();
+  }
+}

+ 84 - 0
includes/batch.queue.inc

@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Queue handlers used by the Batch API.
+ *
+ * These implementations:
+ * - Ensure FIFO ordering.
+ * - Allow an item to be repeatedly claimed until it is actually deleted (no
+ *   notion of lease time or 'expire' date), to allow multipass operations.
+ */
+
+/**
+ * Defines a batch queue.
+ *
+ * Stale items from failed batches are cleaned from the {queue} table on cron
+ * using the 'created' date.
+ */
+class BatchQueue extends SystemQueue {
+
+  /**
+   * Overrides SystemQueue::claimItem().
+   *
+   * Unlike SystemQueue::claimItem(), this method provides a default lease
+   * time of 0 (no expiration) instead of 30. This allows the item to be
+   * claimed repeatedly until it is deleted.
+   */
+  public function claimItem($lease_time = 0) {
+    $item = db_query_range('SELECT data, item_id FROM {queue} q WHERE name = :name ORDER BY item_id ASC', 0, 1, array(':name' => $this->name))->fetchObject();
+    if ($item) {
+      $item->data = unserialize($item->data);
+      return $item;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Retrieves all remaining items in the queue.
+   *
+   * This is specific to Batch API and is not part of the DrupalQueueInterface.
+   */
+  public function getAllItems() {
+    $result = array();
+    $items = db_query('SELECT data FROM {queue} q WHERE name = :name ORDER BY item_id ASC', array(':name' => $this->name))->fetchAll();
+    foreach ($items as $item) {
+      $result[] = unserialize($item->data);
+    }
+    return $result;
+  }
+}
+
+/**
+ * Defines a batch queue for non-progressive batches.
+ */
+class BatchMemoryQueue extends MemoryQueue {
+
+  /**
+   * Overrides MemoryQueue::claimItem().
+   *
+   * Unlike MemoryQueue::claimItem(), this method provides a default lease
+   * time of 0 (no expiration) instead of 30. This allows the item to be
+   * claimed repeatedly until it is deleted.
+   */
+  public function claimItem($lease_time = 0) {
+    if (!empty($this->queue)) {
+      reset($this->queue);
+      return current($this->queue);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Retrieves all remaining items in the queue.
+   *
+   * This is specific to Batch API and is not part of the DrupalQueueInterface.
+   */
+  public function getAllItems() {
+    $result = array();
+    foreach ($this->queue as $item) {
+      $result[] = $item->data;
+    }
+    return $result;
+  }
+}

+ 3399 - 0
includes/bootstrap.inc

@@ -0,0 +1,3399 @@
+<?php
+
+/**
+ * @file
+ * Functions that need to be loaded on every Drupal request.
+ */
+
+/**
+ * The current system version.
+ */
+define('VERSION', '7.22');
+
+/**
+ * Core API compatibility.
+ */
+define('DRUPAL_CORE_COMPATIBILITY', '7.x');
+
+/**
+ * Minimum supported version of PHP.
+ */
+define('DRUPAL_MINIMUM_PHP', '5.2.4');
+
+/**
+ * Minimum recommended value of PHP memory_limit.
+ */
+define('DRUPAL_MINIMUM_PHP_MEMORY_LIMIT', '32M');
+
+/**
+ * Error reporting level: display no errors.
+ */
+define('ERROR_REPORTING_HIDE', 0);
+
+/**
+ * Error reporting level: display errors and warnings.
+ */
+define('ERROR_REPORTING_DISPLAY_SOME', 1);
+
+/**
+ * Error reporting level: display all messages.
+ */
+define('ERROR_REPORTING_DISPLAY_ALL', 2);
+
+/**
+ * Indicates that the item should never be removed unless explicitly selected.
+ *
+ * The item may be removed using cache_clear_all() with a cache ID.
+ */
+define('CACHE_PERMANENT', 0);
+
+/**
+ * Indicates that the item should be removed at the next general cache wipe.
+ */
+define('CACHE_TEMPORARY', -1);
+
+/**
+ * @defgroup logging_severity_levels Logging severity levels
+ * @{
+ * Logging severity levels as defined in RFC 3164.
+ *
+ * The WATCHDOG_* constant definitions correspond to the logging severity levels
+ * defined in RFC 3164, section 4.1.1. PHP supplies predefined LOG_* constants
+ * for use in the syslog() function, but their values on Windows builds do not
+ * correspond to RFC 3164. The associated PHP bug report was closed with the
+ * comment, "And it's also not a bug, as Windows just have less log levels,"
+ * and "So the behavior you're seeing is perfectly normal."
+ *
+ * @see http://www.faqs.org/rfcs/rfc3164.html
+ * @see http://bugs.php.net/bug.php?id=18090
+ * @see http://php.net/manual/function.syslog.php
+ * @see http://php.net/manual/network.constants.php
+ * @see watchdog()
+ * @see watchdog_severity_levels()
+ */
+
+/**
+ * Log message severity -- Emergency: system is unusable.
+ */
+define('WATCHDOG_EMERGENCY', 0);
+
+/**
+ * Log message severity -- Alert: action must be taken immediately.
+ */
+define('WATCHDOG_ALERT', 1);
+
+/**
+ * Log message severity -- Critical conditions.
+ */
+define('WATCHDOG_CRITICAL', 2);
+
+/**
+ * Log message severity -- Error conditions.
+ */
+define('WATCHDOG_ERROR', 3);
+
+/**
+ * Log message severity -- Warning conditions.
+ */
+define('WATCHDOG_WARNING', 4);
+
+/**
+ * Log message severity -- Normal but significant conditions.
+ */
+define('WATCHDOG_NOTICE', 5);
+
+/**
+ * Log message severity -- Informational messages.
+ */
+define('WATCHDOG_INFO', 6);
+
+/**
+ * Log message severity -- Debug-level messages.
+ */
+define('WATCHDOG_DEBUG', 7);
+
+/**
+ * @} End of "defgroup logging_severity_levels".
+ */
+
+/**
+ * First bootstrap phase: initialize configuration.
+ */
+define('DRUPAL_BOOTSTRAP_CONFIGURATION', 0);
+
+/**
+ * Second bootstrap phase: try to serve a cached page.
+ */
+define('DRUPAL_BOOTSTRAP_PAGE_CACHE', 1);
+
+/**
+ * Third bootstrap phase: initialize database layer.
+ */
+define('DRUPAL_BOOTSTRAP_DATABASE', 2);
+
+/**
+ * Fourth bootstrap phase: initialize the variable system.
+ */
+define('DRUPAL_BOOTSTRAP_VARIABLES', 3);
+
+/**
+ * Fifth bootstrap phase: initialize session handling.
+ */
+define('DRUPAL_BOOTSTRAP_SESSION', 4);
+
+/**
+ * Sixth bootstrap phase: set up the page header.
+ */
+define('DRUPAL_BOOTSTRAP_PAGE_HEADER', 5);
+
+/**
+ * Seventh bootstrap phase: find out language of the page.
+ */
+define('DRUPAL_BOOTSTRAP_LANGUAGE', 6);
+
+/**
+ * Final bootstrap phase: Drupal is fully loaded; validate and fix input data.
+ */
+define('DRUPAL_BOOTSTRAP_FULL', 7);
+
+/**
+ * Role ID for anonymous users; should match what's in the "role" table.
+ */
+define('DRUPAL_ANONYMOUS_RID', 1);
+
+/**
+ * Role ID for authenticated users; should match what's in the "role" table.
+ */
+define('DRUPAL_AUTHENTICATED_RID', 2);
+
+/**
+ * The number of bytes in a kilobyte.
+ *
+ * For more information, visit http://en.wikipedia.org/wiki/Kilobyte.
+ */
+define('DRUPAL_KILOBYTE', 1024);
+
+/**
+ * The language code used when no language is explicitly assigned.
+ *
+ * Defined by ISO639-2 for "Undetermined".
+ */
+define('LANGUAGE_NONE', 'und');
+
+/**
+ * The type of language used to define the content language.
+ */
+define('LANGUAGE_TYPE_CONTENT', 'language_content');
+
+/**
+ * The type of language used to select the user interface.
+ */
+define('LANGUAGE_TYPE_INTERFACE', 'language');
+
+/**
+ * The type of language used for URLs.
+ */
+define('LANGUAGE_TYPE_URL', 'language_url');
+
+/**
+ * Language written left to right. Possible value of $language->direction.
+ */
+define('LANGUAGE_LTR', 0);
+
+/**
+ * Language written right to left. Possible value of $language->direction.
+ */
+define('LANGUAGE_RTL', 1);
+
+/**
+ * Time of the current request in seconds elapsed since the Unix Epoch.
+ *
+ * This differs from $_SERVER['REQUEST_TIME'], which is stored as a float
+ * since PHP 5.4.0. Float timestamps confuse most PHP functions
+ * (including date_create()).
+ *
+ * @see http://php.net/manual/reserved.variables.server.php
+ * @see http://php.net/manual/function.time.php
+ */
+define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
+
+/**
+ * Flag for drupal_set_title(); text is not sanitized, so run check_plain().
+ */
+define('CHECK_PLAIN', 0);
+
+/**
+ * Flag for drupal_set_title(); text has already been sanitized.
+ */
+define('PASS_THROUGH', -1);
+
+/**
+ * Signals that the registry lookup cache should be reset.
+ */
+define('REGISTRY_RESET_LOOKUP_CACHE', 1);
+
+/**
+ * Signals that the registry lookup cache should be written to storage.
+ */
+define('REGISTRY_WRITE_LOOKUP_CACHE', 2);
+
+/**
+ * Regular expression to match PHP function names.
+ *
+ * @see http://php.net/manual/en/language.functions.php
+ */
+define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*');
+
+/**
+ * Provides a caching wrapper to be used in place of large array structures.
+ *
+ * This class should be extended by systems that need to cache large amounts
+ * of data and have it represented as an array to calling functions. These
+ * arrays can become very large, so ArrayAccess is used to allow different
+ * strategies to be used for caching internally (lazy loading, building caches
+ * over time etc.). This can dramatically reduce the amount of data that needs
+ * to be loaded from cache backends on each request, and memory usage from
+ * static caches of that same data.
+ *
+ * Note that array_* functions do not work with ArrayAccess. Systems using
+ * DrupalCacheArray should use this only internally. If providing API functions
+ * that return the full array, this can be cached separately or returned
+ * directly. However since DrupalCacheArray holds partial content by design, it
+ * should be a normal PHP array or otherwise contain the full structure.
+ *
+ * Note also that due to limitations in PHP prior to 5.3.4, it is impossible to
+ * write directly to the contents of nested arrays contained in this object.
+ * Only writes to the top-level array elements are possible. So if you
+ * previously had set $object['foo'] = array(1, 2, 'bar' => 'baz'), but later
+ * want to change the value of 'bar' from 'baz' to 'foobar', you cannot do so
+ * a targeted write like $object['foo']['bar'] = 'foobar'. Instead, you must
+ * overwrite the entire top-level 'foo' array with the entire set of new
+ * values: $object['foo'] = array(1, 2, 'bar' => 'foobar'). Due to this same
+ * limitation, attempts to create references to any contained data, nested or
+ * otherwise, will fail silently. So $var = &$object['foo'] will not throw an
+ * error, and $var will be populated with the contents of $object['foo'], but
+ * that data will be passed by value, not reference. For more information on
+ * the PHP limitation, see the note in the official PHP documentation at·
+ * http://php.net/manual/en/arrayaccess.offsetget.php on
+ * ArrayAccess::offsetGet().
+ *
+ * By default, the class accounts for caches where calling functions might
+ * request keys in the array that won't exist even after a cache rebuild. This
+ * prevents situations where a cache rebuild would be triggered over and over
+ * due to a 'missing' item. These cases are stored internally as a value of
+ * NULL. This means that the offsetGet() and offsetExists() methods
+ * must be overridden if caching an array where the top level values can
+ * legitimately be NULL, and where $object->offsetExists() needs to correctly
+ * return (equivalent to array_key_exists() vs. isset()). This should not
+ * be necessary in the majority of cases.
+ *
+ * Classes extending this class must override at least the
+ * resolveCacheMiss() method to have a working implementation.
+ *
+ * offsetSet() is not overridden by this class by default. In practice this
+ * means that assigning an offset via arrayAccess will only apply while the
+ * object is in scope and will not be written back to the persistent cache.
+ * This follows a similar pattern to static vs. persistent caching in
+ * procedural code. Extending classes may wish to alter this behavior, for
+ * example by overriding offsetSet() and adding an automatic call to persist().
+ *
+ * @see SchemaCache
+ */
+abstract class DrupalCacheArray implements ArrayAccess {
+
+  /**
+   * A cid to pass to cache_set() and cache_get().
+   */
+  protected $cid;
+
+  /**
+   * A bin to pass to cache_set() and cache_get().
+   */
+  protected $bin;
+
+  /**
+   * An array of keys to add to the cache at the end of the request.
+   */
+  protected $keysToPersist = array();
+
+  /**
+   * Storage for the data itself.
+   */
+  protected $storage = array();
+
+  /**
+   * Constructs a DrupalCacheArray object.
+   *
+   * @param $cid
+   *   The cid for the array being cached.
+   * @param $bin
+   *   The bin to cache the array.
+   */
+  public function __construct($cid, $bin) {
+    $this->cid = $cid;
+    $this->bin = $bin;
+
+    if ($cached = cache_get($this->cid, $this->bin)) {
+     $this->storage = $cached->data;
+    }
+  }
+
+  /**
+   * Implements ArrayAccess::offsetExists().
+   */
+  public function offsetExists($offset) {
+    return $this->offsetGet($offset) !== NULL;
+  }
+
+  /**
+   * Implements ArrayAccess::offsetGet().
+   */
+  public function offsetGet($offset) {
+    if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) {
+      return $this->storage[$offset];
+    }
+    else {
+      return $this->resolveCacheMiss($offset);
+    }
+  }
+
+  /**
+   * Implements ArrayAccess::offsetSet().
+   */
+  public function offsetSet($offset, $value) {
+    $this->storage[$offset] = $value;
+  }
+
+  /**
+   * Implements ArrayAccess::offsetUnset().
+   */
+  public function offsetUnset($offset) {
+    unset($this->storage[$offset]);
+  }
+
+  /**
+   * Flags an offset value to be written to the persistent cache.
+   *
+   * If a value is assigned to a cache object with offsetSet(), by default it
+   * will not be written to the persistent cache unless it is flagged with this
+   * method. This allows items to be cached for the duration of a request,
+   * without necessarily writing back to the persistent cache at the end.
+   *
+   * @param $offset
+   *   The array offset that was request.
+   * @param $persist
+   *   Optional boolean to specify whether the offset should be persisted or
+   *   not, defaults to TRUE. When called with $persist = FALSE the offset will
+   *   be unflagged so that it will not written at the end of the request.
+   */
+  protected function persist($offset, $persist = TRUE) {
+    $this->keysToPersist[$offset] = $persist;
+  }
+
+  /**
+   * Resolves a cache miss.
+   *
+   * When an offset is not found in the object, this is treated as a cache
+   * miss. This method allows classes implementing the interface to look up
+   * the actual value and allow it to be cached.
+   *
+   * @param $offset
+   *   The offset that was requested.
+   *
+   * @return
+   *   The value of the offset, or NULL if no value was found.
+   */
+  abstract protected function resolveCacheMiss($offset);
+
+  /**
+   * Writes a value to the persistent cache immediately.
+   *
+   * @param $data
+   *   The data to write to the persistent cache.
+   * @param $lock
+   *   Whether to acquire a lock before writing to cache.
+   */
+  protected function set($data, $lock = TRUE) {
+    // Lock cache writes to help avoid stampedes.
+    // To implement locking for cache misses, override __construct().
+    $lock_name = $this->cid . ':' . $this->bin;
+    if (!$lock || lock_acquire($lock_name)) {
+      if ($cached = cache_get($this->cid, $this->bin)) {
+        $data = $cached->data + $data;
+      }
+      cache_set($this->cid, $data, $this->bin);
+      if ($lock) {
+        lock_release($lock_name);
+      }
+    }
+  }
+
+  /**
+   * Destructs the DrupalCacheArray object.
+   */
+  public function __destruct() {
+    $data = array();
+    foreach ($this->keysToPersist as $offset => $persist) {
+      if ($persist) {
+        $data[$offset] = $this->storage[$offset];
+      }
+    }
+    if (!empty($data)) {
+      $this->set($data);
+    }
+  }
+}
+
+/**
+ * Starts the timer with the specified name.
+ *
+ * If you start and stop the same timer multiple times, the measured intervals
+ * will be accumulated.
+ *
+ * @param $name
+ *   The name of the timer.
+ */
+function timer_start($name) {
+  global $timers;
+
+  $timers[$name]['start'] = microtime(TRUE);
+  $timers[$name]['count'] = isset($timers[$name]['count']) ? ++$timers[$name]['count'] : 1;
+}
+
+/**
+ * Reads the current timer value without stopping the timer.
+ *
+ * @param $name
+ *   The name of the timer.
+ *
+ * @return
+ *   The current timer value in ms.
+ */
+function timer_read($name) {
+  global $timers;
+
+  if (isset($timers[$name]['start'])) {
+    $stop = microtime(TRUE);
+    $diff = round(($stop - $timers[$name]['start']) * 1000, 2);
+
+    if (isset($timers[$name]['time'])) {
+      $diff += $timers[$name]['time'];
+    }
+    return $diff;
+  }
+  return $timers[$name]['time'];
+}
+
+/**
+ * Stops the timer with the specified name.
+ *
+ * @param $name
+ *   The name of the timer.
+ *
+ * @return
+ *   A timer array. The array contains the number of times the timer has been
+ *   started and stopped (count) and the accumulated timer value in ms (time).
+ */
+function timer_stop($name) {
+  global $timers;
+
+  if (isset($timers[$name]['start'])) {
+    $stop = microtime(TRUE);
+    $diff = round(($stop - $timers[$name]['start']) * 1000, 2);
+    if (isset($timers[$name]['time'])) {
+      $timers[$name]['time'] += $diff;
+    }
+    else {
+      $timers[$name]['time'] = $diff;
+    }
+    unset($timers[$name]['start']);
+  }
+
+  return $timers[$name];
+}
+
+/**
+ * Returns the appropriate configuration directory.
+ *
+ * Returns the configuration path based on the site's hostname, port, and
+ * pathname. Uses find_conf_path() to find the current configuration directory.
+ * See default.settings.php for examples on how the URL is converted to a
+ * directory.
+ *
+ * @param bool $require_settings
+ *   Only configuration directories with an existing settings.php file
+ *   will be recognized. Defaults to TRUE. During initial installation,
+ *   this is set to FALSE so that Drupal can detect a matching directory,
+ *   then create a new settings.php file in it.
+ * @param bool $reset
+ *   Force a full search for matching directories even if one had been
+ *   found previously. Defaults to FALSE.
+ *
+ * @return
+ *   The path of the matching directory.
+ *
+ * @see default.settings.php
+ */
+function conf_path($require_settings = TRUE, $reset = FALSE) {
+  $conf = &drupal_static(__FUNCTION__, '');
+
+  if ($conf && !$reset) {
+    return $conf;
+  }
+
+  $confdir = 'sites';
+
+  $sites = array();
+  if (file_exists(DRUPAL_ROOT . '/' . $confdir . '/sites.php')) {
+    // This will overwrite $sites with the desired mappings.
+    include(DRUPAL_ROOT . '/' . $confdir . '/sites.php');
+  }
+
+  $uri = explode('/', $_SERVER['SCRIPT_NAME'] ? $_SERVER['SCRIPT_NAME'] : $_SERVER['SCRIPT_FILENAME']);
+  $server = explode('.', implode('.', array_reverse(explode(':', rtrim($_SERVER['HTTP_HOST'], '.')))));
+  for ($i = count($uri) - 1; $i > 0; $i--) {
+    for ($j = count($server); $j > 0; $j--) {
+      $dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($uri, 0, $i));
+      if (isset($sites[$dir]) && file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $sites[$dir])) {
+        $dir = $sites[$dir];
+      }
+      if (file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $dir . '/settings.php') || (!$require_settings && file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $dir))) {
+        $conf = "$confdir/$dir";
+        return $conf;
+      }
+    }
+  }
+  $conf = "$confdir/default";
+  return $conf;
+}
+
+/**
+ * Sets appropriate server variables needed for command line scripts to work.
+ *
+ * This function can be called by command line scripts before bootstrapping
+ * Drupal, to ensure that the page loads with the desired server parameters.
+ * This is because many parts of Drupal assume that they are running in a web
+ * browser and therefore use information from the global PHP $_SERVER variable
+ * that does not get set when Drupal is run from the command line.
+ *
+ * In many cases, the default way in which this function populates the $_SERVER
+ * variable is sufficient, and it can therefore be called without passing in
+ * any input. However, command line scripts running on a multisite installation
+ * (or on any installation that has settings.php stored somewhere other than
+ * the sites/default folder) need to pass in the URL of the site to allow
+ * Drupal to detect the correct location of the settings.php file. Passing in
+ * the 'url' parameter is also required for functions like request_uri() to
+ * return the expected values.
+ *
+ * Most other parameters do not need to be passed in, but may be necessary in
+ * some cases; for example, if Drupal's ip_address() function needs to return
+ * anything but the standard localhost value ('127.0.0.1'), the command line
+ * script should pass in the desired value via the 'REMOTE_ADDR' key.
+ *
+ * @param $variables
+ *   (optional) An associative array of variables within $_SERVER that should
+ *   be replaced. If the special element 'url' is provided in this array, it
+ *   will be used to populate some of the server defaults; it should be set to
+ *   the URL of the current page request, excluding any $_GET request but
+ *   including the script name (e.g., http://www.example.com/mysite/index.php).
+ *
+ * @see conf_path()
+ * @see request_uri()
+ * @see ip_address()
+ */
+function drupal_override_server_variables($variables = array()) {
+  // Allow the provided URL to override any existing values in $_SERVER.
+  if (isset($variables['url'])) {
+    $url = parse_url($variables['url']);
+    if (isset($url['host'])) {
+      $_SERVER['HTTP_HOST'] = $url['host'];
+    }
+    if (isset($url['path'])) {
+      $_SERVER['SCRIPT_NAME'] = $url['path'];
+    }
+    unset($variables['url']);
+  }
+  // Define default values for $_SERVER keys. These will be used if $_SERVER
+  // does not already define them and no other values are passed in to this
+  // function.
+  $defaults = array(
+    'HTTP_HOST' => 'localhost',
+    'SCRIPT_NAME' => NULL,
+    'REMOTE_ADDR' => '127.0.0.1',
+    'REQUEST_METHOD' => 'GET',
+    'SERVER_NAME' => NULL,
+    'SERVER_SOFTWARE' => NULL,
+    'HTTP_USER_AGENT' => NULL,
+  );
+  // Replace elements of the $_SERVER array, as appropriate.
+  $_SERVER = $variables + $_SERVER + $defaults;
+}
+
+/**
+ * Initializes the PHP environment.
+ */
+function drupal_environment_initialize() {
+  if (!isset($_SERVER['HTTP_REFERER'])) {
+    $_SERVER['HTTP_REFERER'] = '';
+  }
+  if (!isset($_SERVER['SERVER_PROTOCOL']) || ($_SERVER['SERVER_PROTOCOL'] != 'HTTP/1.0' && $_SERVER['SERVER_PROTOCOL'] != 'HTTP/1.1')) {
+    $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.0';
+  }
+
+  if (isset($_SERVER['HTTP_HOST'])) {
+    // As HTTP_HOST is user input, ensure it only contains characters allowed
+    // in hostnames. See RFC 952 (and RFC 2181).
+    // $_SERVER['HTTP_HOST'] is lowercased here per specifications.
+    $_SERVER['HTTP_HOST'] = strtolower($_SERVER['HTTP_HOST']);
+    if (!drupal_valid_http_host($_SERVER['HTTP_HOST'])) {
+      // HTTP_HOST is invalid, e.g. if containing slashes it may be an attack.
+      header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
+      exit;
+    }
+  }
+  else {
+    // Some pre-HTTP/1.1 clients will not send a Host header. Ensure the key is
+    // defined for E_ALL compliance.
+    $_SERVER['HTTP_HOST'] = '';
+  }
+
+  // When clean URLs are enabled, emulate ?q=foo/bar using REQUEST_URI. It is
+  // not possible to append the query string using mod_rewrite without the B
+  // flag (this was added in Apache 2.2.8), because mod_rewrite unescapes the
+  // path before passing it on to PHP. This is a problem when the path contains
+  // e.g. "&" or "%" that have special meanings in URLs and must be encoded.
+  $_GET['q'] = request_path();
+
+  // Enforce E_ALL, but allow users to set levels not part of E_ALL.
+  error_reporting(E_ALL | error_reporting());
+
+  // Override PHP settings required for Drupal to work properly.
+  // sites/default/default.settings.php contains more runtime settings.
+  // The .htaccess file contains settings that cannot be changed at runtime.
+
+  // Don't escape quotes when reading files from the database, disk, etc.
+  ini_set('magic_quotes_runtime', '0');
+  // Use session cookies, not transparent sessions that puts the session id in
+  // the query string.
+  ini_set('session.use_cookies', '1');
+  ini_set('session.use_only_cookies', '1');
+  ini_set('session.use_trans_sid', '0');
+  // Don't send HTTP headers using PHP's session handler.
+  ini_set('session.cache_limiter', 'none');
+  // Use httponly session cookies.
+  ini_set('session.cookie_httponly', '1');
+
+  // Set sane locale settings, to ensure consistent string, dates, times and
+  // numbers handling.
+  setlocale(LC_ALL, 'C');
+}
+
+/**
+ * Validates that a hostname (for example $_SERVER['HTTP_HOST']) is safe.
+ *
+ * @return
+ *  TRUE if only containing valid characters, or FALSE otherwise.
+ */
+function drupal_valid_http_host($host) {
+  return preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host);
+}
+
+/**
+ * Sets the base URL, cookie domain, and session name from configuration.
+ */
+function drupal_settings_initialize() {
+  global $base_url, $base_path, $base_root;
+
+  // Export these settings.php variables to the global namespace.
+  global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url;
+  $conf = array();
+
+  if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
+    include_once DRUPAL_ROOT . '/' . conf_path() . '/settings.php';
+  }
+  $is_https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
+
+  if (isset($base_url)) {
+    // Parse fixed base URL from settings.php.
+    $parts = parse_url($base_url);
+    if (!isset($parts['path'])) {
+      $parts['path'] = '';
+    }
+    $base_path = $parts['path'] . '/';
+    // Build $base_root (everything until first slash after "scheme://").
+    $base_root = substr($base_url, 0, strlen($base_url) - strlen($parts['path']));
+  }
+  else {
+    // Create base URL.
+    $http_protocol = $is_https ? 'https' : 'http';
+    $base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST'];
+
+    $base_url = $base_root;
+
+    // $_SERVER['SCRIPT_NAME'] can, in contrast to $_SERVER['PHP_SELF'], not
+    // be modified by a visitor.
+    if ($dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/')) {
+      $base_path = $dir;
+      $base_url .= $base_path;
+      $base_path .= '/';
+    }
+    else {
+      $base_path = '/';
+    }
+  }
+  $base_secure_url = str_replace('http://', 'https://', $base_url);
+  $base_insecure_url = str_replace('https://', 'http://', $base_url);
+
+  if ($cookie_domain) {
+    // If the user specifies the cookie domain, also use it for session name.
+    $session_name = $cookie_domain;
+  }
+  else {
+    // Otherwise use $base_url as session name, without the protocol
+    // to use the same session identifiers across HTTP and HTTPS.
+    list( , $session_name) = explode('://', $base_url, 2);
+    // HTTP_HOST can be modified by a visitor, but we already sanitized it
+    // in drupal_settings_initialize().
+    if (!empty($_SERVER['HTTP_HOST'])) {
+      $cookie_domain = $_SERVER['HTTP_HOST'];
+      // Strip leading periods, www., and port numbers from cookie domain.
+      $cookie_domain = ltrim($cookie_domain, '.');
+      if (strpos($cookie_domain, 'www.') === 0) {
+        $cookie_domain = substr($cookie_domain, 4);
+      }
+      $cookie_domain = explode(':', $cookie_domain);
+      $cookie_domain = '.' . $cookie_domain[0];
+    }
+  }
+  // Per RFC 2109, cookie domains must contain at least one dot other than the
+  // first. For hosts such as 'localhost' or IP Addresses we don't set a cookie domain.
+  if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
+    ini_set('session.cookie_domain', $cookie_domain);
+  }
+  // To prevent session cookies from being hijacked, a user can configure the
+  // SSL version of their website to only transfer session cookies via SSL by
+  // using PHP's session.cookie_secure setting. The browser will then use two
+  // separate session cookies for the HTTPS and HTTP versions of the site. So we
+  // must use different session identifiers for HTTPS and HTTP to prevent a
+  // cookie collision.
+  if ($is_https) {
+    ini_set('session.cookie_secure', TRUE);
+  }
+  $prefix = ini_get('session.cookie_secure') ? 'SSESS' : 'SESS';
+  session_name($prefix . substr(hash('sha256', $session_name), 0, 32));
+}
+
+/**
+ * Returns and optionally sets the filename for a system resource.
+ *
+ * The filename, whether provided, cached, or retrieved from the database, is
+ * only returned if the file exists.
+ *
+ * This function plays a key role in allowing Drupal's resources (modules
+ * and themes) to be located in different places depending on a site's
+ * configuration. For example, a module 'foo' may legally be be located
+ * in any of these three places:
+ *
+ * modules/foo/foo.module
+ * sites/all/modules/foo/foo.module
+ * sites/example.com/modules/foo/foo.module
+ *
+ * Calling drupal_get_filename('module', 'foo') will give you one of
+ * the above, depending on where the module is located.
+ *
+ * @param $type
+ *   The type of the item (i.e. theme, theme_engine, module, profile).
+ * @param $name
+ *   The name of the item for which the filename is requested.
+ * @param $filename
+ *   The filename of the item if it is to be set explicitly rather
+ *   than by consulting the database.
+ *
+ * @return
+ *   The filename of the requested item or NULL if the item is not found.
+ */
+function drupal_get_filename($type, $name, $filename = NULL) {
+  // The location of files will not change during the request, so do not use
+  // drupal_static().
+  static $files = array(), $dirs = array();
+
+  // Profiles are a special case: they have a fixed location and naming.
+  if ($type == 'profile') {
+    $profile_filename = "profiles/$name/$name.profile";
+    $files[$type][$name] = file_exists($profile_filename) ? $profile_filename : FALSE;
+  }
+  if (!isset($files[$type])) {
+    $files[$type] = array();
+  }
+
+  if (!empty($filename) && file_exists($filename)) {
+    $files[$type][$name] = $filename;
+  }
+  elseif (isset($files[$type][$name])) {
+    // nothing
+  }
+  // Verify that we have an active database connection, before querying
+  // the database. This is required because this function is called both
+  // before we have a database connection (i.e. during installation) and
+  // when a database connection fails.
+  else {
+    try {
+      if (function_exists('db_query')) {
+        $file = db_query("SELECT filename FROM {system} WHERE name = :name AND type = :type", array(':name' => $name, ':type' => $type))->fetchField();
+        if (file_exists(DRUPAL_ROOT . '/' . $file)) {
+          $files[$type][$name] = $file;
+        }
+      }
+    }
+    catch (Exception $e) {
+      // The database table may not exist because Drupal is not yet installed,
+      // or the database might be down. We have a fallback for this case so we
+      // hide the error completely.
+    }
+    // Fallback to searching the filesystem if the database could not find the
+    // file or the file returned by the database is not found.
+    if (!isset($files[$type][$name])) {
+      // We have a consistent directory naming: modules, themes...
+      $dir = $type . 's';
+      if ($type == 'theme_engine') {
+        $dir = 'themes/engines';
+        $extension = 'engine';
+      }
+      elseif ($type == 'theme') {
+        $extension = 'info';
+      }
+      else {
+        $extension = $type;
+      }
+
+      if (!isset($dirs[$dir][$extension])) {
+        $dirs[$dir][$extension] = TRUE;
+        if (!function_exists('drupal_system_listing')) {
+          require_once DRUPAL_ROOT . '/includes/common.inc';
+        }
+        // Scan the appropriate directories for all files with the requested
+        // extension, not just the file we are currently looking for. This
+        // prevents unnecessary scans from being repeated when this function is
+        // called more than once in the same page request.
+        $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0);
+        foreach ($matches as $matched_name => $file) {
+          $files[$type][$matched_name] = $file->uri;
+        }
+      }
+    }
+  }
+
+  if (isset($files[$type][$name])) {
+    return $files[$type][$name];
+  }
+}
+
+/**
+ * Loads the persistent variable table.
+ *
+ * The variable table is composed of values that have been saved in the table
+ * with variable_set() as well as those explicitly specified in the
+ * configuration file.
+ */
+function variable_initialize($conf = array()) {
+  // NOTE: caching the variables improves performance by 20% when serving
+  // cached pages.
+  if ($cached = cache_get('variables', 'cache_bootstrap')) {
+    $variables = $cached->data;
+  }
+  else {
+    // Cache miss. Avoid a stampede.
+    $name = 'variable_init';
+    if (!lock_acquire($name, 1)) {
+      // Another request is building the variable cache.
+      // Wait, then re-run this function.
+      lock_wait($name);
+      return variable_initialize($conf);
+    }
+    else {
+      // Proceed with variable rebuild.
+      $variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed());
+      cache_set('variables', $variables, 'cache_bootstrap');
+      lock_release($name);
+    }
+  }
+
+  foreach ($conf as $name => $value) {
+    $variables[$name] = $value;
+  }
+
+  return $variables;
+}
+
+/**
+ * Returns a persistent variable.
+ *
+ * Case-sensitivity of the variable_* functions depends on the database
+ * collation used. To avoid problems, always use lower case for persistent
+ * variable names.
+ *
+ * @param $name
+ *   The name of the variable to return.
+ * @param $default
+ *   The default value to use if this variable has never been set.
+ *
+ * @return
+ *   The value of the variable. Unserialization is taken care of as necessary.
+ *
+ * @see variable_del()
+ * @see variable_set()
+ */
+function variable_get($name, $default = NULL) {
+  global $conf;
+
+  return isset($conf[$name]) ? $conf[$name] : $default;
+}
+
+/**
+ * Sets a persistent variable.
+ *
+ * Case-sensitivity of the variable_* functions depends on the database
+ * collation used. To avoid problems, always use lower case for persistent
+ * variable names.
+ *
+ * @param $name
+ *   The name of the variable to set.
+ * @param $value
+ *   The value to set. This can be any PHP data type; these functions take care
+ *   of serialization as necessary.
+ *
+ * @see variable_del()
+ * @see variable_get()
+ */
+function variable_set($name, $value) {
+  global $conf;
+
+  db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute();
+
+  cache_clear_all('variables', 'cache_bootstrap');
+
+  $conf[$name] = $value;
+}
+
+/**
+ * Unsets a persistent variable.
+ *
+ * Case-sensitivity of the variable_* functions depends on the database
+ * collation used. To avoid problems, always use lower case for persistent
+ * variable names.
+ *
+ * @param $name
+ *   The name of the variable to undefine.
+ *
+ * @see variable_get()
+ * @see variable_set()
+ */
+function variable_del($name) {
+  global $conf;
+
+  db_delete('variable')
+    ->condition('name', $name)
+    ->execute();
+  cache_clear_all('variables', 'cache_bootstrap');
+
+  unset($conf[$name]);
+}
+
+/**
+ * Retrieves the current page from the cache.
+ *
+ * Note: we do not serve cached pages to authenticated users, or to anonymous
+ * users when $_SESSION is non-empty. $_SESSION may contain status messages
+ * from a form submission, the contents of a shopping cart, or other user-
+ * specific content that should not be cached and displayed to other users.
+ *
+ * @param $check_only
+ *   (optional) Set to TRUE to only return whether a previous call found a
+ *   cache entry.
+ *
+ * @return
+ *   The cache object, if the page was found in the cache, NULL otherwise.
+ */
+function drupal_page_get_cache($check_only = FALSE) {
+  global $base_root;
+  static $cache_hit = FALSE;
+
+  if ($check_only) {
+    return $cache_hit;
+  }
+
+  if (drupal_page_is_cacheable()) {
+    $cache = cache_get($base_root . request_uri(), 'cache_page');
+    if ($cache !== FALSE) {
+      $cache_hit = TRUE;
+    }
+    return $cache;
+  }
+}
+
+/**
+ * Determines the cacheability of the current page.
+ *
+ * @param $allow_caching
+ *   Set to FALSE if you want to prevent this page to get cached.
+ *
+ * @return
+ *   TRUE if the current page can be cached, FALSE otherwise.
+ */
+function drupal_page_is_cacheable($allow_caching = NULL) {
+  $allow_caching_static = &drupal_static(__FUNCTION__, TRUE);
+  if (isset($allow_caching)) {
+    $allow_caching_static = $allow_caching;
+  }
+
+  return $allow_caching_static && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
+    && !drupal_is_cli();
+}
+
+/**
+ * Invokes a bootstrap hook in all bootstrap modules that implement it.
+ *
+ * @param $hook
+ *   The name of the bootstrap hook to invoke.
+ *
+ * @see bootstrap_hooks()
+ */
+function bootstrap_invoke_all($hook) {
+  // Bootstrap modules should have been loaded when this function is called, so
+  // we don't need to tell module_list() to reset its internal list (and we
+  // therefore leave the first parameter at its default value of FALSE). We
+  // still pass in TRUE for the second parameter, though; in case this is the
+  // first time during the bootstrap that module_list() is called, we want to
+  // make sure that its internal cache is primed with the bootstrap modules
+  // only.
+  foreach (module_list(FALSE, TRUE) as $module) {
+    drupal_load('module', $module);
+    module_invoke($module, $hook);
+  }
+}
+
+/**
+ * Includes a file with the provided type and name.
+ *
+ * This prevents including a theme, engine, module, etc., more than once.
+ *
+ * @param $type
+ *   The type of item to load (i.e. theme, theme_engine, module).
+ * @param $name
+ *   The name of the item to load.
+ *
+ * @return
+ *   TRUE if the item is loaded or has already been loaded.
+ */
+function drupal_load($type, $name) {
+  // Once a file is included this can't be reversed during a request so do not
+  // use drupal_static() here.
+  static $files = array();
+
+  if (isset($files[$type][$name])) {
+    return TRUE;
+  }
+
+  $filename = drupal_get_filename($type, $name);
+
+  if ($filename) {
+    include_once DRUPAL_ROOT . '/' . $filename;
+    $files[$type][$name] = TRUE;
+
+    return TRUE;
+  }
+
+  return FALSE;
+}
+
+/**
+ * Sets an HTTP response header for the current page.
+ *
+ * Note: When sending a Content-Type header, always include a 'charset' type,
+ * too. This is necessary to avoid security bugs (e.g. UTF-7 XSS).
+ *
+ * @param $name
+ *   The HTTP header name, or the special 'Status' header name.
+ * @param $value
+ *   The HTTP header value; if equal to FALSE, the specified header is unset.
+ *   If $name is 'Status', this is expected to be a status code followed by a
+ *   reason phrase, e.g. "404 Not Found".
+ * @param $append
+ *   Whether to append the value to an existing header or to replace it.
+ */
+function drupal_add_http_header($name, $value, $append = FALSE) {
+  // The headers as name/value pairs.
+  $headers = &drupal_static('drupal_http_headers', array());
+
+  $name_lower = strtolower($name);
+  _drupal_set_preferred_header_name($name);
+
+  if ($value === FALSE) {
+    $headers[$name_lower] = FALSE;
+  }
+  elseif (isset($headers[$name_lower]) && $append) {
+    // Multiple headers with identical names may be combined using comma (RFC
+    // 2616, section 4.2).
+    $headers[$name_lower] .= ',' . $value;
+  }
+  else {
+    $headers[$name_lower] = $value;
+  }
+  drupal_send_headers(array($name => $headers[$name_lower]), TRUE);
+}
+
+/**
+ * Gets the HTTP response headers for the current page.
+ *
+ * @param $name
+ *   An HTTP header name. If omitted, all headers are returned as name/value
+ *   pairs. If an array value is FALSE, the header has been unset.
+ *
+ * @return
+ *   A string containing the header value, or FALSE if the header has been set,
+ *   or NULL if the header has not been set.
+ */
+function drupal_get_http_header($name = NULL) {
+  $headers = &drupal_static('drupal_http_headers', array());
+  if (isset($name)) {
+    $name = strtolower($name);
+    return isset($headers[$name]) ? $headers[$name] : NULL;
+  }
+  else {
+    return $headers;
+  }
+}
+
+/**
+ * Sets the preferred name for the HTTP header.
+ *
+ * Header names are case-insensitive, but for maximum compatibility they should
+ * follow "common form" (see RFC 2617, section 4.2).
+ */
+function _drupal_set_preferred_header_name($name = NULL) {
+  static $header_names = array();
+
+  if (!isset($name)) {
+    return $header_names;
+  }
+  $header_names[strtolower($name)] = $name;
+}
+
+/**
+ * Sends the HTTP response headers that were previously set, adding defaults.
+ *
+ * Headers are set in drupal_add_http_header(). Default headers are not set
+ * if they have been replaced or unset using drupal_add_http_header().
+ *
+ * @param array $default_headers
+ *   (optional) An array of headers as name/value pairs.
+ * @param bool $only_default
+ *   (optional) If TRUE and headers have already been sent, send only the
+ *   specified headers.
+ */
+function drupal_send_headers($default_headers = array(), $only_default = FALSE) {
+  $headers_sent = &drupal_static(__FUNCTION__, FALSE);
+  $headers = drupal_get_http_header();
+  if ($only_default && $headers_sent) {
+    $headers = array();
+  }
+  $headers_sent = TRUE;
+
+  $header_names = _drupal_set_preferred_header_name();
+  foreach ($default_headers as $name => $value) {
+    $name_lower = strtolower($name);
+    if (!isset($headers[$name_lower])) {
+      $headers[$name_lower] = $value;
+      $header_names[$name_lower] = $name;
+    }
+  }
+  foreach ($headers as $name_lower => $value) {
+    if ($name_lower == 'status') {
+      header($_SERVER['SERVER_PROTOCOL'] . ' ' . $value);
+    }
+    // Skip headers that have been unset.
+    elseif ($value !== FALSE) {
+      header($header_names[$name_lower] . ': ' . $value);
+    }
+  }
+}
+
+/**
+ * Sets HTTP headers in preparation for a page response.
+ *
+ * Authenticated users are always given a 'no-cache' header, and will fetch a
+ * fresh page on every request. This prevents authenticated users from seeing
+ * locally cached pages.
+ *
+ * Also give each page a unique ETag. This will force clients to include both
+ * an If-Modified-Since header and an If-None-Match header when doing
+ * conditional requests for the page (required by RFC 2616, section 13.3.4),
+ * making the validation more robust. This is a workaround for a bug in Mozilla
+ * Firefox that is triggered when Drupal's caching is enabled and the user
+ * accesses Drupal via an HTTP proxy (see
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated
+ * user requests a page, and then logs out and requests the same page again,
+ * Firefox may send a conditional request based on the page that was cached
+ * locally when the user was logged in. If this page did not have an ETag
+ * header, the request only contains an If-Modified-Since header. The date will
+ * be recent, because with authenticated users the Last-Modified header always
+ * refers to the time of the request. If the user accesses Drupal via a proxy
+ * server, and the proxy already has a cached copy of the anonymous page with an
+ * older Last-Modified date, the proxy may respond with 304 Not Modified, making
+ * the client think that the anonymous and authenticated pageviews are
+ * identical.
+ *
+ * @see drupal_page_set_cache()
+ */
+function drupal_page_header() {
+  $headers_sent = &drupal_static(__FUNCTION__, FALSE);
+  if ($headers_sent) {
+    return TRUE;
+  }
+  $headers_sent = TRUE;
+
+  $default_headers = array(
+    'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT',
+    'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME),
+    'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0',
+    'ETag' => '"' . REQUEST_TIME . '"',
+  );
+  drupal_send_headers($default_headers);
+}
+
+/**
+ * Sets HTTP headers in preparation for a cached page response.
+ *
+ * The headers allow as much as possible in proxies and browsers without any
+ * particular knowledge about the pages. Modules can override these headers
+ * using drupal_add_http_header().
+ *
+ * If the request is conditional (using If-Modified-Since and If-None-Match),
+ * and the conditions match those currently in the cache, a 304 Not Modified
+ * response is sent.
+ */
+function drupal_serve_page_from_cache(stdClass $cache) {
+  // Negotiate whether to use compression.
+  $page_compression = variable_get('page_compression', TRUE) && extension_loaded('zlib');
+  $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE;
+
+  // Get headers set in hook_boot(). Keys are lower-case.
+  $hook_boot_headers = drupal_get_http_header();
+
+  // Headers generated in this function, that may be replaced or unset using
+  // drupal_add_http_headers(). Keys are mixed-case.
+  $default_headers = array();
+
+  foreach ($cache->data['headers'] as $name => $value) {
+    // In the case of a 304 response, certain headers must be sent, and the
+    // remaining may not (see RFC 2616, section 10.3.5). Do not override
+    // headers set in hook_boot().
+    $name_lower = strtolower($name);
+    if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($hook_boot_headers[$name_lower])) {
+      drupal_add_http_header($name, $value);
+      unset($cache->data['headers'][$name]);
+    }
+  }
+
+  // If the client sent a session cookie, a cached copy will only be served
+  // to that one particular client due to Vary: Cookie. Thus, do not set
+  // max-age > 0, allowing the page to be cached by external proxies, when a
+  // session cookie is present unless the Vary header has been replaced or
+  // unset in hook_boot().
+  $max_age = !isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary']) ? variable_get('page_cache_maximum_age', 0) : 0;
+  $default_headers['Cache-Control'] = 'public, max-age=' . $max_age;
+
+  // Entity tag should change if the output changes.
+  $etag = '"' . $cache->created . '-' . intval($return_compressed) . '"';
+  header('Etag: ' . $etag);
+
+  // See if the client has provided the required HTTP headers.
+  $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE;
+  $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE;
+
+  if ($if_modified_since && $if_none_match
+      && $if_none_match == $etag // etag must match
+      && $if_modified_since == $cache->created) {  // if-modified-since must match
+    header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
+    drupal_send_headers($default_headers);
+    return;
+  }
+
+  // Send the remaining headers.
+  foreach ($cache->data['headers'] as $name => $value) {
+    drupal_add_http_header($name, $value);
+  }
+
+  $default_headers['Last-Modified'] = gmdate(DATE_RFC1123, $cache->created);
+
+  // HTTP/1.0 proxies does not support the Vary header, so prevent any caching
+  // by sending an Expires date in the past. HTTP/1.1 clients ignores the
+  // Expires header if a Cache-Control: max-age= directive is specified (see RFC
+  // 2616, section 14.9.3).
+  $default_headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT';
+
+  drupal_send_headers($default_headers);
+
+  // Allow HTTP proxies to cache pages for anonymous users without a session
+  // cookie. The Vary header is used to indicates the set of request-header
+  // fields that fully determines whether a cache is permitted to use the
+  // response to reply to a subsequent request for a given URL without
+  // revalidation. If a Vary header has been set in hook_boot(), it is assumed
+  // that the module knows how to cache the page.
+  if (!isset($hook_boot_headers['vary']) && !variable_get('omit_vary_cookie')) {
+    header('Vary: Cookie');
+  }
+
+  if ($page_compression) {
+    header('Vary: Accept-Encoding', FALSE);
+    // If page_compression is enabled, the cache contains gzipped data.
+    if ($return_compressed) {
+      // $cache->data['body'] is already gzip'ed, so make sure
+      // zlib.output_compression does not compress it once more.
+      ini_set('zlib.output_compression', '0');
+      header('Content-Encoding: gzip');
+    }
+    else {
+      // The client does not support compression, so unzip the data in the
+      // cache. Strip the gzip header and run uncompress.
+      $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
+    }
+  }
+
+  // Print the page.
+  print $cache->data['body'];
+}
+
+/**
+ * Defines the critical hooks that force modules to always be loaded.
+ */
+function bootstrap_hooks() {
+  return array('boot', 'exit', 'watchdog', 'language_init');
+}
+
+/**
+ * Unserializes and appends elements from a serialized string.
+ *
+ * @param $obj
+ *   The object to which the elements are appended.
+ * @param $field
+ *   The attribute of $obj whose value should be unserialized.
+ */
+function drupal_unpack($obj, $field = 'data') {
+  if ($obj->$field && $data = unserialize($obj->$field)) {
+    foreach ($data as $key => $value) {
+      if (!empty($key) && !isset($obj->$key)) {
+        $obj->$key = $value;
+      }
+    }
+  }
+  return $obj;
+}
+
+/**
+ * Translates a string to the current language or to a given language.
+ *
+ * The t() function serves two purposes. First, at run-time it translates
+ * user-visible text into the appropriate language. Second, various mechanisms
+ * that figure out what text needs to be translated work off t() -- the text
+ * inside t() calls is added to the database of strings to be translated.
+ * These strings are expected to be in English, so the first argument should
+ * always be in English. To enable a fully-translatable site, it is important
+ * that all human-readable text that will be displayed on the site or sent to
+ * a user is passed through the t() function, or a related function. See the
+ * @link http://drupal.org/node/322729 Localization API @endlink pages for
+ * more information, including recommendations on how to break up or not
+ * break up strings for translation.
+ *
+ * You should never use t() to translate variables, such as calling
+ * @code t($text); @endcode, unless the text that the variable holds has been
+ * passed through t() elsewhere (e.g., $text is one of several translated
+ * literal strings in an array). It is especially important never to call
+ * @code t($user_text); @endcode, where $user_text is some text that a user
+ * entered - doing that can lead to cross-site scripting and other security
+ * problems. However, you can use variable substitution in your string, to put
+ * variable text such as user names or link URLs into translated text. Variable
+ * substitution looks like this:
+ * @code
+ * $text = t("@name's blog", array('@name' => format_username($account)));
+ * @endcode
+ * Basically, you can put variables like @name into your string, and t() will
+ * substitute their sanitized values at translation time. (See the
+ * Localization API pages referenced above and the documentation of
+ * format_string() for details about how to define variables in your string.)
+ * Translators can then rearrange the string as necessary for the language
+ * (e.g., in Spanish, it might be "blog de @name").
+ *
+ * During the Drupal installation phase, some resources used by t() wil not be
+ * available to code that needs localization. See st() and get_t() for
+ * alternatives.
+ *
+ * @param $string
+ *   A string containing the English string to translate.
+ * @param $args
+ *   An associative array of replacements to make after translation. Based
+ *   on the first character of the key, the value is escaped and/or themed.
+ *   See format_string() for details.
+ * @param $options
+ *   An associative array of additional options, with the following elements:
+ *   - 'langcode' (defaults to the current language): The language code to
+ *     translate to a language other than what is used to display the page.
+ *   - 'context' (defaults to the empty context): The context the source string
+ *     belongs to.
+ *
+ * @return
+ *   The translated string.
+ *
+ * @see st()
+ * @see get_t()
+ * @see format_string()
+ * @ingroup sanitization
+ */
+function t($string, array $args = array(), array $options = array()) {
+  global $language;
+  static $custom_strings;
+
+  // Merge in default.
+  if (empty($options['langcode'])) {
+    $options['langcode'] = isset($language->language) ? $language->language : 'en';
+  }
+  if (empty($options['context'])) {
+    $options['context'] = '';
+  }
+
+  // First, check for an array of customized strings. If present, use the array
+  // *instead of* database lookups. This is a high performance way to provide a
+  // handful of string replacements. See settings.php for examples.
+  // Cache the $custom_strings variable to improve performance.
+  if (!isset($custom_strings[$options['langcode']])) {
+    $custom_strings[$options['langcode']] = variable_get('locale_custom_strings_' . $options['langcode'], array());
+  }
+  // Custom strings work for English too, even if locale module is disabled.
+  if (isset($custom_strings[$options['langcode']][$options['context']][$string])) {
+    $string = $custom_strings[$options['langcode']][$options['context']][$string];
+  }
+  // Translate with locale module if enabled.
+  elseif ($options['langcode'] != 'en' && function_exists('locale')) {
+    $string = locale($string, $options['context'], $options['langcode']);
+  }
+  if (empty($args)) {
+    return $string;
+  }
+  else {
+    return format_string($string, $args);
+  }
+}
+
+/**
+ * Formats a string for HTML display by replacing variable placeholders.
+ *
+ * This function replaces variable placeholders in a string with the requested
+ * values and escapes the values so they can be safely displayed as HTML. It
+ * should be used on any unknown text that is intended to be printed to an HTML
+ * page (especially text that may have come from untrusted users, since in that
+ * case it prevents cross-site scripting and other security problems).
+ *
+ * In most cases, you should use t() rather than calling this function
+ * directly, since it will translate the text (on non-English-only sites) in
+ * addition to formatting it.
+ *
+ * @param $string
+ *   A string containing placeholders.
+ * @param $args
+ *   An associative array of replacements to make. Occurrences in $string of
+ *   any key in $args are replaced with the corresponding value, after optional
+ *   sanitization and formatting. The type of sanitization and formatting
+ *   depends on the first character of the key:
+ *   - @variable: Escaped to HTML using check_plain(). Use this as the default
+ *     choice for anything displayed on a page on the site.
+ *   - %variable: Escaped to HTML and formatted using drupal_placeholder(),
+ *     which makes it display as <em>emphasized</em> text.
+ *   - !variable: Inserted as is, with no sanitization or formatting. Only use
+ *     this for text that has already been prepared for HTML display (for
+ *     example, user-supplied text that has already been run through
+ *     check_plain() previously, or is expected to contain some limited HTML
+ *     tags and has already been run through filter_xss() previously).
+ *
+ * @see t()
+ * @ingroup sanitization
+ */
+function format_string($string, array $args = array()) {
+  // Transform arguments before inserting them.
+  foreach ($args as $key => $value) {
+    switch ($key[0]) {
+      case '@':
+        // Escaped only.
+        $args[$key] = check_plain($value);
+        break;
+
+      case '%':
+      default:
+        // Escaped and placeholder.
+        $args[$key] = drupal_placeholder($value);
+        break;
+
+      case '!':
+        // Pass-through.
+    }
+  }
+  return strtr($string, $args);
+}
+
+/**
+ * Encodes special characters in a plain-text string for display as HTML.
+ *
+ * Also validates strings as UTF-8 to prevent cross site scripting attacks on
+ * Internet Explorer 6.
+ *
+ * @param $text
+ *   The text to be checked or processed.
+ *
+ * @return
+ *   An HTML safe version of $text, or an empty string if $text is not
+ *   valid UTF-8.
+ *
+ * @see drupal_validate_utf8()
+ * @ingroup sanitization
+ */
+function check_plain($text) {
+  return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
+}
+
+/**
+ * Checks whether a string is valid UTF-8.
+ *
+ * All functions designed to filter input should use drupal_validate_utf8
+ * to ensure they operate on valid UTF-8 strings to prevent bypass of the
+ * filter.
+ *
+ * When text containing an invalid UTF-8 lead byte (0xC0 - 0xFF) is presented
+ * as UTF-8 to Internet Explorer 6, the program may misinterpret subsequent
+ * bytes. When these subsequent bytes are HTML control characters such as
+ * quotes or angle brackets, parts of the text that were deemed safe by filters
+ * end up in locations that are potentially unsafe; An onerror attribute that
+ * is outside of a tag, and thus deemed safe by a filter, can be interpreted
+ * by the browser as if it were inside the tag.
+ *
+ * The function does not return FALSE for strings containing character codes
+ * above U+10FFFF, even though these are prohibited by RFC 3629.
+ *
+ * @param $text
+ *   The text to check.
+ *
+ * @return
+ *   TRUE if the text is valid UTF-8, FALSE if not.
+ */
+function drupal_validate_utf8($text) {
+  if (strlen($text) == 0) {
+    return TRUE;
+  }
+  // With the PCRE_UTF8 modifier 'u', preg_match() fails silently on strings
+  // containing invalid UTF-8 byte sequences. It does not reject character
+  // codes above U+10FFFF (represented by 4 or more octets), though.
+  return (preg_match('/^./us', $text) == 1);
+}
+
+/**
+ * Returns the equivalent of Apache's $_SERVER['REQUEST_URI'] variable.
+ *
+ * Because $_SERVER['REQUEST_URI'] is only available on Apache, we generate an
+ * equivalent using other environment variables.
+ */
+function request_uri() {
+  if (isset($_SERVER['REQUEST_URI'])) {
+    $uri = $_SERVER['REQUEST_URI'];
+  }
+  else {
+    if (isset($_SERVER['argv'])) {
+      $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['argv'][0];
+    }
+    elseif (isset($_SERVER['QUERY_STRING'])) {
+      $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING'];
+    }
+    else {
+      $uri = $_SERVER['SCRIPT_NAME'];
+    }
+  }
+  // Prevent multiple slashes to avoid cross site requests via the Form API.
+  $uri = '/' . ltrim($uri, '/');
+
+  return $uri;
+}
+
+/**
+ * Logs an exception.
+ *
+ * This is a wrapper function for watchdog() which automatically decodes an
+ * exception.
+ *
+ * @param $type
+ *   The category to which this message belongs.
+ * @param $exception
+ *   The exception that is going to be logged.
+ * @param $message
+ *   The message to store in the log. If empty, a text that contains all useful
+ *   information about the passed-in exception is used.
+ * @param $variables
+ *   Array of variables to replace in the message on display. Defaults to the
+ *   return value of drupal_decode_exception().
+ * @param $severity
+ *   The severity of the message, as per RFC 3164.
+ * @param $link
+ *   A link to associate with the message.
+ *
+ * @see watchdog()
+ * @see drupal_decode_exception()
+ */
+function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) {
+
+   // Use a default value if $message is not set.
+   if (empty($message)) {
+     // The exception message is run through check_plain() by _drupal_decode_exception().
+     $message = '%type: !message in %function (line %line of %file).';
+   }
+   // $variables must be an array so that we can add the exception information.
+   if (!is_array($variables)) {
+     $variables = array();
+   }
+
+   require_once DRUPAL_ROOT . '/includes/errors.inc';
+   $variables += _drupal_decode_exception($exception);
+   watchdog($type, $message, $variables, $severity, $link);
+}
+
+/**
+ * Logs a system message.
+ *
+ * @param $type
+ *   The category to which this message belongs. Can be any string, but the
+ *   general practice is to use the name of the module calling watchdog().
+ * @param $message
+ *   The message to store in the log. Keep $message translatable
+ *   by not concatenating dynamic values into it! Variables in the
+ *   message should be added by using placeholder strings alongside
+ *   the variables argument to declare the value of the placeholders.
+ *   See t() for documentation on how $message and $variables interact.
+ * @param $variables
+ *   Array of variables to replace in the message on display or
+ *   NULL if message is already translated or not possible to
+ *   translate.
+ * @param $severity
+ *   The severity of the message; one of the following values as defined in
+ *   @link http://www.faqs.org/rfcs/rfc3164.html RFC 3164: @endlink
+ *   - WATCHDOG_EMERGENCY: Emergency, system is unusable.
+ *   - WATCHDOG_ALERT: Alert, action must be taken immediately.
+ *   - WATCHDOG_CRITICAL: Critical conditions.
+ *   - WATCHDOG_ERROR: Error conditions.
+ *   - WATCHDOG_WARNING: Warning conditions.
+ *   - WATCHDOG_NOTICE: (default) Normal but significant conditions.
+ *   - WATCHDOG_INFO: Informational messages.
+ *   - WATCHDOG_DEBUG: Debug-level messages.
+ * @param $link
+ *   A link to associate with the message.
+ *
+ * @see watchdog_severity_levels()
+ * @see hook_watchdog()
+ */
+function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) {
+  global $user, $base_root;
+
+  static $in_error_state = FALSE;
+
+  // It is possible that the error handling will itself trigger an error. In that case, we could
+  // end up in an infinite loop. To avoid that, we implement a simple static semaphore.
+  if (!$in_error_state && function_exists('module_implements')) {
+    $in_error_state = TRUE;
+
+    // The user object may not exist in all conditions, so 0 is substituted if needed.
+    $user_uid = isset($user->uid) ? $user->uid : 0;
+
+    // Prepare the fields to be logged
+    $log_entry = array(
+      'type'        => $type,
+      'message'     => $message,
+      'variables'   => $variables,
+      'severity'    => $severity,
+      'link'        => $link,
+      'user'        => $user,
+      'uid'         => $user_uid,
+      'request_uri' => $base_root . request_uri(),
+      'referer'     => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
+      'ip'          => ip_address(),
+      // Request time isn't accurate for long processes, use time() instead.
+      'timestamp'   => time(),
+    );
+
+    // Call the logging hooks to log/process the message
+    foreach (module_implements('watchdog') as $module) {
+      module_invoke($module, 'watchdog', $log_entry);
+    }
+
+    // It is critical that the semaphore is only cleared here, in the parent
+    // watchdog() call (not outside the loop), to prevent recursive execution.
+    $in_error_state = FALSE;
+  }
+}
+
+/**
+ * Sets a message to display to the user.
+ *
+ * Messages are stored in a session variable and displayed in page.tpl.php via
+ * the $messages theme variable.
+ *
+ * Example usage:
+ * @code
+ * drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
+ * @endcode
+ *
+ * @param string $message
+ *   (optional) The translated message to be displayed to the user. For
+ *   consistency with other messages, it should begin with a capital letter and
+ *   end with a period.
+ * @param string $type
+ *   (optional) The message's type. Defaults to 'status'. These values are
+ *   supported:
+ *   - 'status'
+ *   - 'warning'
+ *   - 'error'
+ * @param bool $repeat
+ *   (optional) If this is FALSE and the message is already set, then the
+ *   message won't be repeated. Defaults to TRUE.
+ *
+ * @return array|null
+ *   A multidimensional array with keys corresponding to the set message types.
+ *   The indexed array values of each contain the set messages for that type.
+ *   Or, if there are no messages set, the function returns NULL.
+ *
+ * @see drupal_get_messages()
+ * @see theme_status_messages()
+ */
+function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) {
+  if ($message) {
+    if (!isset($_SESSION['messages'][$type])) {
+      $_SESSION['messages'][$type] = array();
+    }
+
+    if ($repeat || !in_array($message, $_SESSION['messages'][$type])) {
+      $_SESSION['messages'][$type][] = $message;
+    }
+
+    // Mark this page as being uncacheable.
+    drupal_page_is_cacheable(FALSE);
+  }
+
+  // Messages not set when DB connection fails.
+  return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL;
+}
+
+/**
+ * Returns all messages that have been set with drupal_set_message().
+ *
+ * @param string $type
+ *   (optional) Limit the messages returned by type. Defaults to NULL, meaning
+ *   all types. These values are supported:
+ *   - NULL
+ *   - 'status'
+ *   - 'warning'
+ *   - 'error'
+ * @param bool $clear_queue
+ *   (optional) If this is TRUE, the queue will be cleared of messages of the
+ *   type specified in the $type parameter. Otherwise the queue will be left
+ *   intact. Defaults to TRUE.
+ *
+ * @return array
+ *   A multidimensional array with keys corresponding to the set message types.
+ *   The indexed array values of each contain the set messages for that type.
+ *   The messages returned are limited to the type specified in the $type
+ *   parameter. If there are no messages of the specified type, an empty array
+ *   is returned.
+ *
+ * @see drupal_set_message()
+ * @see theme_status_messages()
+ */
+function drupal_get_messages($type = NULL, $clear_queue = TRUE) {
+  if ($messages = drupal_set_message()) {
+    if ($type) {
+      if ($clear_queue) {
+        unset($_SESSION['messages'][$type]);
+      }
+      if (isset($messages[$type])) {
+        return array($type => $messages[$type]);
+      }
+    }
+    else {
+      if ($clear_queue) {
+        unset($_SESSION['messages']);
+      }
+      return $messages;
+    }
+  }
+  return array();
+}
+
+/**
+ * Gets the title of the current page.
+ *
+ * The title is displayed on the page and in the title bar.
+ *
+ * @return
+ *   The current page's title.
+ */
+function drupal_get_title() {
+  $title = drupal_set_title();
+
+  // During a bootstrap, menu.inc is not included and thus we cannot provide a title.
+  if (!isset($title) && function_exists('menu_get_active_title')) {
+    $title = check_plain(menu_get_active_title());
+  }
+
+  return $title;
+}
+
+/**
+ * Sets the title of the current page.
+ *
+ * The title is displayed on the page and in the title bar.
+ *
+ * @param $title
+ *   Optional string value to assign to the page title; or if set to NULL
+ *   (default), leaves the current title unchanged.
+ * @param $output
+ *   Optional flag - normally should be left as CHECK_PLAIN. Only set to
+ *   PASS_THROUGH if you have already removed any possibly dangerous code
+ *   from $title using a function like check_plain() or filter_xss(). With this
+ *   flag the string will be passed through unchanged.
+ *
+ * @return
+ *   The updated title of the current page.
+ */
+function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
+  $stored_title = &drupal_static(__FUNCTION__);
+
+  if (isset($title)) {
+    $stored_title = ($output == PASS_THROUGH) ? $title : check_plain($title);
+  }
+
+  return $stored_title;
+}
+
+/**
+ * Checks to see if an IP address has been blocked.
+ *
+ * Blocked IP addresses are stored in the database by default. However for
+ * performance reasons we allow an override in settings.php. This allows us
+ * to avoid querying the database at this critical stage of the bootstrap if
+ * an administrative interface for IP address blocking is not required.
+ *
+ * @param $ip
+ *   IP address to check.
+ *
+ * @return bool
+ *   TRUE if access is denied, FALSE if access is allowed.
+ */
+function drupal_is_denied($ip) {
+  // Because this function is called on every page request, we first check
+  // for an array of IP addresses in settings.php before querying the
+  // database.
+  $blocked_ips = variable_get('blocked_ips');
+  $denied = FALSE;
+  if (isset($blocked_ips) && is_array($blocked_ips)) {
+    $denied = in_array($ip, $blocked_ips);
+  }
+  // Only check if database.inc is loaded already. If
+  // $conf['page_cache_without_database'] = TRUE; is set in settings.php,
+  // then the database won't be loaded here so the IPs in the database
+  // won't be denied. However the user asked explicitly not to use the
+  // database and also in this case it's quite likely that the user relies
+  // on higher performance solutions like a firewall.
+  elseif (class_exists('Database', FALSE)) {
+    $denied = (bool)db_query("SELECT 1 FROM {blocked_ips} WHERE ip = :ip", array(':ip' => $ip))->fetchField();
+  }
+  return $denied;
+}
+
+/**
+ * Handles denied users.
+ *
+ * @param $ip
+ *   IP address to check. Prints a message and exits if access is denied.
+ */
+function drupal_block_denied($ip) {
+  // Deny access to blocked IP addresses - t() is not yet available.
+  if (drupal_is_denied($ip)) {
+    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
+    print 'Sorry, ' . check_plain(ip_address()) . ' has been banned.';
+    exit();
+  }
+}
+
+/**
+ * Returns a string of highly randomized bytes (over the full 8-bit range).
+ *
+ * This function is better than simply calling mt_rand() or any other built-in
+ * PHP function because it can return a long string of bytes (compared to < 4
+ * bytes normally from mt_rand()) and uses the best available pseudo-random
+ * source.
+ *
+ * @param $count
+ *   The number of characters (bytes) to return in the string.
+ */
+function drupal_random_bytes($count)  {
+  // $random_state does not use drupal_static as it stores random bytes.
+  static $random_state, $bytes, $php_compatible;
+  // Initialize on the first call. The contents of $_SERVER includes a mix of
+  // user-specific and system information that varies a little with each page.
+  if (!isset($random_state)) {
+    $random_state = print_r($_SERVER, TRUE);
+    if (function_exists('getmypid')) {
+      // Further initialize with the somewhat random PHP process ID.
+      $random_state .= getmypid();
+    }
+    $bytes = '';
+  }
+  if (strlen($bytes) < $count) {
+    // PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
+    // locking on Windows and rendered it unusable.
+    if (!isset($php_compatible)) {
+      $php_compatible = version_compare(PHP_VERSION, '5.3.4', '>=');
+    }
+    // /dev/urandom is available on many *nix systems and is considered the
+    // best commonly available pseudo-random source.
+    if ($fh = @fopen('/dev/urandom', 'rb')) {
+      // PHP only performs buffered reads, so in reality it will always read
+      // at least 4096 bytes. Thus, it costs nothing extra to read and store
+      // that much so as to speed any additional invocations.
+      $bytes .= fread($fh, max(4096, $count));
+      fclose($fh);
+    }
+    // openssl_random_pseudo_bytes() will find entropy in a system-dependent
+    // way.
+    elseif ($php_compatible && function_exists('openssl_random_pseudo_bytes')) {
+      $bytes .= openssl_random_pseudo_bytes($count - strlen($bytes));
+    }
+    // If /dev/urandom is not available or returns no bytes, this loop will
+    // generate a good set of pseudo-random bytes on any system.
+    // Note that it may be important that our $random_state is passed
+    // through hash() prior to being rolled into $output, that the two hash()
+    // invocations are different, and that the extra input into the first one -
+    // the microtime() - is prepended rather than appended. This is to avoid
+    // directly leaking $random_state via the $output stream, which could
+    // allow for trivial prediction of further "random" numbers.
+    while (strlen($bytes) < $count) {
+      $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
+      $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
+    }
+  }
+  $output = substr($bytes, 0, $count);
+  $bytes = substr($bytes, $count);
+  return $output;
+}
+
+/**
+ * Calculates a base-64 encoded, URL-safe sha-256 hmac.
+ *
+ * @param $data
+ *   String to be validated with the hmac.
+ * @param $key
+ *   A secret string key.
+ *
+ * @return
+ *   A base-64 encoded sha-256 hmac, with + replaced with -, / with _ and
+ *   any = padding characters removed.
+ */
+function drupal_hmac_base64($data, $key) {
+  $hmac = base64_encode(hash_hmac('sha256', $data, $key, TRUE));
+  // Modify the hmac so it's safe to use in URLs.
+  return strtr($hmac, array('+' => '-', '/' => '_', '=' => ''));
+}
+
+/**
+ * Calculates a base-64 encoded, URL-safe sha-256 hash.
+ *
+ * @param $data
+ *   String to be hashed.
+ *
+ * @return
+ *   A base-64 encoded sha-256 hash, with + replaced with -, / with _ and
+ *   any = padding characters removed.
+ */
+function drupal_hash_base64($data) {
+  $hash = base64_encode(hash('sha256', $data, TRUE));
+  // Modify the hash so it's safe to use in URLs.
+  return strtr($hash, array('+' => '-', '/' => '_', '=' => ''));
+}
+
+/**
+ * Merges multiple arrays, recursively, and returns the merged array.
+ *
+ * This function is similar to PHP's array_merge_recursive() function, but it
+ * handles non-array values differently. When merging values that are not both
+ * arrays, the latter value replaces the former rather than merging with it.
+ *
+ * Example:
+ * @code
+ * $link_options_1 = array('fragment' => 'x', 'attributes' => array('title' => t('X'), 'class' => array('a', 'b')));
+ * $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('c', 'd')));
+ *
+ * // This results in array('fragment' => array('x', 'y'), 'attributes' => array('title' => array(t('X'), t('Y')), 'class' => array('a', 'b', 'c', 'd'))).
+ * $incorrect = array_merge_recursive($link_options_1, $link_options_2);
+ *
+ * // This results in array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('a', 'b', 'c', 'd'))).
+ * $correct = drupal_array_merge_deep($link_options_1, $link_options_2);
+ * @endcode
+ *
+ * @param ...
+ *   Arrays to merge.
+ *
+ * @return
+ *   The merged array.
+ *
+ * @see drupal_array_merge_deep_array()
+ */
+function drupal_array_merge_deep() {
+  $args = func_get_args();
+  return drupal_array_merge_deep_array($args);
+}
+
+/**
+ * Merges multiple arrays, recursively, and returns the merged array.
+ *
+ * This function is equivalent to drupal_array_merge_deep(), except the
+ * input arrays are passed as a single array parameter rather than a variable
+ * parameter list.
+ *
+ * The following are equivalent:
+ * - drupal_array_merge_deep($a, $b);
+ * - drupal_array_merge_deep_array(array($a, $b));
+ *
+ * The following are also equivalent:
+ * - call_user_func_array('drupal_array_merge_deep', $arrays_to_merge);
+ * - drupal_array_merge_deep_array($arrays_to_merge);
+ *
+ * @see drupal_array_merge_deep()
+ */
+function drupal_array_merge_deep_array($arrays) {
+  $result = array();
+
+  foreach ($arrays as $array) {
+    foreach ($array as $key => $value) {
+      // Renumber integer keys as array_merge_recursive() does. Note that PHP
+      // automatically converts array keys that are integer strings (e.g., '1')
+      // to integers.
+      if (is_integer($key)) {
+        $result[] = $value;
+      }
+      // Recurse when both values are arrays.
+      elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
+        $result[$key] = drupal_array_merge_deep_array(array($result[$key], $value));
+      }
+      // Otherwise, use the latter value, overriding any previous value.
+      else {
+        $result[$key] = $value;
+      }
+    }
+  }
+
+  return $result;
+}
+
+/**
+ * Generates a default anonymous $user object.
+ *
+ * @return Object - the user object.
+ */
+function drupal_anonymous_user() {
+  $user = new stdClass();
+  $user->uid = 0;
+  $user->hostname = ip_address();
+  $user->roles = array();
+  $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
+  $user->cache = 0;
+  return $user;
+}
+
+/**
+ * Ensures Drupal is bootstrapped to the specified phase.
+ *
+ * In order to bootstrap Drupal from another PHP script, you can use this code:
+ * @code
+ *   define('DRUPAL_ROOT', '/path/to/drupal');
+ *   require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+ *   drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+ * @endcode
+ *
+ * @param $phase
+ *   A constant telling which phase to bootstrap to. When you bootstrap to a
+ *   particular phase, all earlier phases are run automatically. Possible
+ *   values:
+ *   - DRUPAL_BOOTSTRAP_CONFIGURATION: Initializes configuration.
+ *   - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page.
+ *   - DRUPAL_BOOTSTRAP_DATABASE: Initializes the database layer.
+ *   - DRUPAL_BOOTSTRAP_VARIABLES: Initializes the variable system.
+ *   - DRUPAL_BOOTSTRAP_SESSION: Initializes session handling.
+ *   - DRUPAL_BOOTSTRAP_PAGE_HEADER: Sets up the page header.
+ *   - DRUPAL_BOOTSTRAP_LANGUAGE: Finds out the language of the page.
+ *   - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input
+ *     data.
+ * @param $new_phase
+ *   A boolean, set to FALSE if calling drupal_bootstrap from inside a
+ *   function called from drupal_bootstrap (recursion).
+ *
+ * @return
+ *   The most recently completed phase.
+ */
+function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
+  // Not drupal_static(), because does not depend on any run-time information.
+  static $phases = array(
+    DRUPAL_BOOTSTRAP_CONFIGURATION,
+    DRUPAL_BOOTSTRAP_PAGE_CACHE,
+    DRUPAL_BOOTSTRAP_DATABASE,
+    DRUPAL_BOOTSTRAP_VARIABLES,
+    DRUPAL_BOOTSTRAP_SESSION,
+    DRUPAL_BOOTSTRAP_PAGE_HEADER,
+    DRUPAL_BOOTSTRAP_LANGUAGE,
+    DRUPAL_BOOTSTRAP_FULL,
+  );
+  // Not drupal_static(), because the only legitimate API to control this is to
+  // call drupal_bootstrap() with a new phase parameter.
+  static $final_phase;
+  // Not drupal_static(), because it's impossible to roll back to an earlier
+  // bootstrap state.
+  static $stored_phase = -1;
+
+  // When not recursing, store the phase name so it's not forgotten while
+  // recursing.
+  if ($new_phase) {
+    $final_phase = $phase;
+  }
+  if (isset($phase)) {
+    // Call a phase if it has not been called before and is below the requested
+    // phase.
+    while ($phases && $phase > $stored_phase && $final_phase > $stored_phase) {
+      $current_phase = array_shift($phases);
+
+      // This function is re-entrant. Only update the completed phase when the
+      // current call actually resulted in a progress in the bootstrap process.
+      if ($current_phase > $stored_phase) {
+        $stored_phase = $current_phase;
+      }
+
+      switch ($current_phase) {
+        case DRUPAL_BOOTSTRAP_CONFIGURATION:
+          _drupal_bootstrap_configuration();
+          break;
+
+        case DRUPAL_BOOTSTRAP_PAGE_CACHE:
+          _drupal_bootstrap_page_cache();
+          break;
+
+        case DRUPAL_BOOTSTRAP_DATABASE:
+          _drupal_bootstrap_database();
+          break;
+
+        case DRUPAL_BOOTSTRAP_VARIABLES:
+          _drupal_bootstrap_variables();
+          break;
+
+        case DRUPAL_BOOTSTRAP_SESSION:
+          require_once DRUPAL_ROOT . '/' . variable_get('session_inc', 'includes/session.inc');
+          drupal_session_initialize();
+          break;
+
+        case DRUPAL_BOOTSTRAP_PAGE_HEADER:
+          _drupal_bootstrap_page_header();
+          break;
+
+        case DRUPAL_BOOTSTRAP_LANGUAGE:
+          drupal_language_initialize();
+          break;
+
+        case DRUPAL_BOOTSTRAP_FULL:
+          require_once DRUPAL_ROOT . '/includes/common.inc';
+          _drupal_bootstrap_full();
+          break;
+      }
+    }
+  }
+  return $stored_phase;
+}
+
+/**
+ * Returns the time zone of the current user.
+ */
+function drupal_get_user_timezone() {
+  global $user;
+  if (variable_get('configurable_timezones', 1) && $user->uid && $user->timezone) {
+    return $user->timezone;
+  }
+  else {
+    // Ignore PHP strict notice if time zone has not yet been set in the php.ini
+    // configuration.
+    return variable_get('date_default_timezone', @date_default_timezone_get());
+  }
+}
+
+/**
+ * Provides custom PHP error handling.
+ *
+ * @param $error_level
+ *   The level of the error raised.
+ * @param $message
+ *   The error message.
+ * @param $filename
+ *   The filename that the error was raised in.
+ * @param $line
+ *   The line number the error was raised at.
+ * @param $context
+ *   An array that points to the active symbol table at the point the error
+ *   occurred.
+ */
+function _drupal_error_handler($error_level, $message, $filename, $line, $context) {
+  require_once DRUPAL_ROOT . '/includes/errors.inc';
+  _drupal_error_handler_real($error_level, $message, $filename, $line, $context);
+}
+
+/**
+ * Provides custom PHP exception handling.
+ *
+ * Uncaught exceptions are those not enclosed in a try/catch block. They are
+ * always fatal: the execution of the script will stop as soon as the exception
+ * handler exits.
+ *
+ * @param $exception
+ *   The exception object that was thrown.
+ */
+function _drupal_exception_handler($exception) {
+  require_once DRUPAL_ROOT . '/includes/errors.inc';
+
+  try {
+    // Log the message to the watchdog and return an error page to the user.
+    _drupal_log_error(_drupal_decode_exception($exception), TRUE);
+  }
+  catch (Exception $exception2) {
+    // Another uncaught exception was thrown while handling the first one.
+    // If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
+    if (error_displayable()) {
+      print '<h1>Additional uncaught exception thrown while handling exception.</h1>';
+      print '<h2>Original</h2><p>' . _drupal_render_exception_safe($exception) . '</p>';
+      print '<h2>Additional</h2><p>' . _drupal_render_exception_safe($exception2) . '</p><hr />';
+    }
+  }
+}
+
+/**
+ * Sets up the script environment and loads settings.php.
+ */
+function _drupal_bootstrap_configuration() {
+  // Set the Drupal custom error handler.
+  set_error_handler('_drupal_error_handler');
+  set_exception_handler('_drupal_exception_handler');
+
+  drupal_environment_initialize();
+  // Start a page timer:
+  timer_start('page');
+  // Initialize the configuration, including variables from settings.php.
+  drupal_settings_initialize();
+}
+
+/**
+ * Attempts to serve a page from the cache.
+ */
+function _drupal_bootstrap_page_cache() {
+  global $user;
+
+  // Allow specifying special cache handlers in settings.php, like
+  // using memcached or files for storing cache information.
+  require_once DRUPAL_ROOT . '/includes/cache.inc';
+  foreach (variable_get('cache_backends', array()) as $include) {
+    require_once DRUPAL_ROOT . '/' . $include;
+  }
+  // Check for a cache mode force from settings.php.
+  if (variable_get('page_cache_without_database')) {
+    $cache_enabled = TRUE;
+  }
+  else {
+    drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE);
+    $cache_enabled = variable_get('cache');
+  }
+  drupal_block_denied(ip_address());
+  // If there is no session cookie and cache is enabled (or forced), try
+  // to serve a cached page.
+  if (!isset($_COOKIE[session_name()]) && $cache_enabled) {
+    // Make sure there is a user object because its timestamp will be
+    // checked, hook_boot might check for anonymous user etc.
+    $user = drupal_anonymous_user();
+    // Get the page from the cache.
+    $cache = drupal_page_get_cache();
+    // If there is a cached page, display it.
+    if (is_object($cache)) {
+      header('X-Drupal-Cache: HIT');
+      // Restore the metadata cached with the page.
+      $_GET['q'] = $cache->data['path'];
+      drupal_set_title($cache->data['title'], PASS_THROUGH);
+      date_default_timezone_set(drupal_get_user_timezone());
+      // If the skipping of the bootstrap hooks is not enforced, call
+      // hook_boot.
+      if (variable_get('page_cache_invoke_hooks', TRUE)) {
+        bootstrap_invoke_all('boot');
+      }
+      drupal_serve_page_from_cache($cache);
+      // If the skipping of the bootstrap hooks is not enforced, call
+      // hook_exit.
+      if (variable_get('page_cache_invoke_hooks', TRUE)) {
+        bootstrap_invoke_all('exit');
+      }
+      // We are done.
+      exit;
+    }
+    else {
+      header('X-Drupal-Cache: MISS');
+    }
+  }
+}
+
+/**
+ * Initializes the database system and registers autoload functions.
+ */
+function _drupal_bootstrap_database() {
+  // Redirect the user to the installation script if Drupal has not been
+  // installed yet (i.e., if no $databases array has been defined in the
+  // settings.php file) and we are not already installing.
+  if (empty($GLOBALS['databases']) && !drupal_installation_attempted()) {
+    include_once DRUPAL_ROOT . '/includes/install.inc';
+    install_goto('install.php');
+  }
+
+  // The user agent header is used to pass a database prefix in the request when
+  // running tests. However, for security reasons, it is imperative that we
+  // validate we ourselves made the request.
+  if ($test_prefix = drupal_valid_test_ua()) {
+    // Set the test run id for use in other parts of Drupal.
+    $test_info = &$GLOBALS['drupal_test_info'];
+    $test_info['test_run_id'] = $test_prefix;
+    $test_info['in_child_site'] = TRUE;
+
+    foreach ($GLOBALS['databases']['default'] as &$value) {
+      // Extract the current default database prefix.
+      if (!isset($value['prefix'])) {
+        $current_prefix = '';
+      }
+      elseif (is_array($value['prefix'])) {
+        $current_prefix = $value['prefix']['default'];
+      }
+      else {
+        $current_prefix = $value['prefix'];
+      }
+
+      // Remove the current database prefix and replace it by our own.
+      $value['prefix'] = array(
+        'default' => $current_prefix . $test_prefix,
+      );
+    }
+  }
+
+  // Initialize the database system. Note that the connection
+  // won't be initialized until it is actually requested.
+  require_once DRUPAL_ROOT . '/includes/database/database.inc';
+
+  // Register autoload functions so that we can access classes and interfaces.
+  // The database autoload routine comes first so that we can load the database
+  // system without hitting the database. That is especially important during
+  // the install or upgrade process.
+  spl_autoload_register('drupal_autoload_class');
+  spl_autoload_register('drupal_autoload_interface');
+}
+
+/**
+ * Loads system variables and all enabled bootstrap modules.
+ */
+function _drupal_bootstrap_variables() {
+  global $conf;
+
+  // Initialize the lock system.
+  require_once DRUPAL_ROOT . '/' . variable_get('lock_inc', 'includes/lock.inc');
+  lock_initialize();
+
+  // Load variables from the database, but do not overwrite variables set in settings.php.
+  $conf = variable_initialize(isset($conf) ? $conf : array());
+  // Load bootstrap modules.
+  require_once DRUPAL_ROOT . '/includes/module.inc';
+  module_load_all(TRUE);
+}
+
+/**
+ * Invokes hook_boot(), initializes locking system, and sends HTTP headers.
+ */
+function _drupal_bootstrap_page_header() {
+  bootstrap_invoke_all('boot');
+
+  if (!drupal_is_cli()) {
+    ob_start();
+    drupal_page_header();
+  }
+}
+
+/**
+ * Returns the current bootstrap phase for this Drupal process.
+ *
+ * The current phase is the one most recently completed by drupal_bootstrap().
+ *
+ * @see drupal_bootstrap()
+ */
+function drupal_get_bootstrap_phase() {
+  return drupal_bootstrap();
+}
+
+/**
+ * Returns the test prefix if this is an internal request from SimpleTest.
+ *
+ * @return
+ *   Either the simpletest prefix (the string "simpletest" followed by any
+ *   number of digits) or FALSE if the user agent does not contain a valid
+ *   HMAC and timestamp.
+ */
+function drupal_valid_test_ua() {
+  global $drupal_hash_salt;
+  // No reason to reset this.
+  static $test_prefix;
+
+  if (isset($test_prefix)) {
+    return $test_prefix;
+  }
+
+  if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
+    list(, $prefix, $time, $salt, $hmac) = $matches;
+    $check_string =  $prefix . ';' . $time . ';' . $salt;
+    // We use the salt from settings.php to make the HMAC key, since
+    // the database is not yet initialized and we can't access any Drupal variables.
+    // The file properties add more entropy not easily accessible to others.
+    $key = $drupal_hash_salt . filectime(__FILE__) . fileinode(__FILE__);
+    $time_diff = REQUEST_TIME - $time;
+    // Since we are making a local request a 5 second time window is allowed,
+    // and the HMAC must match.
+    if ($time_diff >= 0 && $time_diff <= 5 && $hmac == drupal_hmac_base64($check_string, $key)) {
+      $test_prefix = $prefix;
+      return $test_prefix;
+    }
+  }
+
+  $test_prefix = FALSE;
+  return $test_prefix;
+}
+
+/**
+ * Generates a user agent string with a HMAC and timestamp for simpletest.
+ */
+function drupal_generate_test_ua($prefix) {
+  global $drupal_hash_salt;
+  static $key;
+
+  if (!isset($key)) {
+    // We use the salt from settings.php to make the HMAC key, since
+    // the database is not yet initialized and we can't access any Drupal variables.
+    // The file properties add more entropy not easily accessible to others.
+    $key = $drupal_hash_salt . filectime(__FILE__) . fileinode(__FILE__);
+  }
+  // Generate a moderately secure HMAC based on the database credentials.
+  $salt = uniqid('', TRUE);
+  $check_string = $prefix . ';' . time() . ';' . $salt;
+  return $check_string . ';' . drupal_hmac_base64($check_string, $key);
+}
+
+/**
+ * Enables use of the theme system without requiring database access.
+ *
+ * Loads and initializes the theme system for site installs, updates and when
+ * the site is in maintenance mode. This also applies when the database fails.
+ *
+ * @see _drupal_maintenance_theme()
+ */
+function drupal_maintenance_theme() {
+  require_once DRUPAL_ROOT . '/includes/theme.maintenance.inc';
+  _drupal_maintenance_theme();
+}
+
+/**
+ * Returns a simple 404 Not Found page.
+ *
+ * If fast 404 pages are enabled, and this is a matching page then print a
+ * simple 404 page and exit.
+ *
+ * This function is called from drupal_deliver_html_page() at the time when a
+ * a normal 404 page is generated, but it can also optionally be called directly
+ * from settings.php to prevent a Drupal bootstrap on these pages. See
+ * documentation in settings.php for the benefits and drawbacks of using this.
+ *
+ * Paths to dynamically-generated content, such as image styles, should also be
+ * accounted for in this function.
+ */
+function drupal_fast_404() {
+  $exclude_paths = variable_get('404_fast_paths_exclude', FALSE);
+  if ($exclude_paths && !preg_match($exclude_paths, $_GET['q'])) {
+    $fast_paths = variable_get('404_fast_paths', FALSE);
+    if ($fast_paths && preg_match($fast_paths, $_GET['q'])) {
+      drupal_add_http_header('Status', '404 Not Found');
+      $fast_404_html = variable_get('404_fast_html', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL "@path" was not found on this server.</p></body></html>');
+      // Replace @path in the variable with the page path.
+      print strtr($fast_404_html, array('@path' => check_plain(request_uri())));
+      exit;
+    }
+  }
+}
+
+/**
+ * Returns TRUE if a Drupal installation is currently being attempted.
+ */
+function drupal_installation_attempted() {
+  return defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install';
+}
+
+/**
+ * Returns the name of the proper localization function.
+ *
+ * get_t() exists to support localization for code that might run during
+ * the installation phase, when some elements of the system might not have
+ * loaded.
+ *
+ * This would include implementations of hook_install(), which could run
+ * during the Drupal installation phase, and might also be run during
+ * non-installation time, such as while installing the module from the the
+ * module administration page.
+ *
+ * Example usage:
+ * @code
+ *   $t = get_t();
+ *   $translated = $t('translate this');
+ * @endcode
+ *
+ * Use t() if your code will never run during the Drupal installation phase.
+ * Use st() if your code will only run during installation and never any other
+ * time. Use get_t() if your code could run in either circumstance.
+ *
+ * @see t()
+ * @see st()
+ * @ingroup sanitization
+ */
+function get_t() {
+  static $t;
+  // This is not converted to drupal_static because there is no point in
+  // resetting this as it can not change in the course of a request.
+  if (!isset($t)) {
+    $t = drupal_installation_attempted() ? 'st' : 't';
+  }
+  return $t;
+}
+
+/**
+ * Initializes all the defined language types.
+ */
+function drupal_language_initialize() {
+  $types = language_types();
+
+  // Ensure the language is correctly returned, even without multilanguage
+  // support. Also make sure we have a $language fallback, in case a language
+  // negotiation callback needs to do a full bootstrap.
+  // Useful for eg. XML/HTML 'lang' attributes.
+  $default = language_default();
+  foreach ($types as $type) {
+    $GLOBALS[$type] = $default;
+  }
+  if (drupal_multilingual()) {
+    include_once DRUPAL_ROOT . '/includes/language.inc';
+    foreach ($types as $type) {
+      $GLOBALS[$type] = language_initialize($type);
+    }
+    // Allow modules to react on language system initialization in multilingual
+    // environments.
+    bootstrap_invoke_all('language_init');
+  }
+}
+
+/**
+ * Returns a list of the built-in language types.
+ *
+ * @return
+ *   An array of key-values pairs where the key is the language type and the
+ *   value is its configurability.
+ */
+function drupal_language_types() {
+  return array(
+    LANGUAGE_TYPE_INTERFACE => TRUE,
+    LANGUAGE_TYPE_CONTENT => FALSE,
+    LANGUAGE_TYPE_URL => FALSE,
+  );
+}
+
+/**
+ * Returns TRUE if there is more than one language enabled.
+ *
+ * @return
+ *   TRUE if more than one language is enabled.
+ */
+function drupal_multilingual() {
+  // The "language_count" variable stores the number of enabled languages to
+  // avoid unnecessarily querying the database when building the list of
+  // enabled languages on monolingual sites.
+  return variable_get('language_count', 1) > 1;
+}
+
+/**
+ * Returns an array of the available language types.
+ *
+ * @return
+ *   An array of all language types where the keys of each are the language type
+ *   name and its value is its configurability (TRUE/FALSE).
+ */
+function language_types() {
+  return array_keys(variable_get('language_types', drupal_language_types()));
+}
+
+/**
+ * Returns a list of installed languages, indexed by the specified key.
+ *
+ * @param $field
+ *   (optional) The field to index the list with.
+ *
+ * @return
+ *   An associative array, keyed on the values of $field.
+ *   - If $field is 'weight' or 'enabled', the array is nested, with the outer
+ *     array's values each being associative arrays with language codes as
+ *     keys and language objects as values.
+ *   - For all other values of $field, the array is only one level deep, and
+ *     the array's values are language objects.
+ */
+function language_list($field = 'language') {
+  $languages = &drupal_static(__FUNCTION__);
+  // Init language list
+  if (!isset($languages)) {
+    if (drupal_multilingual() || module_exists('locale')) {
+      $languages['language'] = db_query('SELECT * FROM {languages} ORDER BY weight ASC, name ASC')->fetchAllAssoc('language');
+      // Users cannot uninstall the native English language. However, we allow
+      // it to be hidden from the installed languages. Therefore, at least one
+      // other language must be enabled then.
+      if (!$languages['language']['en']->enabled && !variable_get('language_native_enabled', TRUE)) {
+        unset($languages['language']['en']);
+      }
+    }
+    else {
+      // No locale module, so use the default language only.
+      $default = language_default();
+      $languages['language'][$default->language] = $default;
+    }
+  }
+
+  // Return the array indexed by the right field
+  if (!isset($languages[$field])) {
+    $languages[$field] = array();
+    foreach ($languages['language'] as $lang) {
+      // Some values should be collected into an array
+      if (in_array($field, array('enabled', 'weight'))) {
+        $languages[$field][$lang->$field][$lang->language] = $lang;
+      }
+      else {
+        $languages[$field][$lang->$field] = $lang;
+      }
+    }
+  }
+  return $languages[$field];
+}
+
+/**
+ * Returns the default language used on the site
+ *
+ * @param $property
+ *   Optional property of the language object to return
+ */
+function language_default($property = NULL) {
+  $language = variable_get('language_default', (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => ''));
+  return $property ? $language->$property : $language;
+}
+
+/**
+ * Returns the requested URL path of the page being viewed.
+ *
+ * Examples:
+ * - http://example.com/node/306 returns "node/306".
+ * - http://example.com/drupalfolder/node/306 returns "node/306" while
+ *   base_path() returns "/drupalfolder/".
+ * - http://example.com/path/alias (which is a path alias for node/306) returns
+ *   "path/alias" as opposed to the internal path.
+ * - http://example.com/index.php returns an empty string (meaning: front page).
+ * - http://example.com/index.php?page=1 returns an empty string.
+ *
+ * @return
+ *   The requested Drupal URL path.
+ *
+ * @see current_path()
+ */
+function request_path() {
+  static $path;
+
+  if (isset($path)) {
+    return $path;
+  }
+
+  if (isset($_GET['q']) && is_string($_GET['q'])) {
+    // This is a request with a ?q=foo/bar query string. $_GET['q'] is
+    // overwritten in drupal_path_initialize(), but request_path() is called
+    // very early in the bootstrap process, so the original value is saved in
+    // $path and returned in later calls.
+    $path = $_GET['q'];
+  }
+  elseif (isset($_SERVER['REQUEST_URI'])) {
+    // This request is either a clean URL, or 'index.php', or nonsense.
+    // Extract the path from REQUEST_URI.
+    $request_path = strtok($_SERVER['REQUEST_URI'], '?');
+    $base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/'));
+    // Unescape and strip $base_path prefix, leaving q without a leading slash.
+    $path = substr(urldecode($request_path), $base_path_len + 1);
+    // If the path equals the script filename, either because 'index.php' was
+    // explicitly provided in the URL, or because the server added it to
+    // $_SERVER['REQUEST_URI'] even when it wasn't provided in the URL (some
+    // versions of Microsoft IIS do this), the front page should be served.
+    if ($path == basename($_SERVER['PHP_SELF'])) {
+      $path = '';
+    }
+  }
+  else {
+    // This is the front page.
+    $path = '';
+  }
+
+  // Under certain conditions Apache's RewriteRule directive prepends the value
+  // assigned to $_GET['q'] with a slash. Moreover we can always have a trailing
+  // slash in place, hence we need to normalize $_GET['q'].
+  $path = trim($path, '/');
+
+  return $path;
+}
+
+/**
+ * Returns a component of the current Drupal path.
+ *
+ * When viewing a page at the path "admin/structure/types", for example, arg(0)
+ * returns "admin", arg(1) returns "structure", and arg(2) returns "types".
+ *
+ * Avoid use of this function where possible, as resulting code is hard to
+ * read. In menu callback functions, attempt to use named arguments. See the
+ * explanation in menu.inc for how to construct callbacks that take arguments.
+ * When attempting to use this function to load an element from the current
+ * path, e.g. loading the node on a node page, use menu_get_object() instead.
+ *
+ * @param $index
+ *   The index of the component, where each component is separated by a '/'
+ *   (forward-slash), and where the first component has an index of 0 (zero).
+ * @param $path
+ *   A path to break into components. Defaults to the path of the current page.
+ *
+ * @return
+ *   The component specified by $index, or NULL if the specified component was
+ *   not found. If called without arguments, it returns an array containing all
+ *   the components of the current path.
+ */
+function arg($index = NULL, $path = NULL) {
+  // Even though $arguments doesn't need to be resettable for any functional
+  // reasons (the result of explode() does not depend on any run-time
+  // information), it should be resettable anyway in case a module needs to
+  // free up the memory used by it.
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['arguments'] = &drupal_static(__FUNCTION__);
+  }
+  $arguments = &$drupal_static_fast['arguments'];
+
+  if (!isset($path)) {
+    $path = $_GET['q'];
+  }
+  if (!isset($arguments[$path])) {
+    $arguments[$path] = explode('/', $path);
+  }
+  if (!isset($index)) {
+    return $arguments[$path];
+  }
+  if (isset($arguments[$path][$index])) {
+    return $arguments[$path][$index];
+  }
+}
+
+/**
+ * Returns the IP address of the client machine.
+ *
+ * If Drupal is behind a reverse proxy, we use the X-Forwarded-For header
+ * instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of
+ * the proxy server, and not the client's. The actual header name can be
+ * configured by the reverse_proxy_header variable.
+ *
+ * @return
+ *   IP address of client machine, adjusted for reverse proxy and/or cluster
+ *   environments.
+ */
+function ip_address() {
+  $ip_address = &drupal_static(__FUNCTION__);
+
+  if (!isset($ip_address)) {
+    $ip_address = $_SERVER['REMOTE_ADDR'];
+
+    if (variable_get('reverse_proxy', 0)) {
+      $reverse_proxy_header = variable_get('reverse_proxy_header', 'HTTP_X_FORWARDED_FOR');
+      if (!empty($_SERVER[$reverse_proxy_header])) {
+        // If an array of known reverse proxy IPs is provided, then trust
+        // the XFF header if request really comes from one of them.
+        $reverse_proxy_addresses = variable_get('reverse_proxy_addresses', array());
+
+        // Turn XFF header into an array.
+        $forwarded = explode(',', $_SERVER[$reverse_proxy_header]);
+
+        // Trim the forwarded IPs; they may have been delimited by commas and spaces.
+        $forwarded = array_map('trim', $forwarded);
+
+        // Tack direct client IP onto end of forwarded array.
+        $forwarded[] = $ip_address;
+
+        // Eliminate all trusted IPs.
+        $untrusted = array_diff($forwarded, $reverse_proxy_addresses);
+
+        // The right-most IP is the most specific we can trust.
+        $ip_address = array_pop($untrusted);
+      }
+    }
+  }
+
+  return $ip_address;
+}
+
+/**
+ * @addtogroup schemaapi
+ * @{
+ */
+
+/**
+ * Gets the schema definition of a table, or the whole database schema.
+ *
+ * The returned schema will include any modifications made by any
+ * module that implements hook_schema_alter().
+ *
+ * @param $table
+ *   The name of the table. If not given, the schema of all tables is returned.
+ * @param $rebuild
+ *   If true, the schema will be rebuilt instead of retrieved from the cache.
+ */
+function drupal_get_schema($table = NULL, $rebuild = FALSE) {
+  static $schema;
+
+  if ($rebuild || !isset($table)) {
+    $schema = drupal_get_complete_schema($rebuild);
+  }
+  elseif (!isset($schema)) {
+    $schema = new SchemaCache();
+  }
+
+  if (!isset($table)) {
+    return $schema;
+  }
+  if (isset($schema[$table])) {
+    return $schema[$table];
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Extends DrupalCacheArray to allow for dynamic building of the schema cache.
+ */
+class SchemaCache extends DrupalCacheArray {
+
+  /**
+   * Constructs a SchemaCache object.
+   */
+  public function __construct() {
+    // Cache by request method.
+    parent::__construct('schema:runtime:' . ($_SERVER['REQUEST_METHOD'] == 'GET'), 'cache');
+  }
+
+  /**
+   * Overrides DrupalCacheArray::resolveCacheMiss().
+   */
+  protected function resolveCacheMiss($offset) {
+    $complete_schema = drupal_get_complete_schema();
+    $value = isset($complete_schema[$offset]) ? $complete_schema[$offset] :  NULL;
+    $this->storage[$offset] = $value;
+    $this->persist($offset);
+    return $value;
+  }
+}
+
+/**
+ * Gets the whole database schema.
+ *
+ * The returned schema will include any modifications made by any
+ * module that implements hook_schema_alter().
+ *
+ * @param $rebuild
+ *   If true, the schema will be rebuilt instead of retrieved from the cache.
+ */
+function drupal_get_complete_schema($rebuild = FALSE) {
+  static $schema = array();
+
+  if (empty($schema) || $rebuild) {
+    // Try to load the schema from cache.
+    if (!$rebuild && $cached = cache_get('schema')) {
+      $schema = $cached->data;
+    }
+    // Otherwise, rebuild the schema cache.
+    else {
+      $schema = array();
+      // Load the .install files to get hook_schema.
+      // On some databases this function may be called before bootstrap has
+      // been completed, so we force the functions we need to load just in case.
+      if (function_exists('module_load_all_includes')) {
+        // This function can be called very early in the bootstrap process, so
+        // we force the module_list() cache to be refreshed to ensure that it
+        // contains the complete list of modules before we go on to call
+        // module_load_all_includes().
+        module_list(TRUE);
+        module_load_all_includes('install');
+      }
+
+      require_once DRUPAL_ROOT . '/includes/common.inc';
+      // Invoke hook_schema for all modules.
+      foreach (module_implements('schema') as $module) {
+        // Cast the result of hook_schema() to an array, as a NULL return value
+        // would cause array_merge() to set the $schema variable to NULL as well.
+        // That would break modules which use $schema further down the line.
+        $current = (array) module_invoke($module, 'schema');
+        // Set 'module' and 'name' keys for each table, and remove descriptions,
+        // as they needlessly slow down cache_get() for every single request.
+        _drupal_schema_initialize($current, $module);
+        $schema = array_merge($schema, $current);
+      }
+
+      drupal_alter('schema', $schema);
+      // If the schema is empty, avoid saving it: some database engines require
+      // the schema to perform queries, and this could lead to infinite loops.
+      if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) {
+        cache_set('schema', $schema);
+      }
+      if ($rebuild) {
+        cache_clear_all('schema:', 'cache', TRUE);
+      }
+    }
+  }
+
+  return $schema;
+}
+
+/**
+ * @} End of "addtogroup schemaapi".
+ */
+
+
+/**
+ * @addtogroup registry
+ * @{
+ */
+
+/**
+ * Confirms that an interface is available.
+ *
+ * This function is rarely called directly. Instead, it is registered as an
+ * spl_autoload()  handler, and PHP calls it for us when necessary.
+ *
+ * @param $interface
+ *   The name of the interface to check or load.
+ *
+ * @return
+ *   TRUE if the interface is currently available, FALSE otherwise.
+ */
+function drupal_autoload_interface($interface) {
+  return _registry_check_code('interface', $interface);
+}
+
+/**
+ * Confirms that a class is available.
+ *
+ * This function is rarely called directly. Instead, it is registered as an
+ * spl_autoload()  handler, and PHP calls it for us when necessary.
+ *
+ * @param $class
+ *   The name of the class to check or load.
+ *
+ * @return
+ *   TRUE if the class is currently available, FALSE otherwise.
+ */
+function drupal_autoload_class($class) {
+  return _registry_check_code('class', $class);
+}
+
+/**
+ * Checks for a resource in the registry.
+ *
+ * @param $type
+ *   The type of resource we are looking up, or one of the constants
+ *   REGISTRY_RESET_LOOKUP_CACHE or REGISTRY_WRITE_LOOKUP_CACHE, which
+ *   signal that we should reset or write the cache, respectively.
+ * @param $name
+ *   The name of the resource, or NULL if either of the REGISTRY_* constants
+ *   is passed in.
+ *
+ * @return
+ *   TRUE if the resource was found, FALSE if not.
+ *   NULL if either of the REGISTRY_* constants is passed in as $type.
+ */
+function _registry_check_code($type, $name = NULL) {
+  static $lookup_cache, $cache_update_needed;
+
+  if ($type == 'class' && class_exists($name) || $type == 'interface' && interface_exists($name)) {
+    return TRUE;
+  }
+
+  if (!isset($lookup_cache)) {
+    $lookup_cache = array();
+    if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) {
+      $lookup_cache = $cache->data;
+    }
+  }
+
+  // When we rebuild the registry, we need to reset this cache so
+  // we don't keep lookups for resources that changed during the rebuild.
+  if ($type == REGISTRY_RESET_LOOKUP_CACHE) {
+    $cache_update_needed = TRUE;
+    $lookup_cache = NULL;
+    return;
+  }
+
+  // Called from drupal_page_footer, we write to permanent storage if there
+  // changes to the lookup cache for this request.
+  if ($type == REGISTRY_WRITE_LOOKUP_CACHE) {
+    if ($cache_update_needed) {
+      cache_set('lookup_cache', $lookup_cache, 'cache_bootstrap');
+    }
+    return;
+  }
+
+  // $type is either 'interface' or 'class', so we only need the first letter to
+  // keep the cache key unique.
+  $cache_key = $type[0] . $name;
+  if (isset($lookup_cache[$cache_key])) {
+    if ($lookup_cache[$cache_key]) {
+      require_once DRUPAL_ROOT . '/' . $lookup_cache[$cache_key];
+    }
+    return (bool) $lookup_cache[$cache_key];
+  }
+
+  // This function may get called when the default database is not active, but
+  // there is no reason we'd ever want to not use the default database for
+  // this query.
+  $file = Database::getConnection('default', 'default')->query("SELECT filename FROM {registry} WHERE name = :name AND type = :type", array(
+      ':name' => $name,
+      ':type' => $type,
+    ))
+    ->fetchField();
+
+  // Flag that we've run a lookup query and need to update the cache.
+  $cache_update_needed = TRUE;
+
+  // Misses are valuable information worth caching, so cache even if
+  // $file is FALSE.
+  $lookup_cache[$cache_key] = $file;
+
+  if ($file) {
+    require_once DRUPAL_ROOT . '/' . $file;
+    return TRUE;
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Rescans all enabled modules and rebuilds the registry.
+ *
+ * Rescans all code in modules or includes directories, storing the location of
+ * each interface or class in the database.
+ */
+function registry_rebuild() {
+  system_rebuild_module_data();
+  registry_update();
+}
+
+/**
+ * Updates the registry based on the latest files listed in the database.
+ *
+ * This function should be used when system_rebuild_module_data() does not need
+ * to be called, because it is already known that the list of files in the
+ * {system} table matches those in the file system.
+ *
+ * @return
+ *   TRUE if the registry was rebuilt, FALSE if another thread was rebuilding
+ *   in parallel and the current thread just waited for completion.
+ *
+ * @see registry_rebuild()
+ */
+function registry_update() {
+  // install_system_module() calls module_enable() which calls into this
+  // function during initial system installation, so the lock system is neither
+  // loaded nor does its storage exist yet.
+  $in_installer = drupal_installation_attempted();
+  if (!$in_installer && !lock_acquire(__FUNCTION__)) {
+    // Another request got the lock, wait for it to finish.
+    lock_wait(__FUNCTION__);
+    return FALSE;
+  }
+
+  require_once DRUPAL_ROOT . '/includes/registry.inc';
+  _registry_update();
+
+  if (!$in_installer) {
+    lock_release(__FUNCTION__);
+  }
+  return TRUE;
+}
+
+/**
+ * @} End of "addtogroup registry".
+ */
+
+/**
+ * Provides central static variable storage.
+ *
+ * All functions requiring a static variable to persist or cache data within
+ * a single page request are encouraged to use this function unless it is
+ * absolutely certain that the static variable will not need to be reset during
+ * the page request. By centralizing static variable storage through this
+ * function, other functions can rely on a consistent API for resetting any
+ * other function's static variables.
+ *
+ * Example:
+ * @code
+ * function language_list($field = 'language') {
+ *   $languages = &drupal_static(__FUNCTION__);
+ *   if (!isset($languages)) {
+ *     // If this function is being called for the first time after a reset,
+ *     // query the database and execute any other code needed to retrieve
+ *     // information about the supported languages.
+ *     ...
+ *   }
+ *   if (!isset($languages[$field])) {
+ *     // If this function is being called for the first time for a particular
+ *     // index field, then execute code needed to index the information already
+ *     // available in $languages by the desired field.
+ *     ...
+ *   }
+ *   // Subsequent invocations of this function for a particular index field
+ *   // skip the above two code blocks and quickly return the already indexed
+ *   // information.
+ *   return $languages[$field];
+ * }
+ * function locale_translate_overview_screen() {
+ *   // When building the content for the translations overview page, make
+ *   // sure to get completely fresh information about the supported languages.
+ *   drupal_static_reset('language_list');
+ *   ...
+ * }
+ * @endcode
+ *
+ * In a few cases, a function can have certainty that there is no legitimate
+ * use-case for resetting that function's static variable. This is rare,
+ * because when writing a function, it's hard to forecast all the situations in
+ * which it will be used. A guideline is that if a function's static variable
+ * does not depend on any information outside of the function that might change
+ * during a single page request, then it's ok to use the "static" keyword
+ * instead of the drupal_static() function.
+ *
+ * Example:
+ * @code
+ * function actions_do(...) {
+ *   // $stack tracks the number of recursive calls.
+ *   static $stack;
+ *   $stack++;
+ *   if ($stack > variable_get('actions_max_stack', 35)) {
+ *     ...
+ *     return;
+ *   }
+ *   ...
+ *   $stack--;
+ * }
+ * @endcode
+ *
+ * In a few cases, a function needs a resettable static variable, but the
+ * function is called many times (100+) during a single page request, so
+ * every microsecond of execution time that can be removed from the function
+ * counts. These functions can use a more cumbersome, but faster variant of
+ * calling drupal_static(). It works by storing the reference returned by
+ * drupal_static() in the calling function's own static variable, thereby
+ * removing the need to call drupal_static() for each iteration of the function.
+ * Conceptually, it replaces:
+ * @code
+ * $foo = &drupal_static(__FUNCTION__);
+ * @endcode
+ * with:
+ * @code
+ * // Unfortunately, this does not work.
+ * static $foo = &drupal_static(__FUNCTION__);
+ * @endcode
+ * However, the above line of code does not work, because PHP only allows static
+ * variables to be initializied by literal values, and does not allow static
+ * variables to be assigned to references.
+ * - http://php.net/manual/en/language.variables.scope.php#language.variables.scope.static
+ * - http://php.net/manual/en/language.variables.scope.php#language.variables.scope.references
+ * The example below shows the syntax needed to work around both limitations.
+ * For benchmarks and more information, see http://drupal.org/node/619666.
+ *
+ * Example:
+ * @code
+ * function user_access($string, $account = NULL) {
+ *   // Use the advanced drupal_static() pattern, since this is called very often.
+ *   static $drupal_static_fast;
+ *   if (!isset($drupal_static_fast)) {
+ *     $drupal_static_fast['perm'] = &drupal_static(__FUNCTION__);
+ *   }
+ *   $perm = &$drupal_static_fast['perm'];
+ *   ...
+ * }
+ * @endcode
+ *
+ * @param $name
+ *   Globally unique name for the variable. For a function with only one static,
+ *   variable, the function name (e.g. via the PHP magic __FUNCTION__ constant)
+ *   is recommended. For a function with multiple static variables add a
+ *   distinguishing suffix to the function name for each one.
+ * @param $default_value
+ *   Optional default value.
+ * @param $reset
+ *   TRUE to reset a specific named variable, or all variables if $name is NULL.
+ *   Resetting every variable should only be used, for example, for running
+ *   unit tests with a clean environment. Should be used only though via
+ *   function drupal_static_reset() and the return value should not be used in
+ *   this case.
+ *
+ * @return
+ *   Returns a variable by reference.
+ *
+ * @see drupal_static_reset()
+ */
+function &drupal_static($name, $default_value = NULL, $reset = FALSE) {
+  static $data = array(), $default = array();
+  // First check if dealing with a previously defined static variable.
+  if (isset($data[$name]) || array_key_exists($name, $data)) {
+    // Non-NULL $name and both $data[$name] and $default[$name] statics exist.
+    if ($reset) {
+      // Reset pre-existing static variable to its default value.
+      $data[$name] = $default[$name];
+    }
+    return $data[$name];
+  }
+  // Neither $data[$name] nor $default[$name] static variables exist.
+  if (isset($name)) {
+    if ($reset) {
+      // Reset was called before a default is set and yet a variable must be
+      // returned.
+      return $data;
+    }
+    // First call with new non-NULL $name. Initialize a new static variable.
+    $default[$name] = $data[$name] = $default_value;
+    return $data[$name];
+  }
+  // Reset all: ($name == NULL). This needs to be done one at a time so that
+  // references returned by earlier invocations of drupal_static() also get
+  // reset.
+  foreach ($default as $name => $value) {
+    $data[$name] = $value;
+  }
+  // As the function returns a reference, the return should always be a
+  // variable.
+  return $data;
+}
+
+/**
+ * Resets one or all centrally stored static variable(s).
+ *
+ * @param $name
+ *   Name of the static variable to reset. Omit to reset all variables.
+ */
+function drupal_static_reset($name = NULL) {
+  drupal_static($name, NULL, TRUE);
+}
+
+/**
+ * Detects whether the current script is running in a command-line environment.
+ */
+function drupal_is_cli() {
+  return (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0)));
+}
+
+/**
+ * Formats text for emphasized display in a placeholder inside a sentence.
+ *
+ * Used automatically by format_string().
+ *
+ * @param $text
+ *   The text to format (plain-text).
+ *
+ * @return
+ *   The formatted text (html).
+ */
+function drupal_placeholder($text) {
+  return '<em class="placeholder">' . check_plain($text) . '</em>';
+}
+
+/**
+ * Registers a function for execution on shutdown.
+ *
+ * Wrapper for register_shutdown_function() that catches thrown exceptions to
+ * avoid "Exception thrown without a stack frame in Unknown".
+ *
+ * @param $callback
+ *   The shutdown function to register.
+ * @param ...
+ *   Additional arguments to pass to the shutdown function.
+ *
+ * @return
+ *   Array of shutdown functions to be executed.
+ *
+ * @see register_shutdown_function()
+ * @ingroup php_wrappers
+ */
+function &drupal_register_shutdown_function($callback = NULL) {
+  // We cannot use drupal_static() here because the static cache is reset during
+  // batch processing, which breaks batch handling.
+  static $callbacks = array();
+
+  if (isset($callback)) {
+    // Only register the internal shutdown function once.
+    if (empty($callbacks)) {
+      register_shutdown_function('_drupal_shutdown_function');
+    }
+    $args = func_get_args();
+    array_shift($args);
+    // Save callback and arguments
+    $callbacks[] = array('callback' => $callback, 'arguments' => $args);
+  }
+  return $callbacks;
+}
+
+/**
+ * Executes registered shutdown functions.
+ */
+function _drupal_shutdown_function() {
+  $callbacks = &drupal_register_shutdown_function();
+
+  // Set the CWD to DRUPAL_ROOT as it is not guaranteed to be the same as it
+  // was in the normal context of execution.
+  chdir(DRUPAL_ROOT);
+
+  try {
+    while (list($key, $callback) = each($callbacks)) {
+      call_user_func_array($callback['callback'], $callback['arguments']);
+    }
+  }
+  catch (Exception $exception) {
+    // If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
+   require_once DRUPAL_ROOT . '/includes/errors.inc';
+   if (error_displayable()) {
+      print '<h1>Uncaught exception thrown in shutdown function.</h1>';
+      print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
+    }
+  }
+}

+ 74 - 0
includes/cache-install.inc

@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Provides a stub cache implementation to be used during installation.
+ */
+
+/**
+ * Defines a stub cache implementation to be used during installation.
+ *
+ * The stub implementation is needed when database access is not yet available.
+ * Because Drupal's caching system never requires that cached data be present,
+ * these stub functions can short-circuit the process and sidestep the need for
+ * any persistent storage. Obviously, using this cache implementation during
+ * normal operations would have a negative impact on performance.
+ */
+class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterface {
+
+  /**
+   * Overrides DrupalDatabaseCache::get().
+   */
+  function get($cid) {
+    return FALSE;
+  }
+
+  /**
+   * Overrides DrupalDatabaseCache::getMultiple().
+   */
+  function getMultiple(&$cids) {
+    return array();
+  }
+
+  /**
+   * Overrides DrupalDatabaseCache::set().
+   */
+  function set($cid, $data, $expire = CACHE_PERMANENT) {
+  }
+
+  /**
+   * Overrides DrupalDatabaseCache::clear().
+   */
+  function clear($cid = NULL, $wildcard = FALSE) {
+    // If there is a database cache, attempt to clear it whenever possible. The
+    // reason for doing this is that the database cache can accumulate data
+    // during installation due to any full bootstraps that may occur at the
+    // same time (for example, Ajax requests triggered by the installer). If we
+    // didn't try to clear it whenever this function is called, the data in the
+    // cache would become stale; for example, the installer sometimes calls
+    // variable_set(), which updates the {variable} table and then clears the
+    // cache to make sure that the next page request picks up the new value.
+    // Not actually clearing the cache here therefore leads old variables to be
+    // loaded on the first page requests after installation, which can cause
+    // subtle bugs, some of which would not be fixed unless the site
+    // administrator cleared the cache manually.
+    try {
+      if (class_exists('Database')) {
+        parent::clear($cid, $wildcard);
+      }
+    }
+    // If the attempt at clearing the cache causes an error, that means that
+    // either the database connection is not set up yet or the relevant cache
+    // table in the database has not yet been created, so we can safely do
+    // nothing here.
+    catch (Exception $e) {
+    }
+  }
+
+  /**
+   * Overrides DrupalDatabaseCache::isEmpty().
+   */
+  function isEmpty() {
+    return TRUE;
+  }
+}

+ 545 - 0
includes/cache.inc

@@ -0,0 +1,545 @@
+<?php
+
+/**
+ * @file
+ * Functions and interfaces for cache handling.
+ */
+
+/**
+ * Gets the cache object for a cache bin.
+ *
+ * By default, this returns an instance of the DrupalDatabaseCache class.
+ * Classes implementing DrupalCacheInterface can register themselves both as a
+ * default implementation and for specific bins.
+ *
+ * @param $bin
+ *   The cache bin for which the cache object should be returned.
+ * @return DrupalCacheInterface
+ *   The cache object associated with the specified bin.
+ *
+ * @see DrupalCacheInterface
+ */
+function _cache_get_object($bin) {
+  // We do not use drupal_static() here because we do not want to change the
+  // storage of a cache bin mid-request.
+  static $cache_objects;
+  if (!isset($cache_objects[$bin])) {
+    $class = variable_get('cache_class_' . $bin);
+    if (!isset($class)) {
+      $class = variable_get('cache_default_class', 'DrupalDatabaseCache');
+    }
+    $cache_objects[$bin] = new $class($bin);
+  }
+  return $cache_objects[$bin];
+}
+
+/**
+ * Returns data from the persistent cache.
+ *
+ * Data may be stored as either plain text or as serialized data. cache_get
+ * will automatically return unserialized objects and arrays.
+ *
+ * @param $cid
+ *   The cache ID of the data to retrieve.
+ * @param $bin
+ *   The cache bin to store the data in. Valid core values are 'cache_block',
+ *   'cache_bootstrap', 'cache_field', 'cache_filter', 'cache_form',
+ *   'cache_menu', 'cache_page', 'cache_path', 'cache_update' or 'cache' for
+ *   the default cache.
+ *
+ * @return
+ *   The cache or FALSE on failure.
+ *
+ * @see cache_set()
+ */
+function cache_get($cid, $bin = 'cache') {
+  return _cache_get_object($bin)->get($cid);
+}
+
+/**
+ * Returns data from the persistent cache when given an array of cache IDs.
+ *
+ * @param $cids
+ *   An array of cache IDs for the data to retrieve. This is passed by
+ *   reference, and will have the IDs successfully returned from cache removed.
+ * @param $bin
+ *   The cache bin where the data is stored.
+ *
+ * @return
+ *   An array of the items successfully returned from cache indexed by cid.
+ */
+function cache_get_multiple(array &$cids, $bin = 'cache') {
+  return _cache_get_object($bin)->getMultiple($cids);
+}
+
+/**
+ * Stores data in the persistent cache.
+ *
+ * The persistent cache is split up into several cache bins. In the default
+ * cache implementation, each cache bin corresponds to a database table by the
+ * same name. Other implementations might want to store several bins in data
+ * structures that get flushed together. While it is not a problem for most
+ * cache bins if the entries in them are flushed before their expire time, some
+ * might break functionality or are extremely expensive to recalculate. The
+ * other bins are expired automatically by core. Contributed modules can add
+ * additional bins and get them expired automatically by implementing
+ * hook_flush_caches().
+ *
+ * The reasons for having several bins are as follows:
+ * - Smaller bins mean smaller database tables and allow for faster selects and
+ *   inserts.
+ * - We try to put fast changing cache items and rather static ones into
+ *   different bins. The effect is that only the fast changing bins will need a
+ *   lot of writes to disk. The more static bins will also be better cacheable
+ *   with MySQL's query cache.
+ *
+ * @param $cid
+ *   The cache ID of the data to store.
+ * @param $data
+ *   The data to store in the cache. Complex data types will be automatically
+ *   serialized before insertion. Strings will be stored as plain text and are
+ *   not serialized.
+ * @param $bin
+ *   The cache bin to store the data in. Valid core values are:
+ *   - cache: (default) Generic cache storage bin (used for theme registry,
+ *     locale date, list of simpletest tests, etc.).
+ *   - cache_block: Stores the content of various blocks.
+ *   - cache_bootstrap: Stores the class registry, the system list of modules,
+ *     the list of which modules implement which hooks, and the Drupal variable
+ *     list.
+ *   - cache_field: Stores the field data belonging to a given object.
+ *   - cache_filter: Stores filtered pieces of content.
+ *   - cache_form: Stores multistep forms. Flushing this bin means that some
+ *     forms displayed to users lose their state and the data already submitted
+ *     to them. This bin should not be flushed before its expired time.
+ *   - cache_menu: Stores the structure of visible navigation menus per page.
+ *   - cache_page: Stores generated pages for anonymous users. It is flushed
+ *     very often, whenever a page changes, at least for every node and comment
+ *     submission. This is the only bin affected by the page cache setting on
+ *     the administrator panel.
+ *   - cache_path: Stores the system paths that have an alias.
+ * @param $expire
+ *   One of the following values:
+ *   - CACHE_PERMANENT: Indicates that the item should never be removed unless
+ *     explicitly told to using cache_clear_all() with a cache ID.
+ *   - CACHE_TEMPORARY: Indicates that the item should be removed at the next
+ *     general cache wipe.
+ *   - A Unix timestamp: Indicates that the item should be kept at least until
+ *     the given time, after which it behaves like CACHE_TEMPORARY.
+ *
+ * @see _update_cache_set()
+ * @see cache_get()
+ */
+function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
+  return _cache_get_object($bin)->set($cid, $data, $expire);
+}
+
+/**
+ * Expires data from the cache.
+ *
+ * If called without arguments, expirable entries will be cleared from the
+ * cache_page and cache_block bins.
+ *
+ * @param $cid
+ *   If set, the cache ID to delete. Otherwise, all cache entries that can
+ *   expire are deleted.
+ * @param $bin
+ *   If set, the cache bin to delete from. Mandatory argument if $cid is set.
+ * @param $wildcard
+ *   If TRUE, cache IDs starting with $cid are deleted in addition to the
+ *   exact cache ID specified by $cid. If $wildcard is TRUE and $cid is '*',
+ *   the entire cache bin is emptied.
+ */
+function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
+  if (!isset($cid) && !isset($bin)) {
+    // Clear the block cache first, so stale data will
+    // not end up in the page cache.
+    if (module_exists('block')) {
+      cache_clear_all(NULL, 'cache_block');
+    }
+    cache_clear_all(NULL, 'cache_page');
+    return;
+  }
+  return _cache_get_object($bin)->clear($cid, $wildcard);
+}
+
+/**
+ * Checks if a cache bin is empty.
+ *
+ * A cache bin is considered empty if it does not contain any valid data for any
+ * cache ID.
+ *
+ * @param $bin
+ *   The cache bin to check.
+ *
+ * @return
+ *   TRUE if the cache bin specified is empty.
+ */
+function cache_is_empty($bin) {
+  return _cache_get_object($bin)->isEmpty();
+}
+
+/**
+ * Defines an interface for cache implementations.
+ *
+ * All cache implementations have to implement this interface.
+ * DrupalDatabaseCache provides the default implementation, which can be
+ * consulted as an example.
+ *
+ * To make Drupal use your implementation for a certain cache bin, you have to
+ * set a variable with the name of the cache bin as its key and the name of
+ * your class as its value. For example, if your implementation of
+ * DrupalCacheInterface was called MyCustomCache, the following line would make
+ * Drupal use it for the 'cache_page' bin:
+ * @code
+ *  variable_set('cache_class_cache_page', 'MyCustomCache');
+ * @endcode
+ *
+ * Additionally, you can register your cache implementation to be used by
+ * default for all cache bins by setting the variable 'cache_default_class' to
+ * the name of your implementation of the DrupalCacheInterface, e.g.
+ * @code
+ *  variable_set('cache_default_class', 'MyCustomCache');
+ * @endcode
+ *
+ * To implement a completely custom cache bin, use the same variable format:
+ * @code
+ *  variable_set('cache_class_custom_bin', 'MyCustomCache');
+ * @endcode
+ * To access your custom cache bin, specify the name of the bin when storing
+ * or retrieving cached data:
+ * @code
+ *  cache_set($cid, $data, 'custom_bin', $expire);
+ *  cache_get($cid, 'custom_bin');
+ * @endcode
+ *
+ * @see _cache_get_object()
+ * @see DrupalDatabaseCache
+ */
+interface DrupalCacheInterface {
+  /**
+   * Constructs a new cache interface.
+   *
+   * @param $bin
+   *   The cache bin for which the object is created.
+   */
+  function __construct($bin);
+
+  /**
+   * Returns data from the persistent cache.
+   *
+   * Data may be stored as either plain text or as serialized data. cache_get()
+   * will automatically return unserialized objects and arrays.
+   *
+   * @param $cid
+   *   The cache ID of the data to retrieve.
+   *
+   * @return
+   *   The cache or FALSE on failure.
+   */
+  function get($cid);
+
+  /**
+   * Returns data from the persistent cache when given an array of cache IDs.
+   *
+   * @param $cids
+   *   An array of cache IDs for the data to retrieve. This is passed by
+   *   reference, and will have the IDs successfully returned from cache
+   *   removed.
+   *
+   * @return
+   *   An array of the items successfully returned from cache indexed by cid.
+   */
+   function getMultiple(&$cids);
+
+  /**
+   * Stores data in the persistent cache.
+   *
+   * @param $cid
+   *   The cache ID of the data to store.
+   * @param $data
+   *   The data to store in the cache. Complex data types will be automatically
+   *   serialized before insertion.
+   *   Strings will be stored as plain text and not serialized.
+   * @param $expire
+   *   One of the following values:
+   *   - CACHE_PERMANENT: Indicates that the item should never be removed unless
+   *     explicitly told to using cache_clear_all() with a cache ID.
+   *   - CACHE_TEMPORARY: Indicates that the item should be removed at the next
+   *     general cache wipe.
+   *   - A Unix timestamp: Indicates that the item should be kept at least until
+   *     the given time, after which it behaves like CACHE_TEMPORARY.
+   */
+  function set($cid, $data, $expire = CACHE_PERMANENT);
+
+
+  /**
+   * Expires data from the cache.
+   *
+   * If called without arguments, expirable entries will be cleared from the
+   * cache_page and cache_block bins.
+   *
+   * @param $cid
+   *   If set, the cache ID to delete. Otherwise, all cache entries that can
+   *   expire are deleted.
+   * @param $wildcard
+   *   If set to TRUE, the $cid is treated as a substring
+   *   to match rather than a complete ID. The match is a right hand
+   *   match. If '*' is given as $cid, the bin $bin will be emptied.
+   */
+  function clear($cid = NULL, $wildcard = FALSE);
+
+  /**
+   * Checks if a cache bin is empty.
+   *
+   * A cache bin is considered empty if it does not contain any valid data for
+   * any cache ID.
+   *
+   * @return
+   *   TRUE if the cache bin specified is empty.
+   */
+  function isEmpty();
+}
+
+/**
+ * Defines a default cache implementation.
+ *
+ * This is Drupal's default cache implementation. It uses the database to store
+ * cached data. Each cache bin corresponds to a database table by the same name.
+ */
+class DrupalDatabaseCache implements DrupalCacheInterface {
+  protected $bin;
+
+  /**
+   * Constructs a new DrupalDatabaseCache object.
+   */
+  function __construct($bin) {
+    $this->bin = $bin;
+  }
+
+  /**
+   * Implements DrupalCacheInterface::get().
+   */
+  function get($cid) {
+    $cids = array($cid);
+    $cache = $this->getMultiple($cids);
+    return reset($cache);
+  }
+
+  /**
+   * Implements DrupalCacheInterface::getMultiple().
+   */
+  function getMultiple(&$cids) {
+    try {
+      // Garbage collection necessary when enforcing a minimum cache lifetime.
+      $this->garbageCollection($this->bin);
+
+      // When serving cached pages, the overhead of using db_select() was found
+      // to add around 30% overhead to the request. Since $this->bin is a
+      // variable, this means the call to db_query() here uses a concatenated
+      // string. This is highly discouraged under any other circumstances, and
+      // is used here only due to the performance overhead we would incur
+      // otherwise. When serving an uncached page, the overhead of using
+      // db_select() is a much smaller proportion of the request.
+      $result = db_query('SELECT cid, data, created, expire, serialized FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
+      $cache = array();
+      foreach ($result as $item) {
+        $item = $this->prepareItem($item);
+        if ($item) {
+          $cache[$item->cid] = $item;
+        }
+      }
+      $cids = array_diff($cids, array_keys($cache));
+      return $cache;
+    }
+    catch (Exception $e) {
+      // If the database is never going to be available, cache requests should
+      // return FALSE in order to allow exception handling to occur.
+      return array();
+    }
+  }
+
+  /**
+   * Garbage collection for get() and getMultiple().
+   *
+   * @param $bin
+   *   The bin being requested.
+   */
+  protected function garbageCollection() {
+    $cache_lifetime = variable_get('cache_lifetime', 0);
+
+    // Clean-up the per-user cache expiration session data, so that the session
+    // handler can properly clean-up the session data for anonymous users.
+    if (isset($_SESSION['cache_expiration'])) {
+      $expire = REQUEST_TIME - $cache_lifetime;
+      foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) {
+        if ($timestamp < $expire) {
+          unset($_SESSION['cache_expiration'][$bin]);
+        }
+      }
+      if (!$_SESSION['cache_expiration']) {
+        unset($_SESSION['cache_expiration']);
+      }
+    }
+
+    // Garbage collection of temporary items is only necessary when enforcing
+    // a minimum cache lifetime.
+    if (!$cache_lifetime) {
+      return;
+    }
+    // When cache lifetime is in force, avoid running garbage collection too
+    // often since this will remove temporary cache items indiscriminately.
+    $cache_flush = variable_get('cache_flush_' . $this->bin, 0);
+    if ($cache_flush && ($cache_flush + $cache_lifetime <= REQUEST_TIME)) {
+      // Reset the variable immediately to prevent a meltdown in heavy load situations.
+      variable_set('cache_flush_' . $this->bin, 0);
+      // Time to flush old cache data
+      db_delete($this->bin)
+        ->condition('expire', CACHE_PERMANENT, '<>')
+        ->condition('expire', $cache_flush, '<=')
+        ->execute();
+    }
+  }
+
+  /**
+   * Prepares a cached item.
+   *
+   * Checks that items are either permanent or did not expire, and unserializes
+   * data as appropriate.
+   *
+   * @param $cache
+   *   An item loaded from cache_get() or cache_get_multiple().
+   *
+   * @return
+   *   The item with data unserialized as appropriate or FALSE if there is no
+   *   valid item to load.
+   */
+  protected function prepareItem($cache) {
+    global $user;
+
+    if (!isset($cache->data)) {
+      return FALSE;
+    }
+    // If the cached data is temporary and subject to a per-user minimum
+    // lifetime, compare the cache entry timestamp with the user session
+    // cache_expiration timestamp. If the cache entry is too old, ignore it.
+    if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && isset($_SESSION['cache_expiration'][$this->bin]) && $_SESSION['cache_expiration'][$this->bin] > $cache->created) {
+      // Ignore cache data that is too old and thus not valid for this user.
+      return FALSE;
+    }
+
+    // If the data is permanent or not subject to a minimum cache lifetime,
+    // unserialize and return the cached data.
+    if ($cache->serialized) {
+      $cache->data = unserialize($cache->data);
+    }
+
+    return $cache;
+  }
+
+  /**
+   * Implements DrupalCacheInterface::set().
+   */
+  function set($cid, $data, $expire = CACHE_PERMANENT) {
+    $fields = array(
+      'serialized' => 0,
+      'created' => REQUEST_TIME,
+      'expire' => $expire,
+    );
+    if (!is_string($data)) {
+      $fields['data'] = serialize($data);
+      $fields['serialized'] = 1;
+    }
+    else {
+      $fields['data'] = $data;
+      $fields['serialized'] = 0;
+    }
+
+    try {
+      db_merge($this->bin)
+        ->key(array('cid' => $cid))
+        ->fields($fields)
+        ->execute();
+    }
+    catch (Exception $e) {
+      // The database may not be available, so we'll ignore cache_set requests.
+    }
+  }
+
+  /**
+   * Implements DrupalCacheInterface::clear().
+   */
+  function clear($cid = NULL, $wildcard = FALSE) {
+    global $user;
+
+    if (empty($cid)) {
+      if (variable_get('cache_lifetime', 0)) {
+        // We store the time in the current user's session. We then simulate
+        // that the cache was flushed for this user by not returning cached
+        // data that was cached before the timestamp.
+        $_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME;
+
+        $cache_flush = variable_get('cache_flush_' . $this->bin, 0);
+        if ($cache_flush == 0) {
+          // This is the first request to clear the cache, start a timer.
+          variable_set('cache_flush_' . $this->bin, REQUEST_TIME);
+        }
+        elseif (REQUEST_TIME > ($cache_flush + variable_get('cache_lifetime', 0))) {
+          // Clear the cache for everyone, cache_lifetime seconds have
+          // passed since the first request to clear the cache.
+          db_delete($this->bin)
+            ->condition('expire', CACHE_PERMANENT, '<>')
+            ->condition('expire', REQUEST_TIME, '<')
+            ->execute();
+          variable_set('cache_flush_' . $this->bin, 0);
+        }
+      }
+      else {
+        // No minimum cache lifetime, flush all temporary cache entries now.
+        db_delete($this->bin)
+          ->condition('expire', CACHE_PERMANENT, '<>')
+          ->condition('expire', REQUEST_TIME, '<')
+          ->execute();
+      }
+    }
+    else {
+      if ($wildcard) {
+        if ($cid == '*') {
+          db_truncate($this->bin)->execute();
+        }
+        else {
+          db_delete($this->bin)
+            ->condition('cid', db_like($cid) . '%', 'LIKE')
+            ->execute();
+        }
+      }
+      elseif (is_array($cid)) {
+        // Delete in chunks when a large array is passed.
+        do {
+          db_delete($this->bin)
+            ->condition('cid', array_splice($cid, 0, 1000), 'IN')
+            ->execute();
+        }
+        while (count($cid));
+      }
+      else {
+        db_delete($this->bin)
+          ->condition('cid', $cid)
+          ->execute();
+      }
+    }
+  }
+
+  /**
+   * Implements DrupalCacheInterface::isEmpty().
+   */
+  function isEmpty() {
+    $this->garbageCollection();
+    $query = db_select($this->bin);
+    $query->addExpression('1');
+    $result = $query->range(0, 1)
+      ->execute()
+      ->fetchField();
+    return empty($result);
+  }
+}

File diff suppressed because it is too large
+ 1401 - 0
includes/common.inc


+ 3039 - 0
includes/database/database.inc

@@ -0,0 +1,3039 @@
+<?php
+
+/**
+ * @file
+ * Core systems for the database layer.
+ *
+ * Classes required for basic functioning of the database system should be
+ * placed in this file.  All utility functions should also be placed in this
+ * file only, as they cannot auto-load the way classes can.
+ */
+
+/**
+ * @defgroup database Database abstraction layer
+ * @{
+ * Allow the use of different database servers using the same code base.
+ *
+ * Drupal provides a database abstraction layer to provide developers with
+ * the ability to support multiple database servers easily. The intent of
+ * this layer is to preserve the syntax and power of SQL as much as possible,
+ * but also allow developers a way to leverage more complex functionality in
+ * a unified way. It also provides a structured interface for dynamically
+ * constructing queries when appropriate, and enforcing security checks and
+ * similar good practices.
+ *
+ * The system is built atop PHP's PDO (PHP Data Objects) database API and
+ * inherits much of its syntax and semantics.
+ *
+ * Most Drupal database SELECT queries are performed by a call to db_query() or
+ * db_query_range(). Module authors should also consider using the PagerDefault
+ * Extender for queries that return results that need to be presented on
+ * multiple pages, and the Tablesort Extender for generating appropriate queries
+ * for sortable tables.
+ *
+ * For example, one might wish to return a list of the most recent 10 nodes
+ * authored by a given user. Instead of directly issuing the SQL query
+ * @code
+ * SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid LIMIT 0, 10;
+ * @endcode
+ * one would instead call the Drupal functions:
+ * @code
+ * $result = db_query_range('SELECT n.nid, n.title, n.created
+ *   FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid));
+ * foreach ($result as $record) {
+ *   // Perform operations on $record->title, etc. here.
+ * }
+ * @endcode
+ * Curly braces are used around "node" to provide table prefixing via
+ * DatabaseConnection::prefixTables(). The explicit use of a user ID is pulled
+ * out into an argument passed to db_query() so that SQL injection attacks
+ * from user input can be caught and nullified. The LIMIT syntax varies between
+ * database servers, so that is abstracted into db_query_range() arguments.
+ * Finally, note the PDO-based ability to iterate over the result set using
+ * foreach ().
+ *
+ * All queries are passed as a prepared statement string. A
+ * prepared statement is a "template" of a query that omits literal or variable
+ * values in favor of placeholders. The values to place into those
+ * placeholders are passed separately, and the database driver handles
+ * inserting the values into the query in a secure fashion. That means you
+ * should never quote or string-escape a value to be inserted into the query.
+ *
+ * There are two formats for placeholders: named and unnamed. Named placeholders
+ * are strongly preferred in all cases as they are more flexible and
+ * self-documenting. Named placeholders should start with a colon ":" and can be
+ * followed by one or more letters, numbers or underscores.
+ *
+ * Named placeholders begin with a colon followed by a unique string. Example:
+ * @code
+ * SELECT nid, title FROM {node} WHERE uid=:uid;
+ * @endcode
+ *
+ * ":uid" is a placeholder that will be replaced with a literal value when
+ * the query is executed. A given placeholder label cannot be repeated in a
+ * given query, even if the value should be the same. When using named
+ * placeholders, the array of arguments to the query must be an associative
+ * array where keys are a placeholder label (e.g., :uid) and the value is the
+ * corresponding value to use. The array may be in any order.
+ *
+ * Unnamed placeholders are simply a question mark. Example:
+ * @code
+ * SELECT nid, title FROM {node} WHERE uid=?;
+ * @endcode
+ *
+ * In this case, the array of arguments must be an indexed array of values to
+ * use in the exact same order as the placeholders in the query.
+ *
+ * Note that placeholders should be a "complete" value. For example, when
+ * running a LIKE query the SQL wildcard character, %, should be part of the
+ * value, not the query itself. Thus, the following is incorrect:
+ * @code
+ * SELECT nid, title FROM {node} WHERE title LIKE :title%;
+ * @endcode
+ * It should instead read:
+ * @code
+ * SELECT nid, title FROM {node} WHERE title LIKE :title;
+ * @endcode
+ * and the value for :title should include a % as appropriate. Again, note the
+ * lack of quotation marks around :title. Because the value is not inserted
+ * into the query as one big string but as an explicitly separate value, the
+ * database server knows where the query ends and a value begins. That is
+ * considerably more secure against SQL injection than trying to remember
+ * which values need quotation marks and string escaping and which don't.
+ *
+ * INSERT, UPDATE, and DELETE queries need special care in order to behave
+ * consistently across all different databases. Therefore, they use a special
+ * object-oriented API for defining a query structurally. For example, rather
+ * than:
+ * @code
+ * INSERT INTO node (nid, title, body) VALUES (1, 'my title', 'my body');
+ * @endcode
+ * one would instead write:
+ * @code
+ * $fields = array('nid' => 1, 'title' => 'my title', 'body' => 'my body');
+ * db_insert('node')->fields($fields)->execute();
+ * @endcode
+ * This method allows databases that need special data type handling to do so,
+ * while also allowing optimizations such as multi-insert queries. UPDATE and
+ * DELETE queries have a similar pattern.
+ *
+ * Drupal also supports transactions, including a transparent fallback for
+ * databases that do not support transactions. To start a new transaction,
+ * simply call $txn = db_transaction(); in your own code. The transaction will
+ * remain open for as long as the variable $txn remains in scope.  When $txn is
+ * destroyed, the transaction will be committed.  If your transaction is nested
+ * inside of another then Drupal will track each transaction and only commit
+ * the outer-most transaction when the last transaction object goes out out of
+ * scope, that is, all relevant queries completed successfully.
+ *
+ * Example:
+ * @code
+ * function my_transaction_function() {
+ *   // The transaction opens here.
+ *   $txn = db_transaction();
+ *
+ *   try {
+ *     $id = db_insert('example')
+ *       ->fields(array(
+ *         'field1' => 'mystring',
+ *         'field2' => 5,
+ *       ))
+ *       ->execute();
+ *
+ *     my_other_function($id);
+ *
+ *     return $id;
+ *   }
+ *   catch (Exception $e) {
+ *     // Something went wrong somewhere, so roll back now.
+ *     $txn->rollback();
+ *     // Log the exception to watchdog.
+ *     watchdog_exception('type', $e);
+ *   }
+ *
+ *   // $txn goes out of scope here.  Unless the transaction was rolled back, it
+ *   // gets automatically committed here.
+ * }
+ *
+ * function my_other_function($id) {
+ *   // The transaction is still open here.
+ *
+ *   if ($id % 2 == 0) {
+ *     db_update('example')
+ *       ->condition('id', $id)
+ *       ->fields(array('field2' => 10))
+ *       ->execute();
+ *   }
+ * }
+ * @endcode
+ *
+ * @see http://drupal.org/developing/api/database
+ */
+
+
+/**
+ * Base Database API class.
+ *
+ * This class provides a Drupal-specific extension of the PDO database
+ * abstraction class in PHP. Every database driver implementation must provide a
+ * concrete implementation of it to support special handling required by that
+ * database.
+ *
+ * @see http://php.net/manual/en/book.pdo.php
+ */
+abstract class DatabaseConnection extends PDO {
+
+  /**
+   * The database target this connection is for.
+   *
+   * We need this information for later auditing and logging.
+   *
+   * @var string
+   */
+  protected $target = NULL;
+
+  /**
+   * The key representing this connection.
+   *
+   * The key is a unique string which identifies a database connection. A
+   * connection can be a single server or a cluster of master and slaves (use
+   * target to pick between master and slave).
+   *
+   * @var string
+   */
+  protected $key = NULL;
+
+  /**
+   * The current database logging object for this connection.
+   *
+   * @var DatabaseLog
+   */
+  protected $logger = NULL;
+
+  /**
+   * Tracks the number of "layers" of transactions currently active.
+   *
+   * On many databases transactions cannot nest.  Instead, we track
+   * nested calls to transactions and collapse them into a single
+   * transaction.
+   *
+   * @var array
+   */
+  protected $transactionLayers = array();
+
+  /**
+   * Index of what driver-specific class to use for various operations.
+   *
+   * @var array
+   */
+  protected $driverClasses = array();
+
+  /**
+   * The name of the Statement class for this connection.
+   *
+   * @var string
+   */
+  protected $statementClass = 'DatabaseStatementBase';
+
+  /**
+   * Whether this database connection supports transactions.
+   *
+   * @var bool
+   */
+  protected $transactionSupport = TRUE;
+
+  /**
+   * Whether this database connection supports transactional DDL.
+   *
+   * Set to FALSE by default because few databases support this feature.
+   *
+   * @var bool
+   */
+  protected $transactionalDDLSupport = FALSE;
+
+  /**
+   * An index used to generate unique temporary table names.
+   *
+   * @var integer
+   */
+  protected $temporaryNameIndex = 0;
+
+  /**
+   * The connection information for this connection object.
+   *
+   * @var array
+   */
+  protected $connectionOptions = array();
+
+  /**
+   * The schema object for this connection.
+   *
+   * @var object
+   */
+  protected $schema = NULL;
+
+  /**
+   * The prefixes used by this database connection.
+   *
+   * @var array
+   */
+  protected $prefixes = array();
+
+  /**
+   * List of search values for use in prefixTables().
+   *
+   * @var array
+   */
+  protected $prefixSearch = array();
+
+  /**
+   * List of replacement values for use in prefixTables().
+   *
+   * @var array
+   */
+  protected $prefixReplace = array();
+
+  function __construct($dsn, $username, $password, $driver_options = array()) {
+    // Initialize and prepare the connection prefix.
+    $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
+
+    // Because the other methods don't seem to work right.
+    $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
+
+    // Call PDO::__construct and PDO::setAttribute.
+    parent::__construct($dsn, $username, $password, $driver_options);
+
+    // Set a Statement class, unless the driver opted out.
+    if (!empty($this->statementClass)) {
+      $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
+    }
+  }
+
+  /**
+   * Destroys this Connection object.
+   *
+   * PHP does not destruct an object if it is still referenced in other
+   * variables. In case of PDO database connection objects, PHP only closes the
+   * connection when the PDO object is destructed, so any references to this
+   * object may cause the number of maximum allowed connections to be exceeded.
+   */
+  public function destroy() {
+    // Destroy all references to this connection by setting them to NULL.
+    // The Statement class attribute only accepts a new value that presents a
+    // proper callable, so we reset it to PDOStatement.
+    $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
+    $this->schema = NULL;
+  }
+
+  /**
+   * Returns the default query options for any given query.
+   *
+   * A given query can be customized with a number of option flags in an
+   * associative array:
+   * - target: The database "target" against which to execute a query. Valid
+   *   values are "default" or "slave". The system will first try to open a
+   *   connection to a database specified with the user-supplied key. If one
+   *   is not available, it will silently fall back to the "default" target.
+   *   If multiple databases connections are specified with the same target,
+   *   one will be selected at random for the duration of the request.
+   * - fetch: This element controls how rows from a result set will be
+   *   returned. Legal values include PDO::FETCH_ASSOC, PDO::FETCH_BOTH,
+   *   PDO::FETCH_OBJ, PDO::FETCH_NUM, or a string representing the name of a
+   *   class. If a string is specified, each record will be fetched into a new
+   *   object of that class. The behavior of all other values is defined by PDO.
+   *   See http://php.net/manual/pdostatement.fetch.php
+   * - return: Depending on the type of query, different return values may be
+   *   meaningful. This directive instructs the system which type of return
+   *   value is desired. The system will generally set the correct value
+   *   automatically, so it is extremely rare that a module developer will ever
+   *   need to specify this value. Setting it incorrectly will likely lead to
+   *   unpredictable results or fatal errors. Legal values include:
+   *   - Database::RETURN_STATEMENT: Return the prepared statement object for
+   *     the query. This is usually only meaningful for SELECT queries, where
+   *     the statement object is how one accesses the result set returned by the
+   *     query.
+   *   - Database::RETURN_AFFECTED: Return the number of rows affected by an
+   *     UPDATE or DELETE query. Be aware that means the number of rows actually
+   *     changed, not the number of rows matched by the WHERE clause.
+   *   - Database::RETURN_INSERT_ID: Return the sequence ID (primary key)
+   *     created by an INSERT statement on a table that contains a serial
+   *     column.
+   *   - Database::RETURN_NULL: Do not return anything, as there is no
+   *     meaningful value to return. That is the case for INSERT queries on
+   *     tables that do not contain a serial column.
+   * - throw_exception: By default, the database system will catch any errors
+   *   on a query as an Exception, log it, and then rethrow it so that code
+   *   further up the call chain can take an appropriate action. To suppress
+   *   that behavior and simply return NULL on failure, set this option to
+   *   FALSE.
+   *
+   * @return
+   *   An array of default query options.
+   */
+  protected function defaultOptions() {
+    return array(
+      'target' => 'default',
+      'fetch' => PDO::FETCH_OBJ,
+      'return' => Database::RETURN_STATEMENT,
+      'throw_exception' => TRUE,
+    );
+  }
+
+  /**
+   * Returns the connection information for this connection object.
+   *
+   * Note that Database::getConnectionInfo() is for requesting information
+   * about an arbitrary database connection that is defined. This method
+   * is for requesting the connection information of this specific
+   * open connection object.
+   *
+   * @return
+   *   An array of the connection information. The exact list of
+   *   properties is driver-dependent.
+   */
+  public function getConnectionOptions() {
+    return $this->connectionOptions;
+  }
+
+  /**
+   * Set the list of prefixes used by this database connection.
+   *
+   * @param $prefix
+   *   The prefixes, in any of the multiple forms documented in
+   *   default.settings.php.
+   */
+  protected function setPrefix($prefix) {
+    if (is_array($prefix)) {
+      $this->prefixes = $prefix + array('default' => '');
+    }
+    else {
+      $this->prefixes = array('default' => $prefix);
+    }
+
+    // Set up variables for use in prefixTables(). Replace table-specific
+    // prefixes first.
+    $this->prefixSearch = array();
+    $this->prefixReplace = array();
+    foreach ($this->prefixes as $key => $val) {
+      if ($key != 'default') {
+        $this->prefixSearch[] = '{' . $key . '}';
+        $this->prefixReplace[] = $val . $key;
+      }
+    }
+    // Then replace remaining tables with the default prefix.
+    $this->prefixSearch[] = '{';
+    $this->prefixReplace[] = $this->prefixes['default'];
+    $this->prefixSearch[] = '}';
+    $this->prefixReplace[] = '';
+  }
+
+  /**
+   * Appends a database prefix to all tables in a query.
+   *
+   * Queries sent to Drupal should wrap all table names in curly brackets. This
+   * function searches for this syntax and adds Drupal's table prefix to all
+   * tables, allowing Drupal to coexist with other systems in the same database
+   * and/or schema if necessary.
+   *
+   * @param $sql
+   *   A string containing a partial or entire SQL query.
+   *
+   * @return
+   *   The properly-prefixed string.
+   */
+  public function prefixTables($sql) {
+    return str_replace($this->prefixSearch, $this->prefixReplace, $sql);
+  }
+
+  /**
+   * Find the prefix for a table.
+   *
+   * This function is for when you want to know the prefix of a table. This
+   * is not used in prefixTables due to performance reasons.
+   */
+  public function tablePrefix($table = 'default') {
+    if (isset($this->prefixes[$table])) {
+      return $this->prefixes[$table];
+    }
+    else {
+      return $this->prefixes['default'];
+    }
+  }
+
+  /**
+   * Prepares a query string and returns the prepared statement.
+   *
+   * This method caches prepared statements, reusing them when
+   * possible. It also prefixes tables names enclosed in curly-braces.
+   *
+   * @param $query
+   *   The query string as SQL, with curly-braces surrounding the
+   *   table names.
+   *
+   * @return DatabaseStatementInterface
+   *   A PDO prepared statement ready for its execute() method.
+   */
+  public function prepareQuery($query) {
+    $query = $this->prefixTables($query);
+
+    // Call PDO::prepare.
+    return parent::prepare($query);
+  }
+
+  /**
+   * Tells this connection object what its target value is.
+   *
+   * This is needed for logging and auditing. It's sloppy to do in the
+   * constructor because the constructor for child classes has a different
+   * signature. We therefore also ensure that this function is only ever
+   * called once.
+   *
+   * @param $target
+   *   The target this connection is for. Set to NULL (default) to disable
+   *   logging entirely.
+   */
+  public function setTarget($target = NULL) {
+    if (!isset($this->target)) {
+      $this->target = $target;
+    }
+  }
+
+  /**
+   * Returns the target this connection is associated with.
+   *
+   * @return
+   *   The target string of this connection.
+   */
+  public function getTarget() {
+    return $this->target;
+  }
+
+  /**
+   * Tells this connection object what its key is.
+   *
+   * @param $target
+   *   The key this connection is for.
+   */
+  public function setKey($key) {
+    if (!isset($this->key)) {
+      $this->key = $key;
+    }
+  }
+
+  /**
+   * Returns the key this connection is associated with.
+   *
+   * @return
+   *   The key of this connection.
+   */
+  public function getKey() {
+    return $this->key;
+  }
+
+  /**
+   * Associates a logging object with this connection.
+   *
+   * @param $logger
+   *   The logging object we want to use.
+   */
+  public function setLogger(DatabaseLog $logger) {
+    $this->logger = $logger;
+  }
+
+  /**
+   * Gets the current logging object for this connection.
+   *
+   * @return DatabaseLog
+   *   The current logging object for this connection. If there isn't one,
+   *   NULL is returned.
+   */
+  public function getLogger() {
+    return $this->logger;
+  }
+
+  /**
+   * Creates the appropriate sequence name for a given table and serial field.
+   *
+   * This information is exposed to all database drivers, although it is only
+   * useful on some of them. This method is table prefix-aware.
+   *
+   * @param $table
+   *   The table name to use for the sequence.
+   * @param $field
+   *   The field name to use for the sequence.
+   *
+   * @return
+   *   A table prefix-parsed string for the sequence name.
+   */
+  public function makeSequenceName($table, $field) {
+    return $this->prefixTables('{' . $table . '}_' . $field . '_seq');
+  }
+
+  /**
+   * Flatten an array of query comments into a single comment string.
+   *
+   * The comment string will be sanitized to avoid SQL injection attacks.
+   *
+   * @param $comments
+   *   An array of query comment strings.
+   *
+   * @return
+   *   A sanitized comment string.
+   */
+  public function makeComment($comments) {
+    if (empty($comments))
+      return '';
+
+    // Flatten the array of comments.
+    $comment = implode('; ', $comments);
+
+    // Sanitize the comment string so as to avoid SQL injection attacks.
+    return '/* ' . $this->filterComment($comment) . ' */ ';
+  }
+
+  /**
+   * Sanitize a query comment string.
+   *
+   * Ensure a query comment does not include strings such as "* /" that might
+   * terminate the comment early. This avoids SQL injection attacks via the
+   * query comment. The comment strings in this example are separated by a
+   * space to avoid PHP parse errors.
+   *
+   * For example, the comment:
+   * @code
+   * db_update('example')
+   *  ->condition('id', $id)
+   *  ->fields(array('field2' => 10))
+   *  ->comment('Exploit * / DROP TABLE node; --')
+   *  ->execute()
+   * @endcode
+   *
+   * Would result in the following SQL statement being generated:
+   * @code
+   * "/ * Exploit * / DROP TABLE node; -- * / UPDATE example SET field2=..."
+   * @endcode
+   *
+   * Unless the comment is sanitised first, the SQL server would drop the
+   * node table and ignore the rest of the SQL statement.
+   *
+   * @param $comment
+   *   A query comment string.
+   *
+   * @return
+   *   A sanitized version of the query comment string.
+   */
+  protected function filterComment($comment = '') {
+    return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment);
+  }
+
+  /**
+   * Executes a query string against the database.
+   *
+   * This method provides a central handler for the actual execution of every
+   * query. All queries executed by Drupal are executed as PDO prepared
+   * statements.
+   *
+   * @param $query
+   *   The query to execute. In most cases this will be a string containing
+   *   an SQL query with placeholders. An already-prepared instance of
+   *   DatabaseStatementInterface may also be passed in order to allow calling
+   *   code to manually bind variables to a query. If a
+   *   DatabaseStatementInterface is passed, the $args array will be ignored.
+   *   It is extremely rare that module code will need to pass a statement
+   *   object to this method. It is used primarily for database drivers for
+   *   databases that require special LOB field handling.
+   * @param $args
+   *   An array of arguments for the prepared statement. If the prepared
+   *   statement uses ? placeholders, this array must be an indexed array.
+   *   If it contains named placeholders, it must be an associative array.
+   * @param $options
+   *   An associative array of options to control how the query is run. See
+   *   the documentation for DatabaseConnection::defaultOptions() for details.
+   *
+   * @return DatabaseStatementInterface
+   *   This method will return one of: the executed statement, the number of
+   *   rows affected by the query (not the number matched), or the generated
+   *   insert IT of the last query, depending on the value of
+   *   $options['return']. Typically that value will be set by default or a
+   *   query builder and should not be set by a user. If there is an error,
+   *   this method will return NULL and may throw an exception if
+   *   $options['throw_exception'] is TRUE.
+   *
+   * @throws PDOException
+   */
+  public function query($query, array $args = array(), $options = array()) {
+
+    // Use default values if not already set.
+    $options += $this->defaultOptions();
+
+    try {
+      // We allow either a pre-bound statement object or a literal string.
+      // In either case, we want to end up with an executed statement object,
+      // which we pass to PDOStatement::execute.
+      if ($query instanceof DatabaseStatementInterface) {
+        $stmt = $query;
+        $stmt->execute(NULL, $options);
+      }
+      else {
+        $this->expandArguments($query, $args);
+        $stmt = $this->prepareQuery($query);
+        $stmt->execute($args, $options);
+      }
+
+      // Depending on the type of query we may need to return a different value.
+      // See DatabaseConnection::defaultOptions() for a description of each
+      // value.
+      switch ($options['return']) {
+        case Database::RETURN_STATEMENT:
+          return $stmt;
+        case Database::RETURN_AFFECTED:
+          return $stmt->rowCount();
+        case Database::RETURN_INSERT_ID:
+          return $this->lastInsertId();
+        case Database::RETURN_NULL:
+          return;
+        default:
+          throw new PDOException('Invalid return directive: ' . $options['return']);
+      }
+    }
+    catch (PDOException $e) {
+      if ($options['throw_exception']) {
+        // Add additional debug information.
+        if ($query instanceof DatabaseStatementInterface) {
+          $e->query_string = $stmt->getQueryString();
+        }
+        else {
+          $e->query_string = $query;
+        }
+        $e->args = $args;
+        throw $e;
+      }
+      return NULL;
+    }
+  }
+
+  /**
+   * Expands out shorthand placeholders.
+   *
+   * Drupal supports an alternate syntax for doing arrays of values. We
+   * therefore need to expand them out into a full, executable query string.
+   *
+   * @param $query
+   *   The query string to modify.
+   * @param $args
+   *   The arguments for the query.
+   *
+   * @return
+   *   TRUE if the query was modified, FALSE otherwise.
+   */
+  protected function expandArguments(&$query, &$args) {
+    $modified = FALSE;
+
+    // If the placeholder value to insert is an array, assume that we need
+    // to expand it out into a comma-delimited set of placeholders.
+    foreach (array_filter($args, 'is_array') as $key => $data) {
+      $new_keys = array();
+      foreach ($data as $i => $value) {
+        // This assumes that there are no other placeholders that use the same
+        // name.  For example, if the array placeholder is defined as :example
+        // and there is already an :example_2 placeholder, this will generate
+        // a duplicate key.  We do not account for that as the calling code
+        // is already broken if that happens.
+        $new_keys[$key . '_' . $i] = $value;
+      }
+
+      // Update the query with the new placeholders.
+      // preg_replace is necessary to ensure the replacement does not affect
+      // placeholders that start with the same exact text. For example, if the
+      // query contains the placeholders :foo and :foobar, and :foo has an
+      // array of values, using str_replace would affect both placeholders,
+      // but using the following preg_replace would only affect :foo because
+      // it is followed by a non-word character.
+      $query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);
+
+      // Update the args array with the new placeholders.
+      unset($args[$key]);
+      $args += $new_keys;
+
+      $modified = TRUE;
+    }
+
+    return $modified;
+  }
+
+  /**
+   * Gets the driver-specific override class if any for the specified class.
+   *
+   * @param string $class
+   *   The class for which we want the potentially driver-specific class.
+   * @param array $files
+   *   The name of the files in which the driver-specific class can be.
+   * @param $use_autoload
+   *   If TRUE, attempt to load classes using PHP's autoload capability
+   *   as well as the manual approach here.
+   * @return string
+   *   The name of the class that should be used for this driver.
+   */
+  public function getDriverClass($class, array $files = array(), $use_autoload = FALSE) {
+    if (empty($this->driverClasses[$class])) {
+      $driver = $this->driver();
+      $this->driverClasses[$class] = $class . '_' . $driver;
+      Database::loadDriverFile($driver, $files);
+      if (!class_exists($this->driverClasses[$class], $use_autoload)) {
+        $this->driverClasses[$class] = $class;
+      }
+    }
+    return $this->driverClasses[$class];
+  }
+
+  /**
+   * Prepares and returns a SELECT query object.
+   *
+   * @param $table
+   *   The base table for this query, that is, the first table in the FROM
+   *   clause. This table will also be used as the "base" table for query_alter
+   *   hook implementations.
+   * @param $alias
+   *   The alias of the base table of this query.
+   * @param $options
+   *   An array of options on the query.
+   *
+   * @return SelectQueryInterface
+   *   An appropriate SelectQuery object for this database connection. Note that
+   *   it may be a driver-specific subclass of SelectQuery, depending on the
+   *   driver.
+   *
+   * @see SelectQuery
+   */
+  public function select($table, $alias = NULL, array $options = array()) {
+    $class = $this->getDriverClass('SelectQuery', array('query.inc', 'select.inc'));
+    return new $class($table, $alias, $this, $options);
+  }
+
+  /**
+   * Prepares and returns an INSERT query object.
+   *
+   * @param $options
+   *   An array of options on the query.
+   *
+   * @return InsertQuery
+   *   A new InsertQuery object.
+   *
+   * @see InsertQuery
+   */
+  public function insert($table, array $options = array()) {
+    $class = $this->getDriverClass('InsertQuery', array('query.inc'));
+    return new $class($this, $table, $options);
+  }
+
+  /**
+   * Prepares and returns a MERGE query object.
+   *
+   * @param $options
+   *   An array of options on the query.
+   *
+   * @return MergeQuery
+   *   A new MergeQuery object.
+   *
+   * @see MergeQuery
+   */
+  public function merge($table, array $options = array()) {
+    $class = $this->getDriverClass('MergeQuery', array('query.inc'));
+    return new $class($this, $table, $options);
+  }
+
+
+  /**
+   * Prepares and returns an UPDATE query object.
+   *
+   * @param $options
+   *   An array of options on the query.
+   *
+   * @return UpdateQuery
+   *   A new UpdateQuery object.
+   *
+   * @see UpdateQuery
+   */
+  public function update($table, array $options = array()) {
+    $class = $this->getDriverClass('UpdateQuery', array('query.inc'));
+    return new $class($this, $table, $options);
+  }
+
+  /**
+   * Prepares and returns a DELETE query object.
+   *
+   * @param $options
+   *   An array of options on the query.
+   *
+   * @return DeleteQuery
+   *   A new DeleteQuery object.
+   *
+   * @see DeleteQuery
+   */
+  public function delete($table, array $options = array()) {
+    $class = $this->getDriverClass('DeleteQuery', array('query.inc'));
+    return new $class($this, $table, $options);
+  }
+
+  /**
+   * Prepares and returns a TRUNCATE query object.
+   *
+   * @param $options
+   *   An array of options on the query.
+   *
+   * @return TruncateQuery
+   *   A new TruncateQuery object.
+   *
+   * @see TruncateQuery
+   */
+  public function truncate($table, array $options = array()) {
+    $class = $this->getDriverClass('TruncateQuery', array('query.inc'));
+    return new $class($this, $table, $options);
+  }
+
+  /**
+   * Returns a DatabaseSchema object for manipulating the schema.
+   *
+   * This method will lazy-load the appropriate schema library file.
+   *
+   * @return DatabaseSchema
+   *   The DatabaseSchema object for this connection.
+   */
+  public function schema() {
+    if (empty($this->schema)) {
+      $class = $this->getDriverClass('DatabaseSchema', array('schema.inc'));
+      if (class_exists($class)) {
+        $this->schema = new $class($this);
+      }
+    }
+    return $this->schema;
+  }
+
+  /**
+   * Escapes a table name string.
+   *
+   * Force all table names to be strictly alphanumeric-plus-underscore.
+   * For some database drivers, it may also wrap the table name in
+   * database-specific escape characters.
+   *
+   * @return
+   *   The sanitized table name string.
+   */
+  public function escapeTable($table) {
+    return preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
+  }
+
+  /**
+   * Escapes a field name string.
+   *
+   * Force all field names to be strictly alphanumeric-plus-underscore.
+   * For some database drivers, it may also wrap the field name in
+   * database-specific escape characters.
+   *
+   * @return
+   *   The sanitized field name string.
+   */
+  public function escapeField($field) {
+    return preg_replace('/[^A-Za-z0-9_.]+/', '', $field);
+  }
+
+  /**
+   * Escapes an alias name string.
+   *
+   * Force all alias names to be strictly alphanumeric-plus-underscore. In
+   * contrast to DatabaseConnection::escapeField() /
+   * DatabaseConnection::escapeTable(), this doesn't allow the period (".")
+   * because that is not allowed in aliases.
+   *
+   * @return
+   *   The sanitized field name string.
+   */
+  public function escapeAlias($field) {
+    return preg_replace('/[^A-Za-z0-9_]+/', '', $field);
+  }
+
+  /**
+   * Escapes characters that work as wildcard characters in a LIKE pattern.
+   *
+   * The wildcard characters "%" and "_" as well as backslash are prefixed with
+   * a backslash. Use this to do a search for a verbatim string without any
+   * wildcard behavior.
+   *
+   * For example, the following does a case-insensitive query for all rows whose
+   * name starts with $prefix:
+   * @code
+   * $result = db_query(
+   *   'SELECT * FROM person WHERE name LIKE :pattern',
+   *   array(':pattern' => db_like($prefix) . '%')
+   * );
+   * @endcode
+   *
+   * Backslash is defined as escape character for LIKE patterns in
+   * DatabaseCondition::mapConditionOperator().
+   *
+   * @param $string
+   *   The string to escape.
+   *
+   * @return
+   *   The escaped string.
+   */
+  public function escapeLike($string) {
+    return addcslashes($string, '\%_');
+  }
+
+  /**
+   * Determines if there is an active transaction open.
+   *
+   * @return
+   *   TRUE if we're currently in a transaction, FALSE otherwise.
+   */
+  public function inTransaction() {
+    return ($this->transactionDepth() > 0);
+  }
+
+  /**
+   * Determines current transaction depth.
+   */
+  public function transactionDepth() {
+    return count($this->transactionLayers);
+  }
+
+  /**
+   * Returns a new DatabaseTransaction object on this connection.
+   *
+   * @param $name
+   *   Optional name of the savepoint.
+   *
+   * @return DatabaseTransaction
+   *   A DatabaseTransaction object.
+   *
+   * @see DatabaseTransaction
+   */
+  public function startTransaction($name = '') {
+    $class = $this->getDriverClass('DatabaseTransaction');
+    return new $class($this, $name);
+  }
+
+  /**
+   * Rolls back the transaction entirely or to a named savepoint.
+   *
+   * This method throws an exception if no transaction is active.
+   *
+   * @param $savepoint_name
+   *   The name of the savepoint. The default, 'drupal_transaction', will roll
+   *   the entire transaction back.
+   *
+   * @throws DatabaseTransactionNoActiveException
+   *
+   * @see DatabaseTransaction::rollback()
+   */
+  public function rollback($savepoint_name = 'drupal_transaction') {
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (!$this->inTransaction()) {
+      throw new DatabaseTransactionNoActiveException();
+    }
+    // A previous rollback to an earlier savepoint may mean that the savepoint
+    // in question has already been accidentally committed.
+    if (!isset($this->transactionLayers[$savepoint_name])) {
+      throw new DatabaseTransactionNoActiveException();
+    }
+
+    // We need to find the point we're rolling back to, all other savepoints
+    // before are no longer needed. If we rolled back other active savepoints,
+    // we need to throw an exception.
+    $rolled_back_other_active_savepoints = FALSE;
+    while ($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint == $savepoint_name) {
+        // If it is the last the transaction in the stack, then it is not a
+        // savepoint, it is the transaction itself so we will need to roll back
+        // the transaction rather than a savepoint.
+        if (empty($this->transactionLayers)) {
+          break;
+        }
+        $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint);
+        $this->popCommittableTransactions();
+        if ($rolled_back_other_active_savepoints) {
+          throw new DatabaseTransactionOutOfOrderException();
+        }
+        return;
+      }
+      else {
+        $rolled_back_other_active_savepoints = TRUE;
+      }
+    }
+    parent::rollBack();
+    if ($rolled_back_other_active_savepoints) {
+      throw new DatabaseTransactionOutOfOrderException();
+    }
+  }
+
+  /**
+   * Increases the depth of transaction nesting.
+   *
+   * If no transaction is already active, we begin a new transaction.
+   *
+   * @throws DatabaseTransactionNameNonUniqueException
+   *
+   * @see DatabaseTransaction
+   */
+  public function pushTransaction($name) {
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (isset($this->transactionLayers[$name])) {
+      throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
+    }
+    // If we're already in a transaction then we want to create a savepoint
+    // rather than try to create another transaction.
+    if ($this->inTransaction()) {
+      $this->query('SAVEPOINT ' . $name);
+    }
+    else {
+      parent::beginTransaction();
+    }
+    $this->transactionLayers[$name] = $name;
+  }
+
+  /**
+   * Decreases the depth of transaction nesting.
+   *
+   * If we pop off the last transaction layer, then we either commit or roll
+   * back the transaction as necessary. If no transaction is active, we return
+   * because the transaction may have manually been rolled back.
+   *
+   * @param $name
+   *   The name of the savepoint
+   *
+   * @throws DatabaseTransactionNoActiveException
+   * @throws DatabaseTransactionCommitFailedException
+   *
+   * @see DatabaseTransaction
+   */
+  public function popTransaction($name) {
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    // The transaction has already been committed earlier. There is nothing we
+    // need to do. If this transaction was part of an earlier out-of-order
+    // rollback, an exception would already have been thrown by
+    // Database::rollback().
+    if (!isset($this->transactionLayers[$name])) {
+      return;
+    }
+
+    // Mark this layer as committable.
+    $this->transactionLayers[$name] = FALSE;
+    $this->popCommittableTransactions();
+  }
+
+  /**
+   * Internal function: commit all the transaction layers that can commit.
+   */
+  protected function popCommittableTransactions() {
+    // Commit all the committable layers.
+    foreach (array_reverse($this->transactionLayers) as $name => $active) {
+      // Stop once we found an active transaction.
+      if ($active) {
+        break;
+      }
+
+      // If there are no more layers left then we should commit.
+      unset($this->transactionLayers[$name]);
+      if (empty($this->transactionLayers)) {
+        if (!parent::commit()) {
+          throw new DatabaseTransactionCommitFailedException();
+        }
+      }
+      else {
+        $this->query('RELEASE SAVEPOINT ' . $name);
+      }
+    }
+  }
+
+  /**
+   * Runs a limited-range query on this database object.
+   *
+   * Use this as a substitute for ->query() when a subset of the query is to be
+   * returned. User-supplied arguments to the query should be passed in as
+   * separate parameters so that they can be properly escaped to avoid SQL
+   * injection attacks.
+   *
+   * @param $query
+   *   A string containing an SQL query.
+   * @param $args
+   *   An array of values to substitute into the query at placeholder markers.
+   * @param $from
+   *   The first result row to return.
+   * @param $count
+   *   The maximum number of result rows to return.
+   * @param $options
+   *   An array of options on the query.
+   *
+   * @return DatabaseStatementInterface
+   *   A database query result resource, or NULL if the query was not executed
+   *   correctly.
+   */
+  abstract public function queryRange($query, $from, $count, array $args = array(), array $options = array());
+
+  /**
+   * Generates a temporary table name.
+   *
+   * @return
+   *   A table name.
+   */
+  protected function generateTemporaryTableName() {
+    return "db_temporary_" . $this->temporaryNameIndex++;
+  }
+
+  /**
+   * Runs a SELECT query and stores its results in a temporary table.
+   *
+   * Use this as a substitute for ->query() when the results need to stored
+   * in a temporary table. Temporary tables exist for the duration of the page
+   * request. User-supplied arguments to the query should be passed in as
+   * separate parameters so that they can be properly escaped to avoid SQL
+   * injection attacks.
+   *
+   * Note that if you need to know how many results were returned, you should do
+   * a SELECT COUNT(*) on the temporary table afterwards.
+   *
+   * @param $query
+   *   A string containing a normal SELECT SQL query.
+   * @param $args
+   *   An array of values to substitute into the query at placeholder markers.
+   * @param $options
+   *   An associative array of options to control how the query is run. See
+   *   the documentation for DatabaseConnection::defaultOptions() for details.
+   *
+   * @return
+   *   The name of the temporary table.
+   */
+  abstract function queryTemporary($query, array $args = array(), array $options = array());
+
+  /**
+   * Returns the type of database driver.
+   *
+   * This is not necessarily the same as the type of the database itself. For
+   * instance, there could be two MySQL drivers, mysql and mysql_mock. This
+   * function would return different values for each, but both would return
+   * "mysql" for databaseType().
+   */
+  abstract public function driver();
+
+  /**
+   * Returns the version of the database server.
+   */
+  public function version() {
+    return $this->getAttribute(PDO::ATTR_SERVER_VERSION);
+  }
+
+  /**
+   * Determines if this driver supports transactions.
+   *
+   * @return
+   *   TRUE if this connection supports transactions, FALSE otherwise.
+   */
+  public function supportsTransactions() {
+    return $this->transactionSupport;
+  }
+
+  /**
+   * Determines if this driver supports transactional DDL.
+   *
+   * DDL queries are those that change the schema, such as ALTER queries.
+   *
+   * @return
+   *   TRUE if this connection supports transactions for DDL queries, FALSE
+   *   otherwise.
+   */
+  public function supportsTransactionalDDL() {
+    return $this->transactionalDDLSupport;
+  }
+
+  /**
+   * Returns the name of the PDO driver for this connection.
+   */
+  abstract public function databaseType();
+
+
+  /**
+   * Gets any special processing requirements for the condition operator.
+   *
+   * Some condition types require special processing, such as IN, because
+   * the value data they pass in is not a simple value. This is a simple
+   * overridable lookup function. Database connections should define only
+   * those operators they wish to be handled differently than the default.
+   *
+   * @param $operator
+   *   The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive.
+   *
+   * @return
+   *   The extra handling directives for the specified operator, or NULL.
+   *
+   * @see DatabaseCondition::compile()
+   */
+  abstract public function mapConditionOperator($operator);
+
+  /**
+   * Throws an exception to deny direct access to transaction commits.
+   *
+   * We do not want to allow users to commit transactions at any time, only
+   * by destroying the transaction object or allowing it to go out of scope.
+   * A direct commit bypasses all of the safety checks we've built on top of
+   * PDO's transaction routines.
+   *
+   * @throws DatabaseTransactionExplicitCommitNotAllowedException
+   *
+   * @see DatabaseTransaction
+   */
+  public function commit() {
+    throw new DatabaseTransactionExplicitCommitNotAllowedException();
+  }
+
+  /**
+   * Retrieves an unique id from a given sequence.
+   *
+   * Use this function if for some reason you can't use a serial field. For
+   * example, MySQL has no ways of reading of the current value of a sequence
+   * and PostgreSQL can not advance the sequence to be larger than a given
+   * value. Or sometimes you just need a unique integer.
+   *
+   * @param $existing_id
+   *   After a database import, it might be that the sequences table is behind,
+   *   so by passing in the maximum existing id, it can be assured that we
+   *   never issue the same id.
+   *
+   * @return
+   *   An integer number larger than any number returned by earlier calls and
+   *   also larger than the $existing_id if one was passed in.
+   */
+  abstract public function nextId($existing_id = 0);
+}
+
+/**
+ * Primary front-controller for the database system.
+ *
+ * This class is uninstantiatable and un-extendable. It acts to encapsulate
+ * all control and shepherding of database connections into a single location
+ * without the use of globals.
+ */
+abstract class Database {
+
+  /**
+   * Flag to indicate a query call should simply return NULL.
+   *
+   * This is used for queries that have no reasonable return value anyway, such
+   * as INSERT statements to a table without a serial primary key.
+   */
+  const RETURN_NULL = 0;
+
+  /**
+   * Flag to indicate a query call should return the prepared statement.
+   */
+  const RETURN_STATEMENT = 1;
+
+  /**
+   * Flag to indicate a query call should return the number of affected rows.
+   */
+  const RETURN_AFFECTED = 2;
+
+  /**
+   * Flag to indicate a query call should return the "last insert id".
+   */
+  const RETURN_INSERT_ID = 3;
+
+  /**
+   * An nested array of all active connections. It is keyed by database name
+   * and target.
+   *
+   * @var array
+   */
+  static protected $connections = array();
+
+  /**
+   * A processed copy of the database connection information from settings.php.
+   *
+   * @var array
+   */
+  static protected $databaseInfo = NULL;
+
+  /**
+   * A list of key/target credentials to simply ignore.
+   *
+   * @var array
+   */
+  static protected $ignoreTargets = array();
+
+  /**
+   * The key of the currently active database connection.
+   *
+   * @var string
+   */
+  static protected $activeKey = 'default';
+
+  /**
+   * An array of active query log objects.
+   *
+   * Every connection has one and only one logger object for all targets and
+   * logging keys.
+   *
+   * array(
+   *   '$db_key' => DatabaseLog object.
+   * );
+   *
+   * @var array
+   */
+  static protected $logs = array();
+
+  /**
+   * Starts logging a given logging key on the specified connection.
+   *
+   * @param $logging_key
+   *   The logging key to log.
+   * @param $key
+   *   The database connection key for which we want to log.
+   *
+   * @return DatabaseLog
+   *   The query log object. Note that the log object does support richer
+   *   methods than the few exposed through the Database class, so in some
+   *   cases it may be desirable to access it directly.
+   *
+   * @see DatabaseLog
+   */
+  final public static function startLog($logging_key, $key = 'default') {
+    if (empty(self::$logs[$key])) {
+      self::$logs[$key] = new DatabaseLog($key);
+
+      // Every target already active for this connection key needs to have the
+      // logging object associated with it.
+      if (!empty(self::$connections[$key])) {
+        foreach (self::$connections[$key] as $connection) {
+          $connection->setLogger(self::$logs[$key]);
+        }
+      }
+    }
+
+    self::$logs[$key]->start($logging_key);
+    return self::$logs[$key];
+  }
+
+  /**
+   * Retrieves the queries logged on for given logging key.
+   *
+   * This method also ends logging for the specified key. To get the query log
+   * to date without ending the logger request the logging object by starting
+   * it again (which does nothing to an open log key) and call methods on it as
+   * desired.
+   *
+   * @param $logging_key
+   *   The logging key to log.
+   * @param $key
+   *   The database connection key for which we want to log.
+   *
+   * @return array
+   *   The query log for the specified logging key and connection.
+   *
+   * @see DatabaseLog
+   */
+  final public static function getLog($logging_key, $key = 'default') {
+    if (empty(self::$logs[$key])) {
+      return NULL;
+    }
+    $queries = self::$logs[$key]->get($logging_key);
+    self::$logs[$key]->end($logging_key);
+    return $queries;
+  }
+
+  /**
+   * Gets the connection object for the specified database key and target.
+   *
+   * @param $target
+   *   The database target name.
+   * @param $key
+   *   The database connection key. Defaults to NULL which means the active key.
+   *
+   * @return DatabaseConnection
+   *   The corresponding connection object.
+   */
+  final public static function getConnection($target = 'default', $key = NULL) {
+    if (!isset($key)) {
+      // By default, we want the active connection, set in setActiveConnection.
+      $key = self::$activeKey;
+    }
+    // If the requested target does not exist, or if it is ignored, we fall back
+    // to the default target. The target is typically either "default" or
+    // "slave", indicating to use a slave SQL server if one is available. If
+    // it's not available, then the default/master server is the correct server
+    // to use.
+    if (!empty(self::$ignoreTargets[$key][$target]) || !isset(self::$databaseInfo[$key][$target])) {
+      $target = 'default';
+    }
+
+    if (!isset(self::$connections[$key][$target])) {
+      // If necessary, a new connection is opened.
+      self::$connections[$key][$target] = self::openConnection($key, $target);
+    }
+    return self::$connections[$key][$target];
+  }
+
+  /**
+   * Determines if there is an active connection.
+   *
+   * Note that this method will return FALSE if no connection has been
+   * established yet, even if one could be.
+   *
+   * @return
+   *   TRUE if there is at least one database connection established, FALSE
+   *   otherwise.
+   */
+  final public static function isActiveConnection() {
+    return !empty(self::$activeKey) && !empty(self::$connections) && !empty(self::$connections[self::$activeKey]);
+  }
+
+  /**
+   * Sets the active connection to the specified key.
+   *
+   * @return
+   *   The previous database connection key.
+   */
+  final public static function setActiveConnection($key = 'default') {
+    if (empty(self::$databaseInfo)) {
+      self::parseConnectionInfo();
+    }
+
+    if (!empty(self::$databaseInfo[$key])) {
+      $old_key = self::$activeKey;
+      self::$activeKey = $key;
+      return $old_key;
+    }
+  }
+
+  /**
+   * Process the configuration file for database information.
+   */
+  final public static function parseConnectionInfo() {
+    global $databases;
+
+    $database_info = is_array($databases) ? $databases : array();
+    foreach ($database_info as $index => $info) {
+      foreach ($database_info[$index] as $target => $value) {
+        // If there is no "driver" property, then we assume it's an array of
+        // possible connections for this target. Pick one at random. That allows
+        //  us to have, for example, multiple slave servers.
+        if (empty($value['driver'])) {
+          $database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)];
+        }
+
+        // Parse the prefix information.
+        if (!isset($database_info[$index][$target]['prefix'])) {
+          // Default to an empty prefix.
+          $database_info[$index][$target]['prefix'] = array(
+            'default' => '',
+          );
+        }
+        elseif (!is_array($database_info[$index][$target]['prefix'])) {
+          // Transform the flat form into an array form.
+          $database_info[$index][$target]['prefix'] = array(
+            'default' => $database_info[$index][$target]['prefix'],
+          );
+        }
+      }
+    }
+
+    if (!is_array(self::$databaseInfo)) {
+      self::$databaseInfo = $database_info;
+    }
+
+    // Merge the new $database_info into the existing.
+    // array_merge_recursive() cannot be used, as it would make multiple
+    // database, user, and password keys in the same database array.
+    else {
+      foreach ($database_info as $database_key => $database_values) {
+        foreach ($database_values as $target => $target_values) {
+          self::$databaseInfo[$database_key][$target] = $target_values;
+        }
+      }
+    }
+  }
+
+  /**
+   * Adds database connection information for a given key/target.
+   *
+   * This method allows the addition of new connection credentials at runtime.
+   * Under normal circumstances the preferred way to specify database
+   * credentials is via settings.php. However, this method allows them to be
+   * added at arbitrary times, such as during unit tests, when connecting to
+   * admin-defined third party databases, etc.
+   *
+   * If the given key/target pair already exists, this method will be ignored.
+   *
+   * @param $key
+   *   The database key.
+   * @param $target
+   *   The database target name.
+   * @param $info
+   *   The database connection information, as it would be defined in
+   *   settings.php. Note that the structure of this array will depend on the
+   *   database driver it is connecting to.
+   */
+  public static function addConnectionInfo($key, $target, $info) {
+    if (empty(self::$databaseInfo[$key][$target])) {
+      self::$databaseInfo[$key][$target] = $info;
+    }
+  }
+
+  /**
+   * Gets information on the specified database connection.
+   *
+   * @param $connection
+   *   The connection key for which we want information.
+   */
+  final public static function getConnectionInfo($key = 'default') {
+    if (empty(self::$databaseInfo)) {
+      self::parseConnectionInfo();
+    }
+
+    if (!empty(self::$databaseInfo[$key])) {
+      return self::$databaseInfo[$key];
+    }
+  }
+
+  /**
+   * Rename a connection and its corresponding connection information.
+   *
+   * @param $old_key
+   *   The old connection key.
+   * @param $new_key
+   *   The new connection key.
+   * @return
+   *   TRUE in case of success, FALSE otherwise.
+   */
+  final public static function renameConnection($old_key, $new_key) {
+    if (empty(self::$databaseInfo)) {
+      self::parseConnectionInfo();
+    }
+
+    if (!empty(self::$databaseInfo[$old_key]) && empty(self::$databaseInfo[$new_key])) {
+      // Migrate the database connection information.
+      self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key];
+      unset(self::$databaseInfo[$old_key]);
+
+      // Migrate over the DatabaseConnection object if it exists.
+      if (isset(self::$connections[$old_key])) {
+        self::$connections[$new_key] = self::$connections[$old_key];
+        unset(self::$connections[$old_key]);
+      }
+
+      return TRUE;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  /**
+   * Remove a connection and its corresponding connection information.
+   *
+   * @param $key
+   *   The connection key.
+   * @return
+   *   TRUE in case of success, FALSE otherwise.
+   */
+  final public static function removeConnection($key) {
+    if (isset(self::$databaseInfo[$key])) {
+      self::closeConnection(NULL, $key);
+      unset(self::$databaseInfo[$key]);
+      return TRUE;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  /**
+   * Opens a connection to the server specified by the given key and target.
+   *
+   * @param $key
+   *   The database connection key, as specified in settings.php. The default is
+   *   "default".
+   * @param $target
+   *   The database target to open.
+   *
+   * @throws DatabaseConnectionNotDefinedException
+   * @throws DatabaseDriverNotSpecifiedException
+   */
+  final protected static function openConnection($key, $target) {
+    if (empty(self::$databaseInfo)) {
+      self::parseConnectionInfo();
+    }
+
+    // If the requested database does not exist then it is an unrecoverable
+    // error.
+    if (!isset(self::$databaseInfo[$key])) {
+      throw new DatabaseConnectionNotDefinedException('The specified database connection is not defined: ' . $key);
+    }
+
+    if (!$driver = self::$databaseInfo[$key][$target]['driver']) {
+      throw new DatabaseDriverNotSpecifiedException('Driver not specified for this database connection: ' . $key);
+    }
+
+    // We cannot rely on the registry yet, because the registry requires an
+    // open database connection.
+    $driver_class = 'DatabaseConnection_' . $driver;
+    require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/database.inc';
+    $new_connection = new $driver_class(self::$databaseInfo[$key][$target]);
+    $new_connection->setTarget($target);
+    $new_connection->setKey($key);
+
+    // If we have any active logging objects for this connection key, we need
+    // to associate them with the connection we just opened.
+    if (!empty(self::$logs[$key])) {
+      $new_connection->setLogger(self::$logs[$key]);
+    }
+
+    return $new_connection;
+  }
+
+  /**
+   * Closes a connection to the server specified by the given key and target.
+   *
+   * @param $target
+   *   The database target name.  Defaults to NULL meaning that all target
+   *   connections will be closed.
+   * @param $key
+   *   The database connection key. Defaults to NULL which means the active key.
+   */
+  public static function closeConnection($target = NULL, $key = NULL) {
+    // Gets the active connection by default.
+    if (!isset($key)) {
+      $key = self::$activeKey;
+    }
+    // To close a connection, it needs to be set to NULL and removed from the
+    // static variable. In all cases, closeConnection() might be called for a
+    // connection that was not opened yet, in which case the key is not defined
+    // yet and we just ensure that the connection key is undefined.
+    if (isset($target)) {
+      if (isset(self::$connections[$key][$target])) {
+        self::$connections[$key][$target]->destroy();
+        self::$connections[$key][$target] = NULL;
+      }
+      unset(self::$connections[$key][$target]);
+    }
+    else {
+      if (isset(self::$connections[$key])) {
+        foreach (self::$connections[$key] as $target => $connection) {
+          self::$connections[$key][$target]->destroy();
+          self::$connections[$key][$target] = NULL;
+        }
+      }
+      unset(self::$connections[$key]);
+    }
+  }
+
+  /**
+   * Instructs the system to temporarily ignore a given key/target.
+   *
+   * At times we need to temporarily disable slave queries. To do so, call this
+   * method with the database key and the target to disable. That database key
+   * will then always fall back to 'default' for that key, even if it's defined.
+   *
+   * @param $key
+   *   The database connection key.
+   * @param $target
+   *   The target of the specified key to ignore.
+   */
+  public static function ignoreTarget($key, $target) {
+    self::$ignoreTargets[$key][$target] = TRUE;
+  }
+
+  /**
+   * Load a file for the database that might hold a class.
+   *
+   * @param $driver
+   *   The name of the driver.
+   * @param array $files
+   *   The name of the files the driver specific class can be.
+   */
+  public static function loadDriverFile($driver, array $files = array()) {
+    static $base_path;
+
+    if (empty($base_path)) {
+      $base_path = dirname(realpath(__FILE__));
+    }
+
+    $driver_base_path = "$base_path/$driver";
+    foreach ($files as $file) {
+      // Load the base file first so that classes extending base classes will
+      // have the base class loaded.
+      foreach (array("$base_path/$file", "$driver_base_path/$file") as $filename) {
+        // The OS caches file_exists() and PHP caches require_once(), so
+        // we'll let both of those take care of performance here.
+        if (file_exists($filename)) {
+          require_once $filename;
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Exception for when popTransaction() is called with no active transaction.
+ */
+class DatabaseTransactionNoActiveException extends Exception { }
+
+/**
+ * Exception thrown when a savepoint or transaction name occurs twice.
+ */
+class DatabaseTransactionNameNonUniqueException extends Exception { }
+
+/**
+ * Exception thrown when a commit() function fails.
+ */
+class DatabaseTransactionCommitFailedException extends Exception { }
+
+/**
+ * Exception to deny attempts to explicitly manage transactions.
+ *
+ * This exception will be thrown when the PDO connection commit() is called.
+ * Code should never call this method directly.
+ */
+class DatabaseTransactionExplicitCommitNotAllowedException extends Exception { }
+
+/**
+ * Exception thrown when a rollback() resulted in other active transactions being rolled-back.
+ */
+class DatabaseTransactionOutOfOrderException extends Exception { }
+
+/**
+ * Exception thrown for merge queries that do not make semantic sense.
+ *
+ * There are many ways that a merge query could be malformed.  They should all
+ * throw this exception and set an appropriately descriptive message.
+ */
+class InvalidMergeQueryException extends Exception {}
+
+/**
+ * Exception thrown if an insert query specifies a field twice.
+ *
+ * It is not allowed to specify a field as default and insert field, this
+ * exception is thrown if that is the case.
+ */
+class FieldsOverlapException extends Exception {}
+
+/**
+ * Exception thrown if an insert query doesn't specify insert or default fields.
+ */
+class NoFieldsException extends Exception {}
+
+/**
+ * Exception thrown if an undefined database connection is requested.
+ */
+class DatabaseConnectionNotDefinedException extends Exception {}
+
+/**
+ * Exception thrown if no driver is specified for a database connection.
+ */
+class DatabaseDriverNotSpecifiedException extends Exception {}
+
+
+/**
+ * A wrapper class for creating and managing database transactions.
+ *
+ * Not all databases or database configurations support transactions. For
+ * example, MySQL MyISAM tables do not. It is also easy to begin a transaction
+ * and then forget to commit it, which can lead to connection errors when
+ * another transaction is started.
+ *
+ * This class acts as a wrapper for transactions. To begin a transaction,
+ * simply instantiate it. When the object goes out of scope and is destroyed
+ * it will automatically commit. It also will check to see if the specified
+ * connection supports transactions. If not, it will simply skip any transaction
+ * commands, allowing user-space code to proceed normally. The only difference
+ * is that rollbacks won't actually do anything.
+ *
+ * In the vast majority of cases, you should not instantiate this class
+ * directly. Instead, call ->startTransaction(), from the appropriate connection
+ * object.
+ */
+class DatabaseTransaction {
+
+  /**
+   * The connection object for this transaction.
+   *
+   * @var DatabaseConnection
+   */
+  protected $connection;
+
+  /**
+   * A boolean value to indicate whether this transaction has been rolled back.
+   *
+   * @var Boolean
+   */
+  protected $rolledBack = FALSE;
+
+  /**
+   * The name of the transaction.
+   *
+   * This is used to label the transaction savepoint. It will be overridden to
+   * 'drupal_transaction' if there is no transaction depth.
+   */
+  protected $name;
+
+  public function __construct(DatabaseConnection $connection, $name = NULL) {
+    $this->connection = $connection;
+    // If there is no transaction depth, then no transaction has started. Name
+    // the transaction 'drupal_transaction'.
+    if (!$depth = $connection->transactionDepth()) {
+      $this->name = 'drupal_transaction';
+    }
+    // Within transactions, savepoints are used. Each savepoint requires a
+    // name. So if no name is present we need to create one.
+    elseif (!$name) {
+      $this->name = 'savepoint_' . $depth;
+    }
+    else {
+      $this->name = $name;
+    }
+    $this->connection->pushTransaction($this->name);
+  }
+
+  public function __destruct() {
+    // If we rolled back then the transaction would have already been popped.
+    if (!$this->rolledBack) {
+      $this->connection->popTransaction($this->name);
+    }
+  }
+
+  /**
+   * Retrieves the name of the transaction or savepoint.
+   */
+  public function name() {
+    return $this->name;
+  }
+
+  /**
+   * Rolls back the current transaction.
+   *
+   * This is just a wrapper method to rollback whatever transaction stack we are
+   * currently in, which is managed by the connection object itself. Note that
+   * logging (preferable with watchdog_exception()) needs to happen after a
+   * transaction has been rolled back or the log messages will be rolled back
+   * too.
+   *
+   * @see DatabaseConnection::rollback()
+   * @see watchdog_exception()
+   */
+  public function rollback() {
+    $this->rolledBack = TRUE;
+    $this->connection->rollback($this->name);
+  }
+}
+
+/**
+ * Represents a prepared statement.
+ *
+ * Some methods in that class are purposefully commented out. Due to a change in
+ * how PHP defines PDOStatement, we can't define a signature for those methods
+ * that will work the same way between versions older than 5.2.6 and later
+ * versions.  See http://bugs.php.net/bug.php?id=42452 for more details.
+ *
+ * Child implementations should either extend PDOStatement:
+ * @code
+ * class DatabaseStatement_oracle extends PDOStatement implements DatabaseStatementInterface {}
+ * @endcode
+ * or define their own class. If defining their own class, they will also have
+ * to implement either the Iterator or IteratorAggregate interface before
+ * DatabaseStatementInterface:
+ * @code
+ * class DatabaseStatement_oracle implements Iterator, DatabaseStatementInterface {}
+ * @endcode
+ */
+interface DatabaseStatementInterface extends Traversable {
+
+  /**
+   * Executes a prepared statement
+   *
+   * @param $args
+   *   An array of values with as many elements as there are bound parameters in
+   *   the SQL statement being executed.
+   * @param $options
+   *   An array of options for this query.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function execute($args = array(), $options = array());
+
+  /**
+   * Gets the query string of this statement.
+   *
+   * @return
+   *   The query string, in its form with placeholders.
+   */
+  public function getQueryString();
+
+  /**
+   * Returns the number of rows affected by the last SQL statement.
+   *
+   * @return
+   *   The number of rows affected by the last DELETE, INSERT, or UPDATE
+   *   statement executed.
+   */
+  public function rowCount();
+
+  /**
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/en/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $a1
+   *   An option depending of the fetch mode specified by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   * @param $a2
+   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   constructor.
+   */
+  // public function setFetchMode($mode, $a1 = NULL, $a2 = array());
+
+  /**
+   * Fetches the next row from a result set.
+   *
+   * See http://php.net/manual/en/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   *   Default to what was specified by setFetchMode().
+   * @param $cursor_orientation
+   *   Not implemented in all database drivers, don't use.
+   * @param $cursor_offset
+   *   Not implemented in all database drivers, don't use.
+   *
+   * @return
+   *   A result, formatted according to $mode.
+   */
+  // public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL);
+
+  /**
+   * Returns a single field from the next record of a result set.
+   *
+   * @param $index
+   *   The numeric index of the field to return. Defaults to the first field.
+   *
+   * @return
+   *   A single field from the next record, or FALSE if there is no next record.
+   */
+  public function fetchField($index = 0);
+
+  /**
+   * Fetches the next row and returns it as an object.
+   *
+   * The object will be of the class specified by DatabaseStatementInterface::setFetchMode()
+   * or stdClass if not specified.
+   */
+  // public function fetchObject();
+
+  /**
+   * Fetches the next row and returns it as an associative array.
+   *
+   * This method corresponds to PDOStatement::fetchObject(), but for associative
+   * arrays. For some reason PDOStatement does not have a corresponding array
+   * helper method, so one is added.
+   *
+   * @return
+   *   An associative array, or FALSE if there is no next row.
+   */
+  public function fetchAssoc();
+
+  /**
+   * Returns an array containing all of the result set rows.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $column_index
+   *   If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
+   * @param $constructor_arguments
+   *   If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
+   *
+   * @return
+   *   An array of results.
+   */
+  // function fetchAll($mode = NULL, $column_index = NULL, array $constructor_arguments);
+
+  /**
+   * Returns an entire single column of a result set as an indexed array.
+   *
+   * Note that this method will run the result set to the end.
+   *
+   * @param $index
+   *   The index of the column number to fetch.
+   *
+   * @return
+   *   An indexed array, or an empty array if there is no result set.
+   */
+  public function fetchCol($index = 0);
+
+  /**
+   * Returns the entire result set as a single associative array.
+   *
+   * This method is only useful for two-column result sets. It will return an
+   * associative array where the key is one column from the result set and the
+   * value is another field. In most cases, the default of the first two columns
+   * is appropriate.
+   *
+   * Note that this method will run the result set to the end.
+   *
+   * @param $key_index
+   *   The numeric index of the field to use as the array key.
+   * @param $value_index
+   *   The numeric index of the field to use as the array value.
+   *
+   * @return
+   *   An associative array, or an empty array if there is no result set.
+   */
+  public function fetchAllKeyed($key_index = 0, $value_index = 1);
+
+  /**
+   * Returns the result set as an associative array keyed by the given field.
+   *
+   * If the given key appears multiple times, later records will overwrite
+   * earlier ones.
+   *
+   * @param $key
+   *   The name of the field on which to index the array.
+   * @param $fetch
+   *   The fetchmode to use. If set to PDO::FETCH_ASSOC, PDO::FETCH_NUM, or
+   *   PDO::FETCH_BOTH the returned value with be an array of arrays. For any
+   *   other value it will be an array of objects. By default, the fetch mode
+   *   set for the query will be used.
+   *
+   * @return
+   *   An associative array, or an empty array if there is no result set.
+   */
+  public function fetchAllAssoc($key, $fetch = NULL);
+}
+
+/**
+ * Default implementation of DatabaseStatementInterface.
+ *
+ * PDO allows us to extend the PDOStatement class to provide additional
+ * functionality beyond that offered by default. We do need extra
+ * functionality. By default, this class is not driver-specific. If a given
+ * driver needs to set a custom statement class, it may do so in its
+ * constructor.
+ *
+ * @see http://us.php.net/pdostatement
+ */
+class DatabaseStatementBase extends PDOStatement implements DatabaseStatementInterface {
+
+  /**
+   * Reference to the database connection object for this statement.
+   *
+   * The name $dbh is inherited from PDOStatement.
+   *
+   * @var DatabaseConnection
+   */
+  public $dbh;
+
+  protected function __construct($dbh) {
+    $this->dbh = $dbh;
+    $this->setFetchMode(PDO::FETCH_OBJ);
+  }
+
+  public function execute($args = array(), $options = array()) {
+    if (isset($options['fetch'])) {
+      if (is_string($options['fetch'])) {
+        // Default to an object. Note: db fields will be added to the object
+        // before the constructor is run. If you need to assign fields after
+        // the constructor is run, see http://drupal.org/node/315092.
+        $this->setFetchMode(PDO::FETCH_CLASS, $options['fetch']);
+      }
+      else {
+        $this->setFetchMode($options['fetch']);
+      }
+    }
+
+    $logger = $this->dbh->getLogger();
+    if (!empty($logger)) {
+      $query_start = microtime(TRUE);
+    }
+
+    $return = parent::execute($args);
+
+    if (!empty($logger)) {
+      $query_end = microtime(TRUE);
+      $logger->log($this, $args, $query_end - $query_start);
+    }
+
+    return $return;
+  }
+
+  public function getQueryString() {
+    return $this->queryString;
+  }
+
+  public function fetchCol($index = 0) {
+    return $this->fetchAll(PDO::FETCH_COLUMN, $index);
+  }
+
+  public function fetchAllAssoc($key, $fetch = NULL) {
+    $return = array();
+    if (isset($fetch)) {
+      if (is_string($fetch)) {
+        $this->setFetchMode(PDO::FETCH_CLASS, $fetch);
+      }
+      else {
+        $this->setFetchMode($fetch);
+      }
+    }
+
+    foreach ($this as $record) {
+      $record_key = is_object($record) ? $record->$key : $record[$key];
+      $return[$record_key] = $record;
+    }
+
+    return $return;
+  }
+
+  public function fetchAllKeyed($key_index = 0, $value_index = 1) {
+    $return = array();
+    $this->setFetchMode(PDO::FETCH_NUM);
+    foreach ($this as $record) {
+      $return[$record[$key_index]] = $record[$value_index];
+    }
+    return $return;
+  }
+
+  public function fetchField($index = 0) {
+    // Call PDOStatement::fetchColumn to fetch the field.
+    return $this->fetchColumn($index);
+  }
+
+  public function fetchAssoc() {
+    // Call PDOStatement::fetch to fetch the row.
+    return $this->fetch(PDO::FETCH_ASSOC);
+  }
+}
+
+/**
+ * Empty implementation of a database statement.
+ *
+ * This class satisfies the requirements of being a database statement/result
+ * object, but does not actually contain data.  It is useful when developers
+ * need to safely return an "empty" result set without connecting to an actual
+ * database.  Calling code can then treat it the same as if it were an actual
+ * result set that happens to contain no records.
+ *
+ * @see SearchQuery
+ */
+class DatabaseStatementEmpty implements Iterator, DatabaseStatementInterface {
+
+  public function execute($args = array(), $options = array()) {
+    return FALSE;
+  }
+
+  public function getQueryString() {
+    return '';
+  }
+
+  public function rowCount() {
+    return 0;
+  }
+
+  public function setFetchMode($mode, $a1 = NULL, $a2 = array()) {
+    return;
+  }
+
+  public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {
+    return NULL;
+  }
+
+  public function fetchField($index = 0) {
+    return NULL;
+  }
+
+  public function fetchObject() {
+    return NULL;
+  }
+
+  public function fetchAssoc() {
+    return NULL;
+  }
+
+  function fetchAll($mode = NULL, $column_index = NULL, array $constructor_arguments = array()) {
+    return array();
+  }
+
+  public function fetchCol($index = 0) {
+    return array();
+  }
+
+  public function fetchAllKeyed($key_index = 0, $value_index = 1) {
+    return array();
+  }
+
+  public function fetchAllAssoc($key, $fetch = NULL) {
+    return array();
+  }
+
+  /* Implementations of Iterator. */
+
+  public function current() {
+    return NULL;
+  }
+
+  public function key() {
+    return NULL;
+  }
+
+  public function rewind() {
+    // Nothing to do: our DatabaseStatement can't be rewound.
+  }
+
+  public function next() {
+    // Do nothing, since this is an always-empty implementation.
+  }
+
+  public function valid() {
+    return FALSE;
+  }
+}
+
+/**
+ * The following utility functions are simply convenience wrappers.
+ *
+ * They should never, ever have any database-specific code in them.
+ */
+
+/**
+ * Executes an arbitrary query string against the active database.
+ *
+ * Use this function for SELECT queries if it is just a simple query string.
+ * If the caller or other modules need to change the query, use db_select()
+ * instead.
+ *
+ * Do not use this function for INSERT, UPDATE, or DELETE queries. Those should
+ * be handled via db_insert(), db_update() and db_delete() respectively.
+ *
+ * @param $query
+ *   The prepared statement query to run. Although it will accept both named and
+ *   unnamed placeholders, named placeholders are strongly preferred as they are
+ *   more self-documenting.
+ * @param $args
+ *   An array of values to substitute into the query. If the query uses named
+ *   placeholders, this is an associative array in any order. If the query uses
+ *   unnamed placeholders (?), this is an indexed array and the order must match
+ *   the order of placeholders in the query string.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return DatabaseStatementInterface
+ *   A prepared statement object, already executed.
+ *
+ * @see DatabaseConnection::defaultOptions()
+ */
+function db_query($query, array $args = array(), array $options = array()) {
+  if (empty($options['target'])) {
+    $options['target'] = 'default';
+  }
+
+  return Database::getConnection($options['target'])->query($query, $args, $options);
+}
+
+/**
+ * Executes a query against the active database, restricted to a range.
+ *
+ * @param $query
+ *   The prepared statement query to run. Although it will accept both named and
+ *   unnamed placeholders, named placeholders are strongly preferred as they are
+ *   more self-documenting.
+ * @param $from
+ *   The first record from the result set to return.
+ * @param $count
+ *   The number of records to return from the result set.
+ * @param $args
+ *   An array of values to substitute into the query. If the query uses named
+ *   placeholders, this is an associative array in any order. If the query uses
+ *   unnamed placeholders (?), this is an indexed array and the order must match
+ *   the order of placeholders in the query string.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return DatabaseStatementInterface
+ *   A prepared statement object, already executed.
+ *
+ * @see DatabaseConnection::defaultOptions()
+ */
+function db_query_range($query, $from, $count, array $args = array(), array $options = array()) {
+  if (empty($options['target'])) {
+    $options['target'] = 'default';
+  }
+
+  return Database::getConnection($options['target'])->queryRange($query, $from, $count, $args, $options);
+}
+
+/**
+ * Executes a query string and saves the result set to a temporary table.
+ *
+ * The execution of the query string happens against the active database.
+ *
+ * @param $query
+ *   The prepared statement query to run. Although it will accept both named and
+ *   unnamed placeholders, named placeholders are strongly preferred as they are
+ *   more self-documenting.
+ * @param $args
+ *   An array of values to substitute into the query. If the query uses named
+ *   placeholders, this is an associative array in any order. If the query uses
+ *   unnamed placeholders (?), this is an indexed array and the order must match
+ *   the order of placeholders in the query string.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return
+ *   The name of the temporary table.
+ *
+ * @see DatabaseConnection::defaultOptions()
+ */
+function db_query_temporary($query, array $args = array(), array $options = array()) {
+  if (empty($options['target'])) {
+    $options['target'] = 'default';
+  }
+
+  return Database::getConnection($options['target'])->queryTemporary($query, $args, $options);
+}
+
+/**
+ * Returns a new InsertQuery object for the active database.
+ *
+ * @param $table
+ *   The table into which to insert.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return InsertQuery
+ *   A new InsertQuery object for this connection.
+ */
+function db_insert($table, array $options = array()) {
+  if (empty($options['target']) || $options['target'] == 'slave') {
+    $options['target'] = 'default';
+  }
+  return Database::getConnection($options['target'])->insert($table, $options);
+}
+
+/**
+ * Returns a new MergeQuery object for the active database.
+ *
+ * @param $table
+ *   The table into which to merge.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return MergeQuery
+ *   A new MergeQuery object for this connection.
+ */
+function db_merge($table, array $options = array()) {
+  if (empty($options['target']) || $options['target'] == 'slave') {
+    $options['target'] = 'default';
+  }
+  return Database::getConnection($options['target'])->merge($table, $options);
+}
+
+/**
+ * Returns a new UpdateQuery object for the active database.
+ *
+ * @param $table
+ *   The table to update.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return UpdateQuery
+ *   A new UpdateQuery object for this connection.
+ */
+function db_update($table, array $options = array()) {
+  if (empty($options['target']) || $options['target'] == 'slave') {
+    $options['target'] = 'default';
+  }
+  return Database::getConnection($options['target'])->update($table, $options);
+}
+
+/**
+ * Returns a new DeleteQuery object for the active database.
+ *
+ * @param $table
+ *   The table from which to delete.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return DeleteQuery
+ *   A new DeleteQuery object for this connection.
+ */
+function db_delete($table, array $options = array()) {
+  if (empty($options['target']) || $options['target'] == 'slave') {
+    $options['target'] = 'default';
+  }
+  return Database::getConnection($options['target'])->delete($table, $options);
+}
+
+/**
+ * Returns a new TruncateQuery object for the active database.
+ *
+ * @param $table
+ *   The table from which to delete.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return TruncateQuery
+ *   A new TruncateQuery object for this connection.
+ */
+function db_truncate($table, array $options = array()) {
+  if (empty($options['target']) || $options['target'] == 'slave') {
+    $options['target'] = 'default';
+  }
+  return Database::getConnection($options['target'])->truncate($table, $options);
+}
+
+/**
+ * Returns a new SelectQuery object for the active database.
+ *
+ * @param $table
+ *   The base table for this query. May be a string or another SelectQuery
+ *   object. If a query object is passed, it will be used as a subselect.
+ * @param $alias
+ *   The alias for the base table of this query.
+ * @param $options
+ *   An array of options to control how the query operates.
+ *
+ * @return SelectQuery
+ *   A new SelectQuery object for this connection.
+ */
+function db_select($table, $alias = NULL, array $options = array()) {
+  if (empty($options['target'])) {
+    $options['target'] = 'default';
+  }
+  return Database::getConnection($options['target'])->select($table, $alias, $options);
+}
+
+/**
+ * Returns a new transaction object for the active database.
+ *
+ * @param string $name
+ *   Optional name of the transaction.
+ * @param array $options
+ *   An array of options to control how the transaction operates:
+ *   - target: The database target name.
+ *
+ * @return DatabaseTransaction
+ *   A new DatabaseTransaction object for this connection.
+ */
+function db_transaction($name = NULL, array $options = array()) {
+  if (empty($options['target'])) {
+    $options['target'] = 'default';
+  }
+  return Database::getConnection($options['target'])->startTransaction($name);
+}
+
+/**
+ * Sets a new active database.
+ *
+ * @param $key
+ *   The key in the $databases array to set as the default database.
+ *
+ * @return
+ *   The key of the formerly active database.
+ */
+function db_set_active($key = 'default') {
+  return Database::setActiveConnection($key);
+}
+
+/**
+ * Restricts a dynamic table name to safe characters.
+ *
+ * Only keeps alphanumeric and underscores.
+ *
+ * @param $table
+ *   The table name to escape.
+ *
+ * @return
+ *   The escaped table name as a string.
+ */
+function db_escape_table($table) {
+  return Database::getConnection()->escapeTable($table);
+}
+
+/**
+ * Restricts a dynamic column or constraint name to safe characters.
+ *
+ * Only keeps alphanumeric and underscores.
+ *
+ * @param $field
+ *   The field name to escape.
+ *
+ * @return
+ *   The escaped field name as a string.
+ */
+function db_escape_field($field) {
+  return Database::getConnection()->escapeField($field);
+}
+
+/**
+ * Escapes characters that work as wildcard characters in a LIKE pattern.
+ *
+ * The wildcard characters "%" and "_" as well as backslash are prefixed with
+ * a backslash. Use this to do a search for a verbatim string without any
+ * wildcard behavior.
+ *
+ * For example, the following does a case-insensitive query for all rows whose
+ * name starts with $prefix:
+ * @code
+ * $result = db_query(
+ *   'SELECT * FROM person WHERE name LIKE :pattern',
+ *   array(':pattern' => db_like($prefix) . '%')
+ * );
+ * @endcode
+ *
+ * Backslash is defined as escape character for LIKE patterns in
+ * DatabaseCondition::mapConditionOperator().
+ *
+ * @param $string
+ *   The string to escape.
+ *
+ * @return
+ *   The escaped string.
+ */
+function db_like($string) {
+  return Database::getConnection()->escapeLike($string);
+}
+
+/**
+ * Retrieves the name of the currently active database driver.
+ *
+ * @return
+ *   The name of the currently active database driver.
+ */
+function db_driver() {
+  return Database::getConnection()->driver();
+}
+
+/**
+ * Closes the active database connection.
+ *
+ * @param $options
+ *   An array of options to control which connection is closed. Only the target
+ *   key has any meaning in this case.
+ */
+function db_close(array $options = array()) {
+  if (empty($options['target'])) {
+    $options['target'] = NULL;
+  }
+  Database::closeConnection($options['target']);
+}
+
+/**
+ * Retrieves a unique id.
+ *
+ * Use this function if for some reason you can't use a serial field. Using a
+ * serial field is preferred, and InsertQuery::execute() returns the value of
+ * the last ID inserted.
+ *
+ * @param $existing_id
+ *   After a database import, it might be that the sequences table is behind, so
+ *   by passing in a minimum ID, it can be assured that we never issue the same
+ *   ID.
+ *
+ * @return
+ *   An integer number larger than any number returned before for this sequence.
+ */
+function db_next_id($existing_id = 0) {
+  return Database::getConnection()->nextId($existing_id);
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "OR" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_or() {
+  return new DatabaseCondition('OR');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "AND" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_and() {
+  return new DatabaseCondition('AND');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "XOR" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_xor() {
+  return new DatabaseCondition('XOR');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to the specified conjunction.
+ *
+ * Internal API function call.  The db_and(), db_or(), and db_xor()
+ * functions are preferred.
+ *
+ * @param $conjunction
+ *   The conjunction to use for query conditions (AND, OR or XOR).
+ * @return DatabaseCondition
+ */
+function db_condition($conjunction) {
+  return new DatabaseCondition($conjunction);
+}
+
+/**
+ * @} End of "defgroup database".
+ */
+
+
+/**
+ * @addtogroup schemaapi
+ * @{
+ */
+
+/**
+ * Creates a new table from a Drupal table definition.
+ *
+ * @param $name
+ *   The name of the table to create.
+ * @param $table
+ *   A Schema API table definition array.
+ */
+function db_create_table($name, $table) {
+  return Database::getConnection()->schema()->createTable($name, $table);
+}
+
+/**
+ * Returns an array of field names from an array of key/index column specifiers.
+ *
+ * This is usually an identity function but if a key/index uses a column prefix
+ * specification, this function extracts just the name.
+ *
+ * @param $fields
+ *   An array of key/index column specifiers.
+ *
+ * @return
+ *   An array of field names.
+ */
+function db_field_names($fields) {
+  return Database::getConnection()->schema()->fieldNames($fields);
+}
+
+/**
+ * Checks if an index exists in the given table.
+ *
+ * @param $table
+ *   The name of the table in drupal (no prefixing).
+ * @param $name
+ *   The name of the index in drupal (no prefixing).
+ *
+ * @return
+ *   TRUE if the given index exists, otherwise FALSE.
+ */
+function db_index_exists($table, $name) {
+  return Database::getConnection()->schema()->indexExists($table, $name);
+}
+
+/**
+ * Checks if a table exists.
+ *
+ * @param $table
+ *   The name of the table in drupal (no prefixing).
+ *
+ * @return
+ *   TRUE if the given table exists, otherwise FALSE.
+ */
+function db_table_exists($table) {
+  return Database::getConnection()->schema()->tableExists($table);
+}
+
+/**
+ * Checks if a column exists in the given table.
+ *
+ * @param $table
+ *   The name of the table in drupal (no prefixing).
+ * @param $field
+ *   The name of the field.
+ *
+ * @return
+ *   TRUE if the given column exists, otherwise FALSE.
+ */
+function db_field_exists($table, $field) {
+  return Database::getConnection()->schema()->fieldExists($table, $field);
+}
+
+/**
+ * Finds all tables that are like the specified base table name.
+ *
+ * @param $table_expression
+ *   An SQL expression, for example "simpletest%" (without the quotes).
+ *   BEWARE: this is not prefixed, the caller should take care of that.
+ *
+ * @return
+ *   Array, both the keys and the values are the matching tables.
+ */
+function db_find_tables($table_expression) {
+  return Database::getConnection()->schema()->findTables($table_expression);
+}
+
+function _db_create_keys_sql($spec) {
+  return Database::getConnection()->schema()->createKeysSql($spec);
+}
+
+/**
+ * Renames a table.
+ *
+ * @param $table
+ *   The current name of the table to be renamed.
+ * @param $new_name
+ *   The new name for the table.
+ */
+function db_rename_table($table, $new_name) {
+  return Database::getConnection()->schema()->renameTable($table, $new_name);
+}
+
+/**
+ * Drops a table.
+ *
+ * @param $table
+ *   The table to be dropped.
+ */
+function db_drop_table($table) {
+  return Database::getConnection()->schema()->dropTable($table);
+}
+
+/**
+ * Adds a new field to a table.
+ *
+ * @param $table
+ *   Name of the table to be altered.
+ * @param $field
+ *   Name of the field to be added.
+ * @param $spec
+ *   The field specification array, as taken from a schema definition. The
+ *   specification may also contain the key 'initial'; the newly-created field
+ *   will be set to the value of the key in all rows. This is most useful for
+ *   creating NOT NULL columns with no default value in existing tables.
+ * @param $keys_new
+ *   Optional keys and indexes specification to be created on the table along
+ *   with adding the field. The format is the same as a table specification, but
+ *   without the 'fields' element. If you are adding a type 'serial' field, you
+ *   MUST specify at least one key or index including it in this array. See
+ *   db_change_field() for more explanation why.
+ *
+ * @see db_change_field()
+ */
+function db_add_field($table, $field, $spec, $keys_new = array()) {
+  return Database::getConnection()->schema()->addField($table, $field, $spec, $keys_new);
+}
+
+/**
+ * Drops a field.
+ *
+ * @param $table
+ *   The table to be altered.
+ * @param $field
+ *   The field to be dropped.
+ */
+function db_drop_field($table, $field) {
+  return Database::getConnection()->schema()->dropField($table, $field);
+}
+
+/**
+ * Sets the default value for a field.
+ *
+ * @param $table
+ *   The table to be altered.
+ * @param $field
+ *   The field to be altered.
+ * @param $default
+ *   Default value to be set. NULL for 'default NULL'.
+ */
+function db_field_set_default($table, $field, $default) {
+  return Database::getConnection()->schema()->fieldSetDefault($table, $field, $default);
+}
+
+/**
+ * Sets a field to have no default value.
+ *
+ * @param $table
+ *   The table to be altered.
+ * @param $field
+ *   The field to be altered.
+ */
+function db_field_set_no_default($table, $field) {
+  return Database::getConnection()->schema()->fieldSetNoDefault($table, $field);
+}
+
+/**
+ * Adds a primary key to a database table.
+ *
+ * @param $table
+ *   Name of the table to be altered.
+ * @param $fields
+ *   Array of fields for the primary key.
+ */
+function db_add_primary_key($table, $fields) {
+  return Database::getConnection()->schema()->addPrimaryKey($table, $fields);
+}
+
+/**
+ * Drops the primary key of a database table.
+ *
+ * @param $table
+ *   Name of the table to be altered.
+ */
+function db_drop_primary_key($table) {
+  return Database::getConnection()->schema()->dropPrimaryKey($table);
+}
+
+/**
+ * Adds a unique key.
+ *
+ * @param $table
+ *   The table to be altered.
+ * @param $name
+ *   The name of the key.
+ * @param $fields
+ *   An array of field names.
+ */
+function db_add_unique_key($table, $name, $fields) {
+  return Database::getConnection()->schema()->addUniqueKey($table, $name, $fields);
+}
+
+/**
+ * Drops a unique key.
+ *
+ * @param $table
+ *   The table to be altered.
+ * @param $name
+ *   The name of the key.
+ */
+function db_drop_unique_key($table, $name) {
+  return Database::getConnection()->schema()->dropUniqueKey($table, $name);
+}
+
+/**
+ * Adds an index.
+ *
+ * @param $table
+ *   The table to be altered.
+ * @param $name
+ *   The name of the index.
+ * @param $fields
+ *   An array of field names.
+ */
+function db_add_index($table, $name, $fields) {
+  return Database::getConnection()->schema()->addIndex($table, $name, $fields);
+}
+
+/**
+ * Drops an index.
+ *
+ * @param $table
+ *   The table to be altered.
+ * @param $name
+ *   The name of the index.
+ */
+function db_drop_index($table, $name) {
+  return Database::getConnection()->schema()->dropIndex($table, $name);
+}
+
+/**
+ * Changes a field definition.
+ *
+ * IMPORTANT NOTE: To maintain database portability, you have to explicitly
+ * recreate all indices and primary keys that are using the changed field.
+ *
+ * That means that you have to drop all affected keys and indexes with
+ * db_drop_{primary_key,unique_key,index}() before calling db_change_field().
+ * To recreate the keys and indices, pass the key definitions as the optional
+ * $keys_new argument directly to db_change_field().
+ *
+ * For example, suppose you have:
+ * @code
+ * $schema['foo'] = array(
+ *   'fields' => array(
+ *     'bar' => array('type' => 'int', 'not null' => TRUE)
+ *   ),
+ *   'primary key' => array('bar')
+ * );
+ * @endcode
+ * and you want to change foo.bar to be type serial, leaving it as the primary
+ * key. The correct sequence is:
+ * @code
+ * db_drop_primary_key('foo');
+ * db_change_field('foo', 'bar', 'bar',
+ *   array('type' => 'serial', 'not null' => TRUE),
+ *   array('primary key' => array('bar')));
+ * @endcode
+ *
+ * The reasons for this are due to the different database engines:
+ *
+ * On PostgreSQL, changing a field definition involves adding a new field and
+ * dropping an old one which causes any indices, primary keys and sequences
+ * (from serial-type fields) that use the changed field to be dropped.
+ *
+ * On MySQL, all type 'serial' fields must be part of at least one key or index
+ * as soon as they are created. You cannot use
+ * db_add_{primary_key,unique_key,index}() for this purpose because the ALTER
+ * TABLE command will fail to add the column without a key or index
+ * specification. The solution is to use the optional $keys_new argument to
+ * create the key or index at the same time as field.
+ *
+ * You could use db_add_{primary_key,unique_key,index}() in all cases unless you
+ * are converting a field to be type serial. You can use the $keys_new argument
+ * in all cases.
+ *
+ * @param $table
+ *   Name of the table.
+ * @param $field
+ *   Name of the field to change.
+ * @param $field_new
+ *   New name for the field (set to the same as $field if you don't want to
+ *   change the name).
+ * @param $spec
+ *   The field specification for the new field.
+ * @param $keys_new
+ *   Optional keys and indexes specification to be created on the table along
+ *   with changing the field. The format is the same as a table specification
+ *   but without the 'fields' element.
+ */
+function db_change_field($table, $field, $field_new, $spec, $keys_new = array()) {
+  return Database::getConnection()->schema()->changeField($table, $field, $field_new, $spec, $keys_new);
+}
+
+/**
+ * @} End of "addtogroup schemaapi".
+ */
+
+/**
+ * Sets a session variable specifying the lag time for ignoring a slave server.
+ */
+function db_ignore_slave() {
+  $connection_info = Database::getConnectionInfo();
+  // Only set ignore_slave_server if there are slave servers being used, which
+  // is assumed if there are more than one.
+  if (count($connection_info) > 1) {
+    // Five minutes is long enough to allow the slave to break and resume
+    // interrupted replication without causing problems on the Drupal site from
+    // the old data.
+    $duration = variable_get('maximum_replication_lag', 300);
+    // Set session variable with amount of time to delay before using slave.
+    $_SESSION['ignore_slave_server'] = REQUEST_TIME + $duration;
+  }
+}

+ 161 - 0
includes/database/log.inc

@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Logging classes for the database layer.
+ */
+
+/**
+ * Database query logger.
+ *
+ * We log queries in a separate object rather than in the connection object
+ * because we want to be able to see all queries sent to a given database, not
+ * database target. If we logged the queries in each connection object we
+ * would not be able to track what queries went to which target.
+ *
+ * Every connection has one and only one logging object on it for all targets
+ * and logging keys.
+ */
+class DatabaseLog {
+
+  /**
+   * Cache of logged queries. This will only be used if the query logger is enabled.
+   *
+   * The structure for the logging array is as follows:
+   *
+   * array(
+   *   $logging_key = array(
+   *     array(query => '', args => array(), caller => '', target => '', time => 0),
+   *     array(query => '', args => array(), caller => '', target => '', time => 0),
+   *   ),
+   * );
+   *
+   * @var array
+   */
+  protected $queryLog = array();
+
+  /**
+   * The connection key for which this object is logging.
+   *
+   * @var string
+   */
+  protected $connectionKey = 'default';
+
+  /**
+   * Constructor.
+   *
+   * @param $key
+   *   The database connection key for which to enable logging.
+   */
+  public function __construct($key = 'default') {
+    $this->connectionKey = $key;
+  }
+
+  /**
+   * Begin logging queries to the specified connection and logging key.
+   *
+   * If the specified logging key is already running this method does nothing.
+   *
+   * @param $logging_key
+   *   The identification key for this log request. By specifying different
+   *   logging keys we are able to start and stop multiple logging runs
+   *   simultaneously without them colliding.
+   */
+  public function start($logging_key) {
+    if (empty($this->queryLog[$logging_key])) {
+      $this->clear($logging_key);
+    }
+  }
+
+  /**
+   * Retrieve the query log for the specified logging key so far.
+   *
+   * @param $logging_key
+   *   The logging key to fetch.
+   * @return
+   *   An indexed array of all query records for this logging key.
+   */
+  public function get($logging_key) {
+    return $this->queryLog[$logging_key];
+  }
+
+  /**
+   * Empty the query log for the specified logging key.
+   *
+   * This method does not stop logging, it simply clears the log. To stop
+   * logging, use the end() method.
+   *
+   * @param $logging_key
+   *   The logging key to empty.
+   */
+  public function clear($logging_key) {
+    $this->queryLog[$logging_key] = array();
+  }
+
+  /**
+   * Stop logging for the specified logging key.
+   *
+   * @param $logging_key
+   *   The logging key to stop.
+   */
+  public function end($logging_key) {
+    unset($this->queryLog[$logging_key]);
+  }
+
+  /**
+   * Log a query to all active logging keys.
+   *
+   * @param $statement
+   *   The prepared statement object to log.
+   * @param $args
+   *   The arguments passed to the statement object.
+   * @param $time
+   *   The time in milliseconds the query took to execute.
+   */
+  public function log(DatabaseStatementInterface $statement, $args, $time) {
+    foreach (array_keys($this->queryLog) as $key) {
+      $this->queryLog[$key][] = array(
+        'query' => $statement->getQueryString(),
+        'args' => $args,
+        'target' => $statement->dbh->getTarget(),
+        'caller' => $this->findCaller(),
+        'time' => $time,
+      );
+    }
+  }
+
+  /**
+   * Determine the routine that called this query.
+   *
+   * We define "the routine that called this query" as the first entry in
+   * the call stack that is not inside includes/database and does have a file
+   * (which excludes call_user_func_array(), anonymous functions and similar).
+   * That makes the climbing logic very simple, and handles the variable stack
+   * depth caused by the query builders.
+   *
+   * @link http://www.php.net/debug_backtrace
+   * @return
+   *   This method returns a stack trace entry similar to that generated by
+   *   debug_backtrace(). However, it flattens the trace entry and the trace
+   *   entry before it so that we get the function and args of the function that
+   *   called into the database system, not the function and args of the
+   *   database call itself.
+   */
+  public function findCaller() {
+    $stack = debug_backtrace();
+    $stack_count = count($stack);
+    for ($i = 0; $i < $stack_count; ++$i) {
+      if (!empty($stack[$i]['file']) && strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) {
+        $stack[$i] += array('args' => array());
+        return array(
+          'file' => $stack[$i]['file'],
+          'line' => $stack[$i]['line'],
+          'function' => $stack[$i + 1]['function'],
+          'class' => isset($stack[$i + 1]['class']) ? $stack[$i + 1]['class'] : NULL,
+          'type' => isset($stack[$i + 1]['type']) ? $stack[$i + 1]['type'] : NULL,
+          'args' => $stack[$i + 1]['args'],
+        );
+      }
+    }
+  }
+}

+ 203 - 0
includes/database/mysql/database.inc

@@ -0,0 +1,203 @@
+<?php
+
+/**
+ * @file
+ * Database interface code for MySQL database servers.
+ */
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+class DatabaseConnection_mysql extends DatabaseConnection {
+
+  /**
+   * Flag to indicate if the cleanup function in __destruct() should run.
+   *
+   * @var boolean
+   */
+  protected $needsCleanup = FALSE;
+
+  public function __construct(array $connection_options = array()) {
+    // This driver defaults to transaction support, except if explicitly passed FALSE.
+    $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
+
+    // MySQL never supports transactional DDL.
+    $this->transactionalDDLSupport = FALSE;
+
+    $this->connectionOptions = $connection_options;
+
+    // The DSN should use either a socket or a host/port.
+    if (isset($connection_options['unix_socket'])) {
+      $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
+    }
+    else {
+      // Default to TCP connection on port 3306.
+      $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
+    }
+    $dsn .= ';dbname=' . $connection_options['database'];
+    // Allow PDO options to be overridden.
+    $connection_options += array(
+      'pdo' => array(),
+    );
+    $connection_options['pdo'] += array(
+      // So we don't have to mess around with cursors and unbuffered queries by default.
+      PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
+      // Because MySQL's prepared statements skip the query cache, because it's dumb.
+      PDO::ATTR_EMULATE_PREPARES => TRUE,
+    );
+
+    parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+
+    // Force MySQL to use the UTF-8 character set. Also set the collation, if a
+    // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
+    // for UTF-8.
+    if (!empty($connection_options['collation'])) {
+      $this->exec('SET NAMES utf8 COLLATE ' . $connection_options['collation']);
+    }
+    else {
+      $this->exec('SET NAMES utf8');
+    }
+
+    // Set MySQL init_commands if not already defined.  Default Drupal's MySQL
+    // behavior to conform more closely to SQL standards.  This allows Drupal
+    // to run almost seamlessly on many different kinds of database systems.
+    // These settings force MySQL to behave the same as postgresql, or sqlite
+    // in regards to syntax interpretation and invalid data handling.  See
+    // http://drupal.org/node/344575 for further discussion. Also, as MySQL 5.5
+    // changed the meaning of TRADITIONAL we need to spell out the modes one by
+    // one.
+    $connection_options += array(
+      'init_commands' => array(),
+    );
+    $connection_options['init_commands'] += array(
+      'sql_mode' => "SET sql_mode = 'ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
+    );
+    // Set connection options.
+    $this->exec(implode('; ', $connection_options['init_commands']));
+  }
+
+  public function __destruct() {
+    if ($this->needsCleanup) {
+      $this->nextIdDelete();
+    }
+  }
+
+  public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+    return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
+  }
+
+  public function queryTemporary($query, array $args = array(), array $options = array()) {
+    $tablename = $this->generateTemporaryTableName();
+    $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY SELECT', $query), $args, $options);
+    return $tablename;
+  }
+
+  public function driver() {
+    return 'mysql';
+  }
+
+  public function databaseType() {
+    return 'mysql';
+  }
+
+  public function mapConditionOperator($operator) {
+    // We don't want to override any of the defaults.
+    return NULL;
+  }
+
+  public function nextId($existing_id = 0) {
+    $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
+    // This should only happen after an import or similar event.
+    if ($existing_id >= $new_id) {
+      // If we INSERT a value manually into the sequences table, on the next
+      // INSERT, MySQL will generate a larger value. However, there is no way
+      // of knowing whether this value already exists in the table. MySQL
+      // provides an INSERT IGNORE which would work, but that can mask problems
+      // other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
+      // UPDATE in such a way that the UPDATE does not do anything. This way,
+      // duplicate keys do not generate errors but everything else does.
+      $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id));
+      $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
+    }
+    $this->needsCleanup = TRUE;
+    return $new_id;
+  }
+
+  public function nextIdDelete() {
+    // While we want to clean up the table to keep it up from occupying too
+    // much storage and memory, we must keep the highest value in the table
+    // because InnoDB  uses an in-memory auto-increment counter as long as the
+    // server runs. When the server is stopped and restarted, InnoDB
+    // reinitializes the counter for each table for the first INSERT to the
+    // table based solely on values from the table so deleting all values would
+    // be a problem in this case. Also, TRUNCATE resets the auto increment
+    // counter.
+    try {
+      $max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField();
+      // We know we are using MySQL here, no need for the slower db_delete().
+      $this->query('DELETE FROM {sequences} WHERE value < :value', array(':value' => $max_id));
+    }
+    // During testing, this function is called from shutdown with the
+    // simpletest prefix stored in $this->connection, and those tables are gone
+    // by the time shutdown is called so we need to ignore the database
+    // errors. There is no problem with completely ignoring errors here: if
+    // these queries fail, the sequence will work just fine, just use a bit
+    // more database storage and memory.
+    catch (PDOException $e) {
+    }
+  }
+
+  /**
+   * Overridden to work around issues to MySQL not supporting transactional DDL.
+   */
+  protected function popCommittableTransactions() {
+    // Commit all the committable layers.
+    foreach (array_reverse($this->transactionLayers) as $name => $active) {
+      // Stop once we found an active transaction.
+      if ($active) {
+        break;
+      }
+
+      // If there are no more layers left then we should commit.
+      unset($this->transactionLayers[$name]);
+      if (empty($this->transactionLayers)) {
+        if (!PDO::commit()) {
+          throw new DatabaseTransactionCommitFailedException();
+        }
+      }
+      else {
+        // Attempt to release this savepoint in the standard way.
+        try {
+          $this->query('RELEASE SAVEPOINT ' . $name);
+        }
+        catch (PDOException $e) {
+          // However, in MySQL (InnoDB), savepoints are automatically committed
+          // when tables are altered or created (DDL transactions are not
+          // supported). This can cause exceptions due to trying to release
+          // savepoints which no longer exist.
+          //
+          // To avoid exceptions when no actual error has occurred, we silently
+          // succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
+          if ($e->errorInfo[1] == '1305') {
+            // If one SAVEPOINT was released automatically, then all were.
+            // Therefore, clean the transaction stack.
+            $this->transactionLayers = array();
+            // We also have to explain to PDO that the transaction stack has
+            // been cleaned-up.
+            PDO::commit();
+          }
+          else {
+            throw $e;
+          }
+        }
+      }
+    }
+  }
+}
+
+
+/**
+ * @} End of "addtogroup database".
+ */

+ 33 - 0
includes/database/mysql/install.inc

@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Installation code for MySQL embedded database engine.
+ */
+
+/**
+ * Specifies installation tasks for MySQL and equivalent databases.
+ */
+class DatabaseTasks_mysql extends DatabaseTasks {
+  /**
+   * The PDO driver name for MySQL and equivalent databases.
+   *
+   * @var string
+   */
+  protected $pdoDriver = 'mysql';
+
+  /**
+   * Returns a human-readable name string for MySQL and equivalent databases.
+   */
+  public function name() {
+    return st('MySQL, MariaDB, or equivalent');
+  }
+
+  /**
+   * Returns the minimum version for MySQL.
+   */
+  public function minimumVersion() {
+    return '5.0.15';
+  }
+}
+

+ 107 - 0
includes/database/mysql/query.inc

@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * @file
+ * Query code for MySQL embedded database engine.
+ */
+
+
+class InsertQuery_mysql extends InsertQuery {
+
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (empty($this->fromQuery)) {
+      $max_placeholder = 0;
+      $values = array();
+      foreach ($this->insertValues as $insert_values) {
+        foreach ($insert_values as $value) {
+          $values[':db_insert_placeholder_' . $max_placeholder++] = $value;
+        }
+      }
+    }
+    else {
+      $values = $this->fromQuery->getArguments();
+    }
+
+    $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
+
+    // Re-initialize the values array so that we can re-use this query.
+    $this->insertValues = array();
+
+    return $last_insert_id;
+  }
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Default fields are always placed first for consistency.
+    $insert_fields = array_merge($this->defaultFields, $this->insertFields);
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+    }
+
+    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+    $max_placeholder = 0;
+    $values = array();
+    if (count($this->insertValues)) {
+      foreach ($this->insertValues as $insert_values) {
+        $placeholders = array();
+
+        // Default fields aren't really placeholders, but this is the most convenient
+        // way to handle them.
+        $placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
+
+        $new_placeholder = $max_placeholder + count($insert_values);
+        for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
+          $placeholders[] = ':db_insert_placeholder_' . $i;
+        }
+        $max_placeholder = $new_placeholder;
+        $values[] = '(' . implode(', ', $placeholders) . ')';
+      }
+    }
+    else {
+      // If there are no values, then this is a default-only query. We still need to handle that.
+      $placeholders = array_fill(0, count($this->defaultFields), 'default');
+      $values[] = '(' . implode(', ', $placeholders) . ')';
+    }
+
+    $query .= implode(', ', $values);
+
+    return $query;
+  }
+}
+
+class TruncateQuery_mysql extends TruncateQuery {
+  public function __toString() {
+    // TRUNCATE is actually a DDL statement on MySQL, and DDL statements are
+    // not transactional, and result in an implicit COMMIT. When we are in a
+    // transaction, fallback to the slower, but transactional, DELETE.
+    if ($this->connection->inTransaction()) {
+      // Create a comment string to prepend to the query.
+      $comments = $this->connection->makeComment($this->comments);
+      return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}';
+    }
+    else {
+      return parent::__toString();
+    }
+  }
+}
+
+/**
+ * @} End of "addtogroup database".
+ */

+ 536 - 0
includes/database/mysql/schema.inc

@@ -0,0 +1,536 @@
+<?php
+
+/**
+ * @file
+ * Database schema code for MySQL database servers.
+ */
+
+
+/**
+ * @addtogroup schemaapi
+ * @{
+ */
+
+class DatabaseSchema_mysql extends DatabaseSchema {
+
+  /**
+   * Maximum length of a table comment in MySQL.
+   */
+  const COMMENT_MAX_TABLE = 60;
+
+  /**
+   * Maximum length of a column comment in MySQL.
+   */
+  const COMMENT_MAX_COLUMN = 255;
+
+  /**
+   * Get information about the table and database name from the prefix.
+   *
+   * @return
+   *   A keyed array with information about the database, table name and prefix.
+   */
+  protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
+    $info = array('prefix' => $this->connection->tablePrefix($table));
+    if ($add_prefix) {
+      $table = $info['prefix'] . $table;
+    }
+    if (($pos = strpos($table, '.')) !== FALSE) {
+      $info['database'] = substr($table, 0, $pos);
+      $info['table'] = substr($table, ++$pos);
+    }
+    else {
+      $db_info = Database::getConnectionInfo();
+      $info['database'] = $db_info['default']['database'];
+      $info['table'] = $table;
+    }
+    return $info;
+  }
+
+  /**
+   * Build a condition to match a table name against a standard information_schema.
+   *
+   * MySQL uses databases like schemas rather than catalogs so when we build
+   * a condition to query the information_schema.tables, we set the default
+   * database as the schema unless specified otherwise, and exclude table_catalog
+   * from the condition criteria.
+   */
+  protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
+    $info = $this->connection->getConnectionOptions();
+
+    $table_info = $this->getPrefixInfo($table_name, $add_prefix);
+
+    $condition = new DatabaseCondition('AND');
+    $condition->condition('table_schema', $table_info['database']);
+    $condition->condition('table_name', $table_info['table'], $operator);
+    return $condition;
+  }
+
+  /**
+   * Generate SQL to create a new table from a Drupal schema definition.
+   *
+   * @param $name
+   *   The name of the table to create.
+   * @param $table
+   *   A Schema API table definition array.
+   * @return
+   *   An array of SQL statements to create the table.
+   */
+  protected function createTableSql($name, $table) {
+    $info = $this->connection->getConnectionOptions();
+
+    // Provide defaults if needed.
+    $table += array(
+      'mysql_engine' => 'InnoDB',
+      'mysql_character_set' => 'utf8',
+    );
+
+    $sql = "CREATE TABLE {" . $name . "} (\n";
+
+    // Add the SQL statement for each field.
+    foreach ($table['fields'] as $field_name => $field) {
+      $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
+    }
+
+    // Process keys & indexes.
+    $keys = $this->createKeysSql($table);
+    if (count($keys)) {
+      $sql .= implode(", \n", $keys) . ", \n";
+    }
+
+    // Remove the last comma and space.
+    $sql = substr($sql, 0, -3) . "\n) ";
+
+    $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
+    // By default, MySQL uses the default collation for new tables, which is
+    // 'utf8_general_ci' for utf8. If an alternate collation has been set, it
+    // needs to be explicitly specified.
+    // @see DatabaseConnection_mysql
+    if (!empty($info['collation'])) {
+      $sql .= ' COLLATE ' . $info['collation'];
+    }
+
+    // Add table comment.
+    if (!empty($table['description'])) {
+      $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
+    }
+
+    return array($sql);
+  }
+
+  /**
+   * Create an SQL string for a field to be used in table creation or alteration.
+   *
+   * Before passing a field out of a schema definition into this function it has
+   * to be processed by _db_process_field().
+   *
+   * @param $name
+   *   Name of the field.
+   * @param $spec
+   *   The field specification, as per the schema data structure format.
+   */
+  protected function createFieldSql($name, $spec) {
+    $sql = "`" . $name . "` " . $spec['mysql_type'];
+
+    if (in_array($spec['mysql_type'], array('VARCHAR', 'CHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'TEXT'))) {
+      if (isset($spec['length'])) {
+        $sql .= '(' . $spec['length'] . ')';
+      }
+      if (!empty($spec['binary'])) {
+        $sql .= ' BINARY';
+      }
+    }
+    elseif (isset($spec['precision']) && isset($spec['scale'])) {
+      $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+    }
+
+    if (!empty($spec['unsigned'])) {
+      $sql .= ' unsigned';
+    }
+
+    if (isset($spec['not null'])) {
+      if ($spec['not null']) {
+        $sql .= ' NOT NULL';
+      }
+      else {
+        $sql .= ' NULL';
+      }
+    }
+
+    if (!empty($spec['auto_increment'])) {
+      $sql .= ' auto_increment';
+    }
+
+    // $spec['default'] can be NULL, so we explicitly check for the key here.
+    if (array_key_exists('default', $spec)) {
+      if (is_string($spec['default'])) {
+        $spec['default'] = "'" . $spec['default'] . "'";
+      }
+      elseif (!isset($spec['default'])) {
+        $spec['default'] = 'NULL';
+      }
+      $sql .= ' DEFAULT ' . $spec['default'];
+    }
+
+    if (empty($spec['not null']) && !isset($spec['default'])) {
+      $sql .= ' DEFAULT NULL';
+    }
+
+    // Add column comment.
+    if (!empty($spec['description'])) {
+      $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
+    }
+
+    return $sql;
+  }
+
+  /**
+   * Set database-engine specific properties for a field.
+   *
+   * @param $field
+   *   A field description array, as specified in the schema documentation.
+   */
+  protected function processField($field) {
+
+    if (!isset($field['size'])) {
+      $field['size'] = 'normal';
+    }
+
+    // Set the correct database-engine specific datatype.
+    // In case one is already provided, force it to uppercase.
+    if (isset($field['mysql_type'])) {
+      $field['mysql_type'] = drupal_strtoupper($field['mysql_type']);
+    }
+    else {
+      $map = $this->getFieldTypeMap();
+      $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
+    }
+
+    if (isset($field['type']) && $field['type'] == 'serial') {
+      $field['auto_increment'] = TRUE;
+    }
+
+    return $field;
+  }
+
+  public function getFieldTypeMap() {
+    // Put :normal last so it gets preserved by array_flip. This makes
+    // it much easier for modules (such as schema.module) to map
+    // database types back into schema types.
+    // $map does not use drupal_static as its value never changes.
+    static $map = array(
+      'varchar:normal'  => 'VARCHAR',
+      'char:normal'     => 'CHAR',
+
+      'text:tiny'       => 'TINYTEXT',
+      'text:small'      => 'TINYTEXT',
+      'text:medium'     => 'MEDIUMTEXT',
+      'text:big'        => 'LONGTEXT',
+      'text:normal'     => 'TEXT',
+
+      'serial:tiny'     => 'TINYINT',
+      'serial:small'    => 'SMALLINT',
+      'serial:medium'   => 'MEDIUMINT',
+      'serial:big'      => 'BIGINT',
+      'serial:normal'   => 'INT',
+
+      'int:tiny'        => 'TINYINT',
+      'int:small'       => 'SMALLINT',
+      'int:medium'      => 'MEDIUMINT',
+      'int:big'         => 'BIGINT',
+      'int:normal'      => 'INT',
+
+      'float:tiny'      => 'FLOAT',
+      'float:small'     => 'FLOAT',
+      'float:medium'    => 'FLOAT',
+      'float:big'       => 'DOUBLE',
+      'float:normal'    => 'FLOAT',
+
+      'numeric:normal'  => 'DECIMAL',
+
+      'blob:big'        => 'LONGBLOB',
+      'blob:normal'     => 'BLOB',
+    );
+    return $map;
+  }
+
+  protected function createKeysSql($spec) {
+    $keys = array();
+
+    if (!empty($spec['primary key'])) {
+      $keys[] = 'PRIMARY KEY (' . $this->createKeysSqlHelper($spec['primary key']) . ')';
+    }
+    if (!empty($spec['unique keys'])) {
+      foreach ($spec['unique keys'] as $key => $fields) {
+        $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeysSqlHelper($fields) . ')';
+      }
+    }
+    if (!empty($spec['indexes'])) {
+      foreach ($spec['indexes'] as $index => $fields) {
+        $keys[] = 'INDEX `' . $index . '` (' . $this->createKeysSqlHelper($fields) . ')';
+      }
+    }
+
+    return $keys;
+  }
+
+  protected function createKeySql($fields) {
+    $return = array();
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
+      }
+      else {
+        $return[] = '`' . $field . '`';
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  protected function createKeysSqlHelper($fields) {
+    $return = array();
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
+      }
+      else {
+        $return[] = '`' . $field . '`';
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  public function renameTable($table, $new_name) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
+    }
+    if ($this->tableExists($new_name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
+    }
+
+    $info = $this->getPrefixInfo($new_name);
+    return $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
+  }
+
+  public function dropTable($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+
+    $this->connection->query('DROP TABLE {' . $table . '}');
+    return TRUE;
+  }
+
+  public function addField($table, $field, $spec, $keys_new = array()) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+    }
+    if ($this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+    }
+
+    $fixnull = FALSE;
+    if (!empty($spec['not null']) && !isset($spec['default'])) {
+      $fixnull = TRUE;
+      $spec['not null'] = FALSE;
+    }
+    $query = 'ALTER TABLE {' . $table . '} ADD ';
+    $query .= $this->createFieldSql($field, $this->processField($spec));
+    if ($keys_sql = $this->createKeysSql($keys_new)) {
+      $query .= ', ADD ' . implode(', ADD ', $keys_sql);
+    }
+    $this->connection->query($query);
+    if (isset($spec['initial'])) {
+      $this->connection->update($table)
+        ->fields(array($field => $spec['initial']))
+        ->execute();
+    }
+    if ($fixnull) {
+      $spec['not null'] = TRUE;
+      $this->changeField($table, $field, $field, $spec);
+    }
+  }
+
+  public function dropField($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
+    return TRUE;
+  }
+
+  public function fieldSetDefault($table, $field, $default) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+    }
+
+    if (!isset($default)) {
+      $default = 'NULL';
+    }
+    else {
+      $default = is_string($default) ? "'$default'" : $default;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` SET DEFAULT ' . $default);
+  }
+
+  public function fieldSetNoDefault($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT');
+  }
+
+  public function indexExists($table, $name) {
+    // Returns one row for each column in the index. Result is string or FALSE.
+    // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
+    $row = $this->connection->query('SHOW INDEX FROM {' . $table . "} WHERE key_name = '$name'")->fetchAssoc();
+    return isset($row['Key_name']);
+  }
+
+  public function addPrimaryKey($table, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
+    }
+    if ($this->indexExists($table, 'PRIMARY')) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
+  }
+
+  public function dropPrimaryKey($table) {
+    if (!$this->indexExists($table, 'PRIMARY')) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
+    return TRUE;
+  }
+
+  public function addUniqueKey($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
+  }
+
+  public function dropUniqueKey($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
+    return TRUE;
+  }
+
+  public function addIndex($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
+  }
+
+  public function dropIndex($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
+    return TRUE;
+  }
+
+  public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
+    }
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
+    }
+
+    $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
+    if ($keys_sql = $this->createKeysSql($keys_new)) {
+      $sql .= ', ADD ' . implode(', ADD ', $keys_sql);
+    }
+    $this->connection->query($sql);
+  }
+
+  public function prepareComment($comment, $length = NULL) {
+    // Work around a bug in some versions of PDO, see http://bugs.php.net/bug.php?id=41125
+    $comment = str_replace("'", '’', $comment);
+
+    // Truncate comment to maximum comment length.
+    if (isset($length)) {
+      // Add table prefixes before truncating.
+      $comment = truncate_utf8($this->connection->prefixTables($comment), $length, TRUE, TRUE);
+    }
+
+    return $this->connection->quote($comment);
+  }
+
+  /**
+   * Retrieve a table or column comment.
+   */
+  public function getComment($table, $column = NULL) {
+    $condition = $this->buildTableNameCondition($table);
+    if (isset($column)) {
+      $condition->condition('column_name', $column);
+      $condition->compile($this->connection, $this);
+      // Don't use {} around information_schema.columns table.
+      return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
+    }
+    $condition->compile($this->connection, $this);
+    // Don't use {} around information_schema.tables table.
+    $comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
+    // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
+    return preg_replace('/; InnoDB free:.*$/', '', $comment);
+  }
+
+  public function tableExists($table) {
+    // The information_schema table is very slow to query under MySQL 5.0.
+    // Instead, we try to select from the table in question.  If it fails,
+    // the most likely reason is that it does not exist. That is dramatically
+    // faster than using information_schema.
+    // @link http://bugs.mysql.com/bug.php?id=19588
+    // @todo: This override should be removed once we require a version of MySQL
+    // that has that bug fixed.
+    try {
+      $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
+      return TRUE;
+    }
+    catch (Exception $e) {
+      return FALSE;
+    }
+  }
+
+  public function fieldExists($table, $column) {
+    // The information_schema table is very slow to query under MySQL 5.0.
+    // Instead, we try to select from the table and field in question. If it
+    // fails, the most likely reason is that it does not exist. That is
+    // dramatically faster than using information_schema.
+    // @link http://bugs.mysql.com/bug.php?id=19588
+    // @todo: This override should be removed once we require a version of MySQL
+    // that has that bug fixed.
+    try {
+      $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
+      return TRUE;
+    }
+    catch (Exception $e) {
+      return FALSE;
+    }
+  }
+
+}
+
+/**
+ * @} End of "addtogroup schemaapi".
+ */

+ 223 - 0
includes/database/pgsql/database.inc

@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * @file
+ * Database interface code for PostgreSQL database servers.
+ */
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * The name by which to obtain a lock for retrive the next insert id.
+ */
+define('POSTGRESQL_NEXTID_LOCK', 1000);
+
+class DatabaseConnection_pgsql extends DatabaseConnection {
+
+  public function __construct(array $connection_options = array()) {
+    // This driver defaults to transaction support, except if explicitly passed FALSE.
+    $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
+
+    // Transactional DDL is always available in PostgreSQL,
+    // but we'll only enable it if standard transactions are.
+    $this->transactionalDDLSupport = $this->transactionSupport;
+
+    // Default to TCP connection on port 5432.
+    if (empty($connection_options['port'])) {
+      $connection_options['port'] = 5432;
+    }
+
+    // PostgreSQL in trust mode doesn't require a password to be supplied.
+    if (empty($connection_options['password'])) {
+      $connection_options['password'] = NULL;
+    }
+    // If the password contains a backslash it is treated as an escape character
+    // http://bugs.php.net/bug.php?id=53217
+    // so backslashes in the password need to be doubled up.
+    // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
+    // will break on this doubling up when the bug is fixed, so check the version
+    //elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
+    else {
+      $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
+    }
+
+    $this->connectionOptions = $connection_options;
+
+    $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
+
+    // Allow PDO options to be overridden.
+    $connection_options += array(
+      'pdo' => array(),
+    );
+    $connection_options['pdo'] += array(
+      // Prepared statements are most effective for performance when queries
+      // are recycled (used several times). However, if they are not re-used,
+      // prepared statements become ineffecient. Since most of Drupal's
+      // prepared queries are not re-used, it should be faster to emulate
+      // the preparation than to actually ready statements for re-use. If in
+      // doubt, reset to FALSE and measure performance.
+      PDO::ATTR_EMULATE_PREPARES => TRUE,
+      // Convert numeric values to strings when fetching.
+      PDO::ATTR_STRINGIFY_FETCHES => TRUE,
+    );
+    parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+
+    // Force PostgreSQL to use the UTF-8 character set by default.
+    $this->exec("SET NAMES 'UTF8'");
+
+    // Execute PostgreSQL init_commands.
+    if (isset($connection_options['init_commands'])) {
+      $this->exec(implode('; ', $connection_options['init_commands']));
+    }
+  }
+
+  public function prepareQuery($query) {
+    // mapConditionOperator converts LIKE operations to ILIKE for consistency
+    // with MySQL. However, Postgres does not support ILIKE on bytea (blobs)
+    // fields.
+    // To make the ILIKE operator work, we type-cast bytea fields into text.
+    // @todo This workaround only affects bytea fields, but the involved field
+    //   types involved in the query are unknown, so there is no way to
+    //   conditionally execute this for affected queries only.
+    return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE) /i', ' ${1}::text ${2} ', $query));
+  }
+
+  public function query($query, array $args = array(), $options = array()) {
+
+    $options += $this->defaultOptions();
+
+    // The PDO PostgreSQL driver has a bug which
+    // doesn't type cast booleans correctly when
+    // parameters are bound using associative
+    // arrays.
+    // See http://bugs.php.net/bug.php?id=48383
+    foreach ($args as &$value) {
+      if (is_bool($value)) {
+        $value = (int) $value;
+      }
+    }
+
+    try {
+      if ($query instanceof DatabaseStatementInterface) {
+        $stmt = $query;
+        $stmt->execute(NULL, $options);
+      }
+      else {
+        $this->expandArguments($query, $args);
+        $stmt = $this->prepareQuery($query);
+        $stmt->execute($args, $options);
+      }
+
+      switch ($options['return']) {
+        case Database::RETURN_STATEMENT:
+          return $stmt;
+        case Database::RETURN_AFFECTED:
+          return $stmt->rowCount();
+        case Database::RETURN_INSERT_ID:
+          return $this->lastInsertId($options['sequence_name']);
+        case Database::RETURN_NULL:
+          return;
+        default:
+          throw new PDOException('Invalid return directive: ' . $options['return']);
+      }
+    }
+    catch (PDOException $e) {
+      if ($options['throw_exception']) {
+        // Add additional debug information.
+        if ($query instanceof DatabaseStatementInterface) {
+          $e->query_string = $stmt->getQueryString();
+        }
+        else {
+          $e->query_string = $query;
+        }
+        $e->args = $args;
+        throw $e;
+      }
+      return NULL;
+    }
+  }
+
+  public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+    return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
+  }
+
+  public function queryTemporary($query, array $args = array(), array $options = array()) {
+    $tablename = $this->generateTemporaryTableName();
+    $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} AS SELECT', $query), $args, $options);
+    return $tablename;
+  }
+
+  public function driver() {
+    return 'pgsql';
+  }
+
+  public function databaseType() {
+    return 'pgsql';
+  }
+
+  public function mapConditionOperator($operator) {
+    static $specials;
+
+    // Function calls not allowed in static declarations, thus this method.
+    if (!isset($specials)) {
+      $specials = array(
+        // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE
+        // statements, we need to use ILIKE instead.
+        'LIKE' => array('operator' => 'ILIKE'),
+        'NOT LIKE' => array('operator' => 'NOT ILIKE'),
+      );
+    }
+
+    return isset($specials[$operator]) ? $specials[$operator] : NULL;
+  }
+
+  /**
+   * Retrive a the next id in a sequence.
+   *
+   * PostgreSQL has built in sequences. We'll use these instead of inserting
+   * and updating a sequences table.
+   */
+  public function nextId($existing = 0) {
+
+    // Retrive the name of the sequence. This information cannot be cached
+    // because the prefix may change, for example, like it does in simpletests.
+    $sequence_name = $this->makeSequenceName('sequences', 'value');
+
+    // When PostgreSQL gets a value too small then it will lock the table,
+    // retry the INSERT and if it's still too small then alter the sequence.
+    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
+    if ($id > $existing) {
+      return $id;
+    }
+
+    // PostgreSQL advisory locks are simply locks to be used by an
+    // application such as Drupal. This will prevent other Drupal proccesses
+    // from altering the sequence while we are.
+    $this->query("SELECT pg_advisory_lock(" . POSTGRESQL_NEXTID_LOCK . ")");
+
+    // While waiting to obtain the lock, the sequence may have been altered
+    // so lets try again to obtain an adequate value.
+    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
+    if ($id > $existing) {
+      $this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")");
+      return $id;
+    }
+
+    // Reset the sequence to a higher value than the existing id.
+    $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
+
+    // Retrive the next id. We know this will be as high as we want it.
+    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
+
+    $this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")");
+
+    return $id;
+  }
+}
+
+/**
+ * @} End of "addtogroup database".
+ */

+ 197 - 0
includes/database/pgsql/install.inc

@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * @file
+ * Install functions for PostgreSQL embedded database engine.
+ */
+
+
+// PostgreSQL specific install functions
+
+class DatabaseTasks_pgsql extends DatabaseTasks {
+  protected $pdoDriver = 'pgsql';
+
+  public function __construct() {
+    $this->tasks[] = array(
+      'function' => 'checkEncoding',
+      'arguments' => array(),
+    );
+    $this->tasks[] = array(
+      'function' => 'checkPHPVersion',
+      'arguments' => array(),
+    );
+    $this->tasks[] = array(
+      'function' => 'checkBinaryOutput',
+      'arguments' => array(),
+    );
+    $this->tasks[] = array(
+      'function' => 'initializeDatabase',
+      'arguments' => array(),
+    );
+  }
+
+  public function name() {
+    return st('PostgreSQL');
+  }
+
+  public function minimumVersion() {
+    return '8.3';
+  }
+
+  /**
+   * Check encoding is UTF8.
+   */
+  protected function checkEncoding() {
+    try {
+      if (db_query('SHOW server_encoding')->fetchField() == 'UTF8') {
+        $this->pass(st('Database is encoded in UTF-8'));
+      }
+      else {
+        $replacements = array(
+          '%encoding' => 'UTF8',
+          '%driver' => $this->name(),
+          '!link' => '<a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a>'
+        );
+        $text  = 'The %driver database must use %encoding encoding to work with Drupal.';
+        $text .= 'Recreate the database with %encoding encoding. See !link for more details.';
+        $this->fail(st($text, $replacements));
+      }
+    }
+    catch (Exception $e) {
+      $this->fail(st('Drupal could not determine the encoding of the database was set to UTF-8'));
+    }
+  }
+
+  /**
+   * Check PHP version.
+   *
+   * There are two bugs in PDO_pgsql affecting Drupal:
+   *
+   * - in versions < 5.2.7, PDO_pgsql refuses to insert an empty string into
+   *   a NOT NULL BLOB column. See: http://bugs.php.net/bug.php?id=46249
+   * - in versions < 5.2.11 and < 5.3.1 that prevents inserting integer values
+   *   into numeric columns that exceed the PHP_INT_MAX value.
+   *   See: http://bugs.php.net/bug.php?id=48924
+   */
+  function checkPHPVersion() {
+    if (!version_compare(PHP_VERSION, '5.2.11', '>=') || (version_compare(PHP_VERSION, '5.3.0', '>=') && !version_compare(PHP_VERSION, '5.3.1', '>='))) {
+      $this->fail(st('The version of PHP you are using has known issues with PostgreSQL. You need to upgrade PHP to 5.2.11, 5.3.1 or greater.'));
+    };
+  }
+
+  /**
+   * Check Binary Output.
+   *
+   * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
+   */
+  function checkBinaryOutput() {
+    // PostgreSQL < 9 doesn't support bytea_output, so verify we are running
+    // at least PostgreSQL 9.
+    $database_connection = Database::getConnection();
+    if (version_compare($database_connection->version(), '9') >= 0) {
+      if (!$this->checkBinaryOutputSuccess()) {
+        // First try to alter the database. If it fails, raise an error telling
+        // the user to do it themselves.
+        $connection_options = $database_connection->getConnectionOptions();
+        // It is safe to include the database name directly here, because this
+        // code is only called when a connection to the database is already
+        // established, thus the database name is guaranteed to be a correct
+        // value.
+        $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET bytea_output = 'escape';";
+        try {
+          db_query($query);
+        }
+        catch (Exception $e) {
+          // Ignore possible errors when the user doesn't have the necessary
+          // privileges to ALTER the database.
+        }
+
+        // Close the database connection so that the configuration parameter
+        // is applied to the current connection.
+        db_close();
+
+        // Recheck, if it fails, finally just rely on the end user to do the
+        // right thing.
+        if (!$this->checkBinaryOutputSuccess()) {
+          $replacements = array(
+            '%setting' => 'bytea_output',
+            '%current_value' => 'hex',
+            '%needed_value' => 'escape',
+            '!query' => "<code>" . $query . "</code>",
+          );
+          $this->fail(st("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: !query", $replacements));
+        }
+      }
+    }
+  }
+
+  /**
+   * Verify that a binary data roundtrip returns the original string.
+   */
+  protected function checkBinaryOutputSuccess() {
+    $bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
+    return ($bytea_output == 'encoding');
+  }
+
+  /**
+   * Make PostgreSQL Drupal friendly.
+   */
+  function initializeDatabase() {
+    // We create some functions using global names instead of prefixing them
+    // like we do with table names. This is so that we don't double up if more
+    // than one instance of Drupal is running on a single database. We therefore
+    // avoid trying to create them again in that case.
+
+    try {
+      // Create functions.
+      db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS
+        \'SELECT CASE WHEN (($1 > $2) OR ($2 IS NULL)) THEN $1 ELSE $2 END;\'
+        LANGUAGE \'sql\''
+      );
+      db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric, numeric) RETURNS numeric AS
+        \'SELECT greatest($1, greatest($2, $3));\'
+        LANGUAGE \'sql\''
+      );
+      // Don't use {} around pg_proc table.
+      if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
+        db_query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
+          \'SELECT random();\'
+          LANGUAGE \'sql\''
+        );
+      }
+
+      db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
+        \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
+        LANGUAGE \'sql\''
+      );
+
+      // Using || to concatenate in Drupal is not recommeneded because there are
+      // database drivers for Drupal that do not support the syntax, however
+      // they do support CONCAT(item1, item2) which we can replicate in
+      // PostgreSQL. PostgreSQL requires the function to be defined for each
+      // different argument variation the function can handle.
+      db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, anynonarray) RETURNS text AS
+        \'SELECT CAST($1 AS text) || CAST($2 AS text);\'
+        LANGUAGE \'sql\'
+      ');
+      db_query('CREATE OR REPLACE FUNCTION "concat"(text, anynonarray) RETURNS text AS
+        \'SELECT $1 || CAST($2 AS text);\'
+        LANGUAGE \'sql\'
+      ');
+      db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, text) RETURNS text AS
+        \'SELECT CAST($1 AS text) || $2;\'
+        LANGUAGE \'sql\'
+      ');
+      db_query('CREATE OR REPLACE FUNCTION "concat"(text, text) RETURNS text AS
+        \'SELECT $1 || $2;\'
+        LANGUAGE \'sql\'
+      ');
+
+      $this->pass(st('PostgreSQL has initialized itself.'));
+    }
+    catch (Exception $e) {
+      $this->fail(st('Drupal could not be correctly setup with the existing database. Revise any errors.'));
+    }
+  }
+}
+

+ 209 - 0
includes/database/pgsql/query.inc

@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+/**
+ * @file
+ * Query code for PostgreSQL embedded database engine.
+ */
+
+
+class InsertQuery_pgsql extends InsertQuery {
+
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    $stmt = $this->connection->prepareQuery((string) $this);
+
+    // Fetch the list of blobs and sequences used on that table.
+    $table_information = $this->connection->schema()->queryTableInformation($this->table);
+
+    $max_placeholder = 0;
+    $blobs = array();
+    $blob_count = 0;
+    foreach ($this->insertValues as $insert_values) {
+      foreach ($this->insertFields as $idx => $field) {
+        if (isset($table_information->blob_fields[$field])) {
+          $blobs[$blob_count] = fopen('php://memory', 'a');
+          fwrite($blobs[$blob_count], $insert_values[$idx]);
+          rewind($blobs[$blob_count]);
+
+          $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], PDO::PARAM_LOB);
+
+          // Pre-increment is faster in PHP than increment.
+          ++$blob_count;
+        }
+        else {
+          $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
+        }
+      }
+      // Check if values for a serial field has been passed.
+      if (!empty($table_information->serial_fields)) {
+        foreach ($table_information->serial_fields as $index => $serial_field) {
+          $serial_key = array_search($serial_field, $this->insertFields);
+          if ($serial_key !== FALSE) {
+            $serial_value = $insert_values[$serial_key];
+
+            // Force $last_insert_id to the specified value. This is only done
+            // if $index is 0.
+            if ($index == 0) {
+              $last_insert_id = $serial_value;
+            }
+            // Set the sequence to the bigger value of either the passed
+            // value or the max value of the column. It can happen that another
+            // thread calls nextval() which could lead to a serial number being
+            // used twice. However, trying to insert a value into a serial
+            // column should only be done in very rare cases and is not thread
+            // safe by definition.
+            $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value));
+          }
+        }
+      }
+    }
+    if (!empty($this->fromQuery)) {
+      // bindParam stores only a reference to the variable that is followed when
+      // the statement is executed. We pass $arguments[$key] instead of $value
+      // because the second argument to bindParam is passed by reference and
+      // the foreach statement assigns the element to the existing reference.
+      $arguments = $this->fromQuery->getArguments();
+      foreach ($arguments as $key => $value) {
+        $stmt->bindParam($key, $arguments[$key]);
+      }
+    }
+
+    // PostgreSQL requires the table name to be specified explicitly
+    // when requesting the last insert ID, so we pass that in via
+    // the options array.
+    $options = $this->queryOptions;
+
+    if (!empty($table_information->sequences)) {
+      $options['sequence_name'] = $table_information->sequences[0];
+    }
+    // If there are no sequences then we can't get a last insert id.
+    elseif ($options['return'] == Database::RETURN_INSERT_ID) {
+      $options['return'] = Database::RETURN_NULL;
+    }
+    // Only use the returned last_insert_id if it is not already set.
+    if (!empty($last_insert_id)) {
+      $this->connection->query($stmt, array(), $options);
+    }
+    else {
+      $last_insert_id = $this->connection->query($stmt, array(), $options);
+    }
+
+    // Re-initialize the values array so that we can re-use this query.
+    $this->insertValues = array();
+
+    return $last_insert_id;
+  }
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Default fields are always placed first for consistency.
+    $insert_fields = array_merge($this->defaultFields, $this->insertFields);
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+    }
+
+    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+    $max_placeholder = 0;
+    $values = array();
+      if (count($this->insertValues)) {
+      foreach ($this->insertValues as $insert_values) {
+        $placeholders = array();
+
+        // Default fields aren't really placeholders, but this is the most convenient
+        // way to handle them.
+        $placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
+
+        $new_placeholder = $max_placeholder + count($insert_values);
+        for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
+          $placeholders[] = ':db_insert_placeholder_' . $i;
+        }
+        $max_placeholder = $new_placeholder;
+        $values[] = '(' . implode(', ', $placeholders) . ')';
+      }
+    }
+    else {
+      // If there are no values, then this is a default-only query. We still need to handle that.
+      $placeholders = array_fill(0, count($this->defaultFields), 'default');
+      $values[] = '(' . implode(', ', $placeholders) . ')';
+    }
+
+    $query .= implode(', ', $values);
+
+    return $query;
+  }
+}
+
+class UpdateQuery_pgsql extends UpdateQuery {
+  public function execute() {
+    $max_placeholder = 0;
+    $blobs = array();
+    $blob_count = 0;
+
+    // Because we filter $fields the same way here and in __toString(), the
+    // placeholders will all match up properly.
+    $stmt = $this->connection->prepareQuery((string) $this);
+
+    // Fetch the list of blobs and sequences used on that table.
+    $table_information = $this->connection->schema()->queryTableInformation($this->table);
+
+    // Expressions take priority over literal fields, so we process those first
+    // and remove any literal fields that conflict.
+    $fields = $this->fields;
+    $expression_fields = array();
+    foreach ($this->expressionFields as $field => $data) {
+      if (!empty($data['arguments'])) {
+        foreach ($data['arguments'] as $placeholder => $argument) {
+          // We assume that an expression will never happen on a BLOB field,
+          // which is a fairly safe assumption to make since in most cases
+          // it would be an invalid query anyway.
+          $stmt->bindParam($placeholder, $data['arguments'][$placeholder]);
+        }
+      }
+      unset($fields[$field]);
+    }
+
+    foreach ($fields as $field => $value) {
+      $placeholder = ':db_update_placeholder_' . ($max_placeholder++);
+
+      if (isset($table_information->blob_fields[$field])) {
+        $blobs[$blob_count] = fopen('php://memory', 'a');
+        fwrite($blobs[$blob_count], $value);
+        rewind($blobs[$blob_count]);
+        $stmt->bindParam($placeholder, $blobs[$blob_count], PDO::PARAM_LOB);
+        ++$blob_count;
+      }
+      else {
+        $stmt->bindParam($placeholder, $fields[$field]);
+      }
+    }
+
+    if (count($this->condition)) {
+      $this->condition->compile($this->connection, $this);
+
+      $arguments = $this->condition->arguments();
+      foreach ($arguments as $placeholder => $value) {
+        $stmt->bindParam($placeholder, $arguments[$placeholder]);
+      }
+    }
+
+    $options = $this->queryOptions;
+    $options['already_prepared'] = TRUE;
+    $this->connection->query($stmt, $options);
+
+    return $stmt->rowCount();
+  }
+}

+ 617 - 0
includes/database/pgsql/schema.inc

@@ -0,0 +1,617 @@
+<?php
+
+/**
+ * @file
+ * Database schema code for PostgreSQL database servers.
+ */
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+class DatabaseSchema_pgsql extends DatabaseSchema {
+
+  /**
+   * A cache of information about blob columns and sequences of tables.
+   *
+   * This is collected by DatabaseConnection_pgsql->queryTableInformation(),
+   * by introspecting the database.
+   *
+   * @see DatabaseConnection_pgsql->queryTableInformation()
+   * @var array
+   */
+  protected $tableInformation = array();
+
+  /**
+   * Fetch the list of blobs and sequences used on a table.
+   *
+   * We introspect the database to collect the information required by insert
+   * and update queries.
+   *
+   * @param $table_name
+   *   The non-prefixed name of the table.
+   * @return
+   *   An object with two member variables:
+   *     - 'blob_fields' that lists all the blob fields in the table.
+   *     - 'sequences' that lists the sequences used in that table.
+   */
+  public function queryTableInformation($table) {
+    // Generate a key to reference this table's information on.
+    $key = $this->connection->prefixTables('{' . $table . '}');
+    if (!strpos($key, '.')) {
+      $key = 'public.' . $key;
+    }
+
+    if (!isset($this->tableInformation[$key])) {
+      // Split the key into schema and table for querying.
+      list($schema, $table_name) = explode('.', $key);
+      $table_information = (object) array(
+        'blob_fields' => array(),
+        'sequences' => array(),
+      );
+      // Don't use {} around information_schema.columns table.
+      $result = $this->connection->query("SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = :schema AND table_name = :table AND (data_type = 'bytea' OR (numeric_precision IS NOT NULL AND column_default LIKE :default))", array(
+        ':schema' => $schema,
+        ':table' => $table_name,
+        ':default' => '%nextval%',
+      ));
+      foreach ($result as $column) {
+        if ($column->data_type == 'bytea') {
+          $table_information->blob_fields[$column->column_name] = TRUE;
+        }
+        elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) {
+          // We must know of any sequences in the table structure to help us
+          // return the last insert id. If there is more than 1 sequences the
+          // first one (index 0 of the sequences array) will be used.
+          $table_information->sequences[] = $matches[1];
+          $table_information->serial_fields[] = $column->column_name;
+        }
+      }
+      $this->tableInformation[$key] = $table_information;
+    }
+    return $this->tableInformation[$key];
+  }
+
+  /**
+   * Fetch the list of CHECK constraints used on a field.
+   *
+   * We introspect the database to collect the information required by field
+   * alteration.
+   *
+   * @param $table
+   *   The non-prefixed name of the table.
+   * @param $field
+   *   The name of the field.
+   * @return
+   *   An array of all the checks for the field.
+   */
+  public function queryFieldInformation($table, $field) {
+    $prefixInfo = $this->getPrefixInfo($table, TRUE);
+
+    // Split the key into schema and table for querying.
+    $schema = $prefixInfo['schema'];
+    $table_name = $prefixInfo['table'];
+
+    $field_information = (object) array(
+        'checks' => array(),
+    );
+    $checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = 'c' AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", array(
+      ':schema' => $schema,
+      ':table' => $table_name,
+      ':column' => $field,
+    ));
+    $field_information = $checks->fetchCol();
+
+    return $field_information;
+  }
+
+  /**
+   * Generate SQL to create a new table from a Drupal schema definition.
+   *
+   * @param $name
+   *   The name of the table to create.
+   * @param $table
+   *   A Schema API table definition array.
+   * @return
+   *   An array of SQL statements to create the table.
+   */
+  protected function createTableSql($name, $table) {
+    $sql_fields = array();
+    foreach ($table['fields'] as $field_name => $field) {
+      $sql_fields[] = $this->createFieldSql($field_name, $this->processField($field));
+    }
+
+    $sql_keys = array();
+    if (isset($table['primary key']) && is_array($table['primary key'])) {
+      $sql_keys[] = 'PRIMARY KEY (' . implode(', ', $table['primary key']) . ')';
+    }
+    if (isset($table['unique keys']) && is_array($table['unique keys'])) {
+      foreach ($table['unique keys'] as $key_name => $key) {
+        $sql_keys[] = 'CONSTRAINT ' . $this->prefixNonTable($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
+      }
+    }
+
+    $sql = "CREATE TABLE {" . $name . "} (\n\t";
+    $sql .= implode(",\n\t", $sql_fields);
+    if (count($sql_keys) > 0) {
+      $sql .= ",\n\t";
+    }
+    $sql .= implode(",\n\t", $sql_keys);
+    $sql .= "\n)";
+    $statements[] = $sql;
+
+    if (isset($table['indexes']) && is_array($table['indexes'])) {
+      foreach ($table['indexes'] as $key_name => $key) {
+        $statements[] = $this->_createIndexSql($name, $key_name, $key);
+      }
+    }
+
+    // Add table comment.
+    if (!empty($table['description'])) {
+      $statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']);
+    }
+
+    // Add column comments.
+    foreach ($table['fields'] as $field_name => $field) {
+      if (!empty($field['description'])) {
+        $statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']);
+      }
+    }
+
+    return $statements;
+  }
+
+  /**
+   * Create an SQL string for a field to be used in table creation or
+   * alteration.
+   *
+   * Before passing a field out of a schema definition into this
+   * function it has to be processed by _db_process_field().
+   *
+   * @param $name
+   *    Name of the field.
+   * @param $spec
+   *    The field specification, as per the schema data structure format.
+   */
+  protected function createFieldSql($name, $spec) {
+    $sql = $name . ' ' . $spec['pgsql_type'];
+
+    if (isset($spec['type']) && $spec['type'] == 'serial') {
+      unset($spec['not null']);
+    }
+
+    if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
+      $sql .= '(' . $spec['length'] . ')';
+    }
+    elseif (isset($spec['precision']) && isset($spec['scale'])) {
+      $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+    }
+
+    if (!empty($spec['unsigned'])) {
+      $sql .= " CHECK ($name >= 0)";
+    }
+
+    if (isset($spec['not null'])) {
+      if ($spec['not null']) {
+        $sql .= ' NOT NULL';
+      }
+      else {
+        $sql .= ' NULL';
+      }
+    }
+    if (isset($spec['default'])) {
+      $default = is_string($spec['default']) ? "'" . $spec['default'] . "'" : $spec['default'];
+      $sql .= " default $default";
+    }
+
+    return $sql;
+  }
+
+  /**
+   * Set database-engine specific properties for a field.
+   *
+   * @param $field
+   *   A field description array, as specified in the schema documentation.
+   */
+  protected function processField($field) {
+    if (!isset($field['size'])) {
+      $field['size'] = 'normal';
+    }
+
+    // Set the correct database-engine specific datatype.
+    // In case one is already provided, force it to lowercase.
+    if (isset($field['pgsql_type'])) {
+      $field['pgsql_type'] = drupal_strtolower($field['pgsql_type']);
+    }
+    else {
+      $map = $this->getFieldTypeMap();
+      $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']];
+    }
+
+    if (!empty($field['unsigned'])) {
+      // Unsigned datatypes are not supported in PostgreSQL 8.3. In MySQL,
+      // they are used to ensure a positive number is inserted and it also
+      // doubles the maximum integer size that can be stored in a field.
+      // The PostgreSQL schema in Drupal creates a check constraint
+      // to ensure that a value inserted is >= 0. To provide the extra
+      // integer capacity, here, we bump up the column field size.
+      if (!isset($map)) {
+        $map = $this->getFieldTypeMap();
+      }
+      switch ($field['pgsql_type']) {
+        case 'smallint':
+          $field['pgsql_type'] = $map['int:medium'];
+          break;
+        case 'int' :
+          $field['pgsql_type'] = $map['int:big'];
+          break;
+      }
+    }
+    if (isset($field['type']) && $field['type'] == 'serial') {
+      unset($field['not null']);
+    }
+    return $field;
+  }
+
+  /**
+   * This maps a generic data type in combination with its data size
+   * to the engine-specific data type.
+   */
+  function getFieldTypeMap() {
+    // Put :normal last so it gets preserved by array_flip. This makes
+    // it much easier for modules (such as schema.module) to map
+    // database types back into schema types.
+    // $map does not use drupal_static as its value never changes.
+    static $map = array(
+      'varchar:normal' => 'varchar',
+      'char:normal' => 'character',
+
+      'text:tiny' => 'text',
+      'text:small' => 'text',
+      'text:medium' => 'text',
+      'text:big' => 'text',
+      'text:normal' => 'text',
+
+      'int:tiny' => 'smallint',
+      'int:small' => 'smallint',
+      'int:medium' => 'int',
+      'int:big' => 'bigint',
+      'int:normal' => 'int',
+
+      'float:tiny' => 'real',
+      'float:small' => 'real',
+      'float:medium' => 'real',
+      'float:big' => 'double precision',
+      'float:normal' => 'real',
+
+      'numeric:normal' => 'numeric',
+
+      'blob:big' => 'bytea',
+      'blob:normal' => 'bytea',
+
+      'serial:tiny' => 'serial',
+      'serial:small' => 'serial',
+      'serial:medium' => 'serial',
+      'serial:big' => 'bigserial',
+      'serial:normal' => 'serial',
+      );
+    return $map;
+  }
+
+  protected function _createKeySql($fields) {
+    $return = array();
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')';
+      }
+      else {
+        $return[] = '"' . $field . '"';
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  function renameTable($table, $new_name) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
+    }
+    if ($this->tableExists($new_name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
+    }
+
+    // Get the schema and tablename for the old table.
+    $old_full_name = $this->connection->prefixTables('{' . $table . '}');
+    list($old_schema, $old_table_name) = strpos($old_full_name, '.') ? explode('.', $old_full_name) : array('public', $old_full_name);
+
+    // Index names and constraint names are global in PostgreSQL, so we need to
+    // rename them when renaming the table.
+    $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', array(':schema' => $old_schema, ':table' => $old_table_name));
+    foreach ($indexes as $index) {
+      if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)$/', $index->indexname, $matches)) {
+        $index_name = $matches[1];
+        $this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name);
+      }
+    }
+
+    // Now rename the table.
+    // Ensure the new table name does not include schema syntax.
+    $prefixInfo = $this->getPrefixInfo($new_name);
+    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']);
+  }
+
+  public function dropTable($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+
+    $this->connection->query('DROP TABLE {' . $table . '}');
+    return TRUE;
+  }
+
+  public function addField($table, $field, $spec, $new_keys = array()) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+    }
+    if ($this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+    }
+
+    $fixnull = FALSE;
+    if (!empty($spec['not null']) && !isset($spec['default'])) {
+      $fixnull = TRUE;
+      $spec['not null'] = FALSE;
+    }
+    $query = 'ALTER TABLE {' . $table . '} ADD COLUMN ';
+    $query .= $this->createFieldSql($field, $this->processField($spec));
+    $this->connection->query($query);
+    if (isset($spec['initial'])) {
+      $this->connection->update($table)
+        ->fields(array($field => $spec['initial']))
+        ->execute();
+    }
+    if ($fixnull) {
+      $this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL");
+    }
+    if (isset($new_keys)) {
+      $this->_createKeys($table, $new_keys);
+    }
+    // Add column comment.
+    if (!empty($spec['description'])) {
+      $this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description']));
+    }
+  }
+
+  public function dropField($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"');
+    return TRUE;
+  }
+
+  public function fieldSetDefault($table, $field, $default) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+    }
+
+    if (!isset($default)) {
+      $default = 'NULL';
+    }
+    else {
+      $default = is_string($default) ? "'$default'" : $default;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" SET DEFAULT ' . $default);
+  }
+
+  public function fieldSetNoDefault($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
+  }
+
+  public function indexExists($table, $name) {
+    // Details http://www.postgresql.org/docs/8.3/interactive/view-pg-indexes.html
+    $index_name = '{' . $table . '}_' . $name . '_idx';
+    return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField();
+  }
+
+  /**
+   * Helper function: check if a constraint (PK, FK, UK) exists.
+   *
+   * @param $table
+   *   The name of the table.
+   * @param $name
+   *   The name of the constraint (typically 'pkey' or '[constraint]_key').
+   */
+  protected function constraintExists($table, $name) {
+    $constraint_name = '{' . $table . '}_' . $name;
+    return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField();
+  }
+
+  public function addPrimaryKey($table, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
+    }
+    if ($this->constraintExists($table, 'pkey')) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . implode(',', $fields) . ')');
+  }
+
+  public function dropPrimaryKey($table) {
+    if (!$this->constraintExists($table, 'pkey')) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->prefixNonTable($table, 'pkey'));
+    return TRUE;
+  }
+
+  function addUniqueKey($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+    }
+    if ($this->constraintExists($table, $name . '_key')) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
+  }
+
+  public function dropUniqueKey($table, $name) {
+    if (!$this->constraintExists($table, $name . '_key')) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '"');
+    return TRUE;
+  }
+
+  public function addIndex($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
+    }
+
+    $this->connection->query($this->_createIndexSql($table, $name, $fields));
+  }
+
+  public function dropIndex($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $this->connection->query('DROP INDEX ' . $this->prefixNonTable($table, $name, 'idx'));
+    return TRUE;
+  }
+
+  public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
+    }
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
+    }
+
+    $spec = $this->processField($spec);
+
+    // We need to typecast the new column to best be able to transfer the data
+    // Schema_pgsql::getFieldTypeMap() will return possibilities that are not
+    // 'cast-able' such as 'serial' - so they need to be casted int instead.
+    if (in_array($spec['pgsql_type'], array('serial', 'bigserial', 'numeric'))) {
+      $typecast = 'int';
+    }
+    else {
+      $typecast = $spec['pgsql_type'];
+    }
+
+    if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
+      $typecast .= '(' . $spec['length'] . ')';
+    }
+    elseif (isset($spec['precision']) && isset($spec['scale'])) {
+      $typecast .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+    }
+
+    // Remove old check constraints.
+    $field_info = $this->queryFieldInformation($table, $field);
+
+    foreach ($field_info as $check) {
+      $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"');
+    }
+
+    // Remove old default.
+    $this->fieldSetNoDefault($table, $field);
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $typecast . ' USING "' . $field . '"::' . $typecast);
+
+    if (isset($spec['not null'])) {
+      if ($spec['not null']) {
+        $nullaction = 'SET NOT NULL';
+      }
+      else {
+        $nullaction = 'DROP NOT NULL';
+      }
+      $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $nullaction);
+    }
+
+    if (in_array($spec['pgsql_type'], array('serial', 'bigserial'))) {
+      // Type "serial" is known to PostgreSQL, but *only* during table creation,
+      // not when altering. Because of that, the sequence needs to be created
+      // and initialized by hand.
+      $seq = "{" . $table . "}_" . $field_new . "_seq";
+      $this->connection->query("CREATE SEQUENCE " . $seq);
+      // Set sequence to maximal field value to not conflict with existing
+      // entries.
+      $this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}");
+      $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" SET DEFAULT nextval(\'' . $seq . '\')');
+    }
+
+    // Rename the column if necessary.
+    if ($field != $field_new) {
+      $this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"');
+    }
+
+    // Add unsigned check if necessary.
+    if (!empty($spec['unsigned'])) {
+      $this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)');
+    }
+
+    // Add default if necessary.
+    if (isset($spec['default'])) {
+      $this->fieldSetDefault($table, $field_new, $spec['default']);
+    }
+
+    // Change description if necessary.
+    if (!empty($spec['description'])) {
+      $this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description']));
+    }
+
+    if (isset($new_keys)) {
+      $this->_createKeys($table, $new_keys);
+    }
+  }
+
+  protected function _createIndexSql($table, $name, $fields) {
+    $query = 'CREATE INDEX "' . $this->prefixNonTable($table, $name, 'idx') . '" ON {' . $table . '} (';
+    $query .= $this->_createKeySql($fields) . ')';
+    return $query;
+  }
+
+  protected function _createKeys($table, $new_keys) {
+    if (isset($new_keys['primary key'])) {
+      $this->addPrimaryKey($table, $new_keys['primary key']);
+    }
+    if (isset($new_keys['unique keys'])) {
+      foreach ($new_keys['unique keys'] as $name => $fields) {
+        $this->addUniqueKey($table, $name, $fields);
+      }
+    }
+    if (isset($new_keys['indexes'])) {
+      foreach ($new_keys['indexes'] as $name => $fields) {
+        $this->addIndex($table, $name, $fields);
+      }
+    }
+  }
+
+  /**
+   * Retrieve a table or column comment.
+   */
+  public function getComment($table, $column = NULL) {
+    $info = $this->getPrefixInfo($table);
+    // Don't use {} around pg_class, pg_attribute tables.
+    if (isset($column)) {
+      return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', array($info['table'], $column))->fetchField();
+    }
+    else {
+      return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', array('pg_class', $info['table']))->fetchField();
+    }
+  }
+}

+ 108 - 0
includes/database/pgsql/select.inc

@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @file
+ * Select builder for PostgreSQL database engine.
+ */
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+class SelectQuery_pgsql extends SelectQuery {
+
+  public function orderRandom() {
+    $alias = $this->addExpression('RANDOM()', 'random_field');
+    $this->orderBy($alias);
+    return $this;
+  }
+
+  /**
+   * Overrides SelectQuery::orderBy().
+   *
+   * PostgreSQL adheres strictly to the SQL-92 standard and requires that when
+   * using DISTINCT or GROUP BY conditions, fields and expressions that are
+   * ordered on also need to be selected. This is a best effort implementation
+   * to handle the cases that can be automated by adding the field if it is not
+   * yet selected.
+   *
+   * @code
+   *   $query = db_select('node', 'n');
+   *   $query->join('node_revision', 'nr', 'n.vid = nr.vid');
+   *   $query
+   *     ->distinct()
+   *     ->fields('n')
+   *     ->orderBy('timestamp');
+   * @endcode
+   *
+   * In this query, it is not possible (without relying on the schema) to know
+   * whether timestamp belongs to node_revisions and needs to be added or
+   * belongs to node and is already selected. Queries like this will need to be
+   * corrected in the original query by adding an explicit call to
+   * SelectQuery::addField() or SelectQuery::fields().
+   *
+   * Since this has a small performance impact, both by the additional
+   * processing in this function and in the database that needs to return the
+   * additional fields, this is done as an override instead of implementing it
+   * directly in SelectQuery::orderBy().
+   */
+  public function orderBy($field, $direction = 'ASC') {
+    // Call parent function to order on this.
+    $return = parent::orderBy($field, $direction);
+
+    // If there is a table alias specified, split it up.
+    if (strpos($field, '.') !== FALSE) {
+      list($table, $table_field) = explode('.', $field);
+    }
+    // Figure out if the field has already been added.
+    foreach ($this->fields as $existing_field) {
+      if (!empty($table)) {
+        // If table alias is given, check if field and table exists.
+        if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
+          return $return;
+        }
+      }
+      else {
+        // If there is no table, simply check if the field exists as a field or
+        // an aliased field.
+        if ($existing_field['alias'] == $field) {
+          return $return;
+        }
+      }
+    }
+
+    // Also check expression aliases.
+    foreach ($this->expressions as $expression) {
+      if ($expression['alias'] == $field) {
+        return $return;
+      }
+    }
+
+    // If a table loads all fields, it can not be added again. It would
+    // result in an ambigious alias error because that field would be loaded
+    // twice: Once through table_alias.* and once directly. If the field
+    // actually belongs to a different table, it must be added manually.
+    foreach ($this->tables as $table) {
+      if (!empty($table['all_fields'])) {
+        return $return;
+      }
+    }
+
+    // If $field contains an characters which are not allowed in a field name
+    // it is considered an expression, these can't be handeld automatically
+    // either.
+    if ($this->connection->escapeField($field) != $field) {
+      return $return;
+    }
+
+    // This is a case that can be handled automatically, add the field.
+    $this->addField(NULL, $field);
+    return $return;
+  }
+}
+
+/**
+ * @} End of "addtogroup database".
+ */
+

+ 507 - 0
includes/database/prefetch.inc

@@ -0,0 +1,507 @@
+<?php
+
+/**
+ * @file
+ * Database interface code for engines that need complete control over their
+ * result sets. For example, SQLite will prefix some column names by the name
+ * of the table. We post-process the data, by renaming the column names
+ * using the same convention as MySQL and PostgreSQL.
+ */
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * An implementation of DatabaseStatementInterface that prefetches all data.
+ *
+ * This class behaves very similar to a PDOStatement but as it always fetches
+ * every row it is possible to manipulate those results.
+ */
+class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface {
+
+  /**
+   * The query string.
+   *
+   * @var string
+   */
+  protected $queryString;
+
+  /**
+   * Driver-specific options. Can be used by child classes.
+   *
+   * @var Array
+   */
+  protected $driverOptions;
+
+  /**
+   * Reference to the database connection object for this statement.
+   *
+   * The name $dbh is inherited from PDOStatement.
+   *
+   * @var DatabaseConnection
+   */
+  public $dbh;
+
+  /**
+   * Main data store.
+   *
+   * @var Array
+   */
+  protected $data = array();
+
+  /**
+   * The current row, retrieved in PDO::FETCH_ASSOC format.
+   *
+   * @var Array
+   */
+  protected $currentRow = NULL;
+
+  /**
+   * The key of the current row.
+   *
+   * @var int
+   */
+  protected $currentKey = NULL;
+
+  /**
+   * The list of column names in this result set.
+   *
+   * @var Array
+   */
+  protected $columnNames = NULL;
+
+  /**
+   * The number of rows affected by the last query.
+   *
+   * @var int
+   */
+  protected $rowCount = NULL;
+
+  /**
+   * The number of rows in this result set.
+   *
+   * @var int
+   */
+  protected $resultRowCount = 0;
+
+  /**
+   * Holds the current fetch style (which will be used by the next fetch).
+   * @see PDOStatement::fetch()
+   *
+   * @var int
+   */
+  protected $fetchStyle = PDO::FETCH_OBJ;
+
+  /**
+   * Holds supplementary current fetch options (which will be used by the next fetch).
+   *
+   * @var Array
+   */
+  protected $fetchOptions = array(
+    'class' => 'stdClass',
+    'constructor_args' => array(),
+    'object' => NULL,
+    'column' => 0,
+  );
+
+  /**
+   * Holds the default fetch style.
+   *
+   * @var int
+   */
+  protected $defaultFetchStyle = PDO::FETCH_OBJ;
+
+  /**
+   * Holds supplementary default fetch options.
+   *
+   * @var Array
+   */
+  protected $defaultFetchOptions = array(
+    'class' => 'stdClass',
+    'constructor_args' => array(),
+    'object' => NULL,
+    'column' => 0,
+  );
+
+  public function __construct(DatabaseConnection $connection, $query, array $driver_options = array()) {
+    $this->dbh = $connection;
+    $this->queryString = $query;
+    $this->driverOptions = $driver_options;
+  }
+
+  /**
+   * Executes a prepared statement.
+   *
+   * @param $args
+   *   An array of values with as many elements as there are bound parameters in the SQL statement being executed.
+   * @param $options
+   *   An array of options for this query.
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function execute($args = array(), $options = array()) {
+    if (isset($options['fetch'])) {
+      if (is_string($options['fetch'])) {
+        // Default to an object. Note: db fields will be added to the object
+        // before the constructor is run. If you need to assign fields after
+        // the constructor is run, see http://drupal.org/node/315092.
+        $this->setFetchMode(PDO::FETCH_CLASS, $options['fetch']);
+      }
+      else {
+        $this->setFetchMode($options['fetch']);
+      }
+    }
+
+    $logger = $this->dbh->getLogger();
+    if (!empty($logger)) {
+      $query_start = microtime(TRUE);
+    }
+
+    // Prepare the query.
+    $statement = $this->getStatement($this->queryString, $args);
+    if (!$statement) {
+      $this->throwPDOException();
+    }
+
+    $return = $statement->execute($args);
+    if (!$return) {
+      $this->throwPDOException();
+    }
+
+    // Fetch all the data from the reply, in order to release any lock
+    // as soon as possible.
+    $this->rowCount = $statement->rowCount();
+    $this->data = $statement->fetchAll(PDO::FETCH_ASSOC);
+    // Destroy the statement as soon as possible. See
+    // DatabaseConnection_sqlite::PDOPrepare() for explanation.
+    unset($statement);
+
+    $this->resultRowCount = count($this->data);
+
+    if ($this->resultRowCount) {
+      $this->columnNames = array_keys($this->data[0]);
+    }
+    else {
+      $this->columnNames = array();
+    }
+
+    if (!empty($logger)) {
+      $query_end = microtime(TRUE);
+      $logger->log($this, $args, $query_end - $query_start);
+    }
+
+    // Initialize the first row in $this->currentRow.
+    $this->next();
+
+    return $return;
+  }
+
+  /**
+   * Throw a PDO Exception based on the last PDO error.
+   */
+  protected function throwPDOException() {
+    $error_info = $this->dbh->errorInfo();
+    // We rebuild a message formatted in the same way as PDO.
+    $exception = new PDOException("SQLSTATE[" . $error_info[0] . "]: General error " . $error_info[1] . ": " . $error_info[2]);
+    $exception->errorInfo = $error_info;
+    throw $exception;
+  }
+
+  /**
+   * Grab a PDOStatement object from a given query and its arguments.
+   *
+   * Some drivers (including SQLite) will need to perform some preparation
+   * themselves to get the statement right.
+   *
+   * @param $query
+   *   The query.
+   * @param array $args
+   *   An array of arguments.
+   * @return PDOStatement
+   *   A PDOStatement object.
+   */
+  protected function getStatement($query, &$args = array()) {
+    return $this->dbh->prepare($query);
+  }
+
+  /**
+   * Return the object's SQL query string.
+   */
+  public function getQueryString() {
+    return $this->queryString;
+  }
+
+  /**
+   * @see PDOStatement::setFetchMode()
+   */
+  public function setFetchMode($fetchStyle, $a2 = NULL, $a3 = NULL) {
+    $this->defaultFetchStyle = $fetchStyle;
+    switch ($fetchStyle) {
+      case PDO::FETCH_CLASS:
+        $this->defaultFetchOptions['class'] = $a2;
+        if ($a3) {
+          $this->defaultFetchOptions['constructor_args'] = $a3;
+        }
+        break;
+      case PDO::FETCH_COLUMN:
+        $this->defaultFetchOptions['column'] = $a2;
+        break;
+      case PDO::FETCH_INTO:
+        $this->defaultFetchOptions['object'] = $a2;
+        break;
+    }
+
+    // Set the values for the next fetch.
+    $this->fetchStyle = $this->defaultFetchStyle;
+    $this->fetchOptions = $this->defaultFetchOptions;
+  }
+
+  /**
+   * Return the current row formatted according to the current fetch style.
+   *
+   * This is the core method of this class. It grabs the value at the current
+   * array position in $this->data and format it according to $this->fetchStyle
+   * and $this->fetchMode.
+   *
+   * @return
+   *  The current row formatted as requested.
+   */
+  public function current() {
+    if (isset($this->currentRow)) {
+      switch ($this->fetchStyle) {
+        case PDO::FETCH_ASSOC:
+          return $this->currentRow;
+        case PDO::FETCH_BOTH:
+          // PDO::FETCH_BOTH returns an array indexed by both the column name
+          // and the column number.
+          return $this->currentRow + array_values($this->currentRow);
+        case PDO::FETCH_NUM:
+          return array_values($this->currentRow);
+        case PDO::FETCH_LAZY:
+          // We do not do lazy as everything is fetched already. Fallback to
+          // PDO::FETCH_OBJ.
+        case PDO::FETCH_OBJ:
+          return (object) $this->currentRow;
+        case PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE:
+          $class_name = array_unshift($this->currentRow);
+          // Deliberate no break.
+        case PDO::FETCH_CLASS:
+          if (!isset($class_name)) {
+            $class_name = $this->fetchOptions['class'];
+          }
+          if (count($this->fetchOptions['constructor_args'])) {
+            $reflector = new ReflectionClass($class_name);
+            $result = $reflector->newInstanceArgs($this->fetchOptions['constructor_args']);
+          }
+          else {
+            $result = new $class_name();
+          }
+          foreach ($this->currentRow as $k => $v) {
+            $result->$k = $v;
+          }
+          return $result;
+        case PDO::FETCH_INTO:
+          foreach ($this->currentRow as $k => $v) {
+            $this->fetchOptions['object']->$k = $v;
+          }
+          return $this->fetchOptions['object'];
+        case PDO::FETCH_COLUMN:
+          if (isset($this->columnNames[$this->fetchOptions['column']])) {
+            return $this->currentRow[$k][$this->columnNames[$this->fetchOptions['column']]];
+          }
+          else {
+            return;
+          }
+      }
+    }
+  }
+
+  /* Implementations of Iterator. */
+
+  public function key() {
+    return $this->currentKey;
+  }
+
+  public function rewind() {
+    // Nothing to do: our DatabaseStatement can't be rewound.
+  }
+
+  public function next() {
+    if (!empty($this->data)) {
+      $this->currentRow = reset($this->data);
+      $this->currentKey = key($this->data);
+      unset($this->data[$this->currentKey]);
+    }
+    else {
+      $this->currentRow = NULL;
+    }
+  }
+
+  public function valid() {
+    return isset($this->currentRow);
+  }
+
+  /* Implementations of DatabaseStatementInterface. */
+
+  public function rowCount() {
+    return $this->rowCount;
+  }
+
+  public function fetch($fetch_style = NULL, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = NULL) {
+    if (isset($this->currentRow)) {
+      // Set the fetch parameter.
+      $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
+      $this->fetchOptions = $this->defaultFetchOptions;
+
+      // Grab the row in the format specified above.
+      $return = $this->current();
+      // Advance the cursor.
+      $this->next();
+
+      // Reset the fetch parameters to the value stored using setFetchMode().
+      $this->fetchStyle = $this->defaultFetchStyle;
+      $this->fetchOptions = $this->defaultFetchOptions;
+      return $return;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  public function fetchColumn($index = 0) {
+    if (isset($this->currentRow) && isset($this->columnNames[$index])) {
+      // We grab the value directly from $this->data, and format it.
+      $return = $this->currentRow[$this->columnNames[$index]];
+      $this->next();
+      return $return;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  public function fetchField($index = 0) {
+    return $this->fetchColumn($index);
+  }
+
+  public function fetchObject($class_name = NULL, $constructor_args = array()) {
+    if (isset($this->currentRow)) {
+      if (!isset($class_name)) {
+        // Directly cast to an object to avoid a function call.
+        $result = (object) $this->currentRow;
+      }
+      else {
+        $this->fetchStyle = PDO::FETCH_CLASS;
+        $this->fetchOptions = array('constructor_args' => $constructor_args);
+        // Grab the row in the format specified above.
+        $result = $this->current();
+        // Reset the fetch parameters to the value stored using setFetchMode().
+        $this->fetchStyle = $this->defaultFetchStyle;
+        $this->fetchOptions = $this->defaultFetchOptions;
+      }
+
+      $this->next();
+
+      return $result;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  public function fetchAssoc() {
+    if (isset($this->currentRow)) {
+      $result = $this->currentRow;
+      $this->next();
+      return $result;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  public function fetchAll($fetch_style = NULL, $fetch_column = NULL, $constructor_args = NULL) {
+    $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
+    $this->fetchOptions = $this->defaultFetchOptions;
+    if (isset($fetch_column)) {
+      $this->fetchOptions['column'] = $fetch_column;
+    }
+    if (isset($constructor_args)) {
+      $this->fetchOptions['constructor_args'] = $constructor_args;
+    }
+
+    $result = array();
+    // Traverse the array as PHP would have done.
+    while (isset($this->currentRow)) {
+      // Grab the row in the format specified above.
+      $result[] = $this->current();
+      $this->next();
+    }
+
+    // Reset the fetch parameters to the value stored using setFetchMode().
+    $this->fetchStyle = $this->defaultFetchStyle;
+    $this->fetchOptions = $this->defaultFetchOptions;
+    return $result;
+  }
+
+  public function fetchCol($index = 0) {
+    if (isset($this->columnNames[$index])) {
+      $column = $this->columnNames[$index];
+      $result = array();
+      // Traverse the array as PHP would have done.
+      while (isset($this->currentRow)) {
+        $result[] = $this->currentRow[$this->columnNames[$index]];
+        $this->next();
+      }
+      return $result;
+    }
+    else {
+      return array();
+    }
+  }
+
+  public function fetchAllKeyed($key_index = 0, $value_index = 1) {
+    if (!isset($this->columnNames[$key_index]) || !isset($this->columnNames[$value_index]))
+      return array();
+
+    $key = $this->columnNames[$key_index];
+    $value = $this->columnNames[$value_index];
+
+    $result = array();
+    // Traverse the array as PHP would have done.
+    while (isset($this->currentRow)) {
+      $result[$this->currentRow[$key]] = $this->currentRow[$value];
+      $this->next();
+    }
+    return $result;
+  }
+
+  public function fetchAllAssoc($key, $fetch_style = NULL) {
+    $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
+    $this->fetchOptions = $this->defaultFetchOptions;
+
+    $result = array();
+    // Traverse the array as PHP would have done.
+    while (isset($this->currentRow)) {
+      // Grab the row in its raw PDO::FETCH_ASSOC format.
+      $row = $this->currentRow;
+      // Grab the row in the format specified above.
+      $result_row = $this->current();
+      $result[$this->currentRow[$key]] = $result_row;
+      $this->next();
+    }
+
+    // Reset the fetch parameters to the value stored using setFetchMode().
+    $this->fetchStyle = $this->defaultFetchStyle;
+    $this->fetchOptions = $this->defaultFetchOptions;
+    return $result;
+  }
+
+}
+
+/**
+ * @} End of "addtogroup database".
+ */
+

+ 1964 - 0
includes/database/query.inc

@@ -0,0 +1,1964 @@
+<?php
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * @file
+ * Non-specific Database query code. Used by all engines.
+ */
+
+/**
+ * Interface for a conditional clause in a query.
+ */
+interface QueryConditionInterface {
+
+  /**
+   * Helper function: builds the most common conditional clauses.
+   *
+   * This method can take a variable number of parameters. If called with two
+   * parameters, they are taken as $field and $value with $operator having a
+   * value of IN if $value is an array and = otherwise.
+   *
+   * Do not use this method to test for NULL values. Instead, use
+   * QueryConditionInterface::isNull() or QueryConditionInterface::isNotNull().
+   *
+   * @param $field
+   *   The name of the field to check. If you would like to add a more complex
+   *   condition involving operators or functions, use where().
+   * @param $value
+   *   The value to test the field against. In most cases, this is a scalar.
+   *   For more complex options, it is an array. The meaning of each element in
+   *   the array is dependent on the $operator.
+   * @param $operator
+   *   The comparison operator, such as =, <, or >=. It also accepts more
+   *   complex options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is
+   *   an array, and = otherwise.
+   *
+   * @return QueryConditionInterface
+   *   The called object.
+   *
+   * @see QueryConditionInterface::isNull()
+   * @see QueryConditionInterface::isNotNull()
+   */
+  public function condition($field, $value = NULL, $operator = NULL);
+
+  /**
+   * Adds an arbitrary WHERE clause to the query.
+   *
+   * @param $snippet
+   *   A portion of a WHERE clause as a prepared statement. It must use named
+   *   placeholders, not ? placeholders.
+   * @param $args
+   *   An associative array of arguments.
+   *
+   * @return QueryConditionInterface
+   *   The called object.
+   */
+  public function where($snippet, $args = array());
+
+  /**
+   * Sets a condition that the specified field be NULL.
+   *
+   * @param $field
+   *   The name of the field to check.
+   *
+   * @return QueryConditionInterface
+   *   The called object.
+   */
+  public function isNull($field);
+
+  /**
+   * Sets a condition that the specified field be NOT NULL.
+   *
+   * @param $field
+   *   The name of the field to check.
+   *
+   * @return QueryConditionInterface
+   *   The called object.
+   */
+  public function isNotNull($field);
+
+  /**
+   * Sets a condition that the specified subquery returns values.
+   * 
+   * @param SelectQueryInterface $select
+   *   The subquery that must contain results.
+   *
+   * @return QueryConditionInterface
+   *   The called object.
+   */
+  public function exists(SelectQueryInterface $select);
+  
+  /**
+   * Sets a condition that the specified subquery returns no values.
+   * 
+   * @param SelectQueryInterface $select
+   *   The subquery that must not contain results.
+   *
+   * @return QueryConditionInterface
+   *   The called object.
+   */
+  public function notExists(SelectQueryInterface $select);
+  
+  /**
+   * Gets a complete list of all conditions in this conditional clause.
+   *
+   * This method returns by reference. That allows alter hooks to access the
+   * data structure directly and manipulate it before it gets compiled.
+   *
+   * The data structure that is returned is an indexed array of entries, where
+   * each entry looks like the following:
+   * @code
+   * array(
+   *   'field' => $field,
+   *   'value' => $value,
+   *   'operator' => $operator,
+   * );
+   * @endcode
+   *
+   * In the special case that $operator is NULL, the $field is taken as a raw
+   * SQL snippet (possibly containing a function) and $value is an associative
+   * array of placeholders for the snippet.
+   *
+   * There will also be a single array entry of #conjunction, which is the
+   * conjunction that will be applied to the array, such as AND.
+   */
+  public function &conditions();
+
+  /**
+   * Gets a complete list of all values to insert into the prepared statement.
+   *
+   * @return
+   *   An associative array of placeholders and values.
+   */
+  public function arguments();
+
+  /**
+   * Compiles the saved conditions for later retrieval.
+   *
+   * This method does not return anything, but simply prepares data to be
+   * retrieved via __toString() and arguments().
+   *
+   * @param $connection
+   *   The database connection for which to compile the conditionals.
+   * @param $queryPlaceholder
+   *   The query this condition belongs to. If not given, the current query is
+   *   used.
+   */
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder);
+
+  /**
+   * Check whether a condition has been previously compiled.
+   *
+   * @return
+   *   TRUE if the condition has been previously compiled.
+   */
+  public function compiled();
+}
+
+
+/**
+ * Interface for a query that can be manipulated via an alter hook.
+ */
+interface QueryAlterableInterface {
+
+  /**
+   * Adds a tag to a query.
+   *
+   * Tags are strings that identify a query. A query may have any number of
+   * tags. Tags are used to mark a query so that alter hooks may decide if they
+   * wish to take action. Tags should be all lower-case and contain only
+   * letters, numbers, and underscore, and start with a letter. That is, they
+   * should follow the same rules as PHP identifiers in general.
+   *
+   * @param $tag
+   *   The tag to add.
+   *
+   * @return QueryAlterableInterface
+   *   The called object.
+   */
+  public function addTag($tag);
+
+  /**
+   * Determines if a given query has a given tag.
+   *
+   * @param $tag
+   *   The tag to check.
+   *
+   * @return
+   *   TRUE if this query has been marked with this tag, FALSE otherwise.
+   */
+  public function hasTag($tag);
+
+  /**
+   * Determines if a given query has all specified tags.
+   *
+   * @param $tags
+   *   A variable number of arguments, one for each tag to check.
+   *
+   * @return
+   *   TRUE if this query has been marked with all specified tags, FALSE
+   *   otherwise.
+   */
+  public function hasAllTags();
+
+  /**
+   * Determines if a given query has any specified tag.
+   *
+   * @param $tags
+   *   A variable number of arguments, one for each tag to check.
+   *
+   * @return
+   *   TRUE if this query has been marked with at least one of the specified
+   *   tags, FALSE otherwise.
+   */
+  public function hasAnyTag();
+
+  /**
+   * Adds additional metadata to the query.
+   *
+   * Often, a query may need to provide additional contextual data to alter
+   * hooks. Alter hooks may then use that information to decide if and how
+   * to take action.
+   *
+   * @param $key
+   *   The unique identifier for this piece of metadata. Must be a string that
+   *   follows the same rules as any other PHP identifier.
+   * @param $object
+   *   The additional data to add to the query. May be any valid PHP variable.
+   *
+   * @return QueryAlterableInterface
+   *   The called object.
+   */
+  public function addMetaData($key, $object);
+
+  /**
+   * Retrieves a given piece of metadata.
+   *
+   * @param $key
+   *   The unique identifier for the piece of metadata to retrieve.
+   *
+   * @return
+   *   The previously attached metadata object, or NULL if one doesn't exist.
+   */
+  public function getMetaData($key);
+}
+
+/**
+ * Interface for a query that accepts placeholders.
+ */
+interface QueryPlaceholderInterface {
+
+  /**
+   * Returns a unique identifier for this object.
+   */
+  public function uniqueIdentifier();
+
+  /**
+   * Returns the next placeholder ID for the query.
+   *
+   * @return
+   *   The next available placeholder ID as an integer.
+   */
+  public function nextPlaceholder();
+}
+
+/**
+ * Base class for query builders.
+ *
+ * Note that query builders use PHP's magic __toString() method to compile the
+ * query object into a prepared statement.
+ */
+abstract class Query implements QueryPlaceholderInterface {
+
+  /**
+   * The connection object on which to run this query.
+   *
+   * @var DatabaseConnection
+   */
+  protected $connection;
+
+  /**
+   * The target of the connection object.
+   * 
+   * @var string
+   */
+  protected $connectionTarget;
+
+  /**
+   * The key of the connection object.
+   * 
+   * @var string
+   */
+  protected $connectionKey;
+
+  /**
+   * The query options to pass on to the connection object.
+   *
+   * @var array
+   */
+  protected $queryOptions;
+
+  /**
+   * A unique identifier for this query object.
+   */
+  protected $uniqueIdentifier;
+
+  /**
+   * The placeholder counter.
+   */
+  protected $nextPlaceholder = 0;
+
+  /**
+   * An array of comments that can be prepended to a query.
+   *
+   * @var array
+   */
+  protected $comments = array();
+
+  /**
+   * Constructs a Query object.
+   *
+   * @param DatabaseConnection $connection
+   *   Database connection object.
+   * @param array $options
+   *   Array of query options.
+   */
+  public function __construct(DatabaseConnection $connection, $options) {
+    $this->uniqueIdentifier = uniqid('', TRUE);
+
+    $this->connection = $connection;
+    $this->connectionKey = $this->connection->getKey();
+    $this->connectionTarget = $this->connection->getTarget();
+
+    $this->queryOptions = $options;
+  }
+
+  /**
+   * Implements the magic __sleep function to disconnect from the database.
+   */
+  public function __sleep() {
+    $keys = get_object_vars($this);
+    unset($keys['connection']);
+    return array_keys($keys);
+  }
+
+  /**
+   * Implements the magic __wakeup function to reconnect to the database.
+   */
+  public function __wakeup() {
+    $this->connection = Database::getConnection($this->connectionTarget, $this->connectionKey);
+  }
+
+  /**
+   * Implements the magic __clone function.
+   */
+  public function __clone() {
+    $this->uniqueIdentifier = uniqid('', TRUE);
+  }
+
+  /**
+   * Runs the query against the database.
+   */
+  abstract protected function execute();
+
+  /**
+   * Implements PHP magic __toString method to convert the query to a string.
+   *
+   * The toString operation is how we compile a query object to a prepared
+   * statement.
+   *
+   * @return
+   *   A prepared statement query string for this object.
+   */
+  abstract public function __toString();
+
+  /**
+   * Returns a unique identifier for this object.
+   */
+  public function uniqueIdentifier() {
+    return $this->uniqueIdentifier;
+  }
+
+  /**
+   * Gets the next placeholder value for this query object.
+   *
+   * @return int
+   *   Next placeholder value.
+   */
+  public function nextPlaceholder() {
+    return $this->nextPlaceholder++;
+  }
+
+  /**
+   * Adds a comment to the query.
+   *
+   * By adding a comment to a query, you can more easily find it in your
+   * query log or the list of active queries on an SQL server. This allows
+   * for easier debugging and allows you to more easily find where a query
+   * with a performance problem is being generated.
+   *
+   * The comment string will be sanitized to remove * / and other characters
+   * that may terminate the string early so as to avoid SQL injection attacks.
+   *
+   * @param $comment
+   *   The comment string to be inserted into the query.
+   *
+   * @return Query
+   *   The called object.
+   */
+  public function comment($comment) {
+    $this->comments[] = $comment;
+    return $this;
+  }
+
+  /**
+   * Returns a reference to the comments array for the query.
+   *
+   * Because this method returns by reference, alter hooks may edit the comments
+   * array directly to make their changes. If just adding comments, however, the
+   * use of comment() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   * @code
+   * $comments =& $query->getComments();
+   * @endcode
+   *
+   * @return
+   *   A reference to the comments array structure.
+   */
+  public function &getComments() {
+    return $this->comments;
+  }
+}
+
+/**
+ * General class for an abstracted INSERT query.
+ */
+class InsertQuery extends Query {
+
+  /**
+   * The table on which to insert.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * An array of fields on which to insert.
+   *
+   * @var array
+   */
+  protected $insertFields = array();
+
+  /**
+   * An array of fields that should be set to their database-defined defaults.
+   *
+   * @var array
+   */
+  protected $defaultFields = array();
+
+  /**
+   * A nested array of values to insert.
+   *
+   * $insertValues is an array of arrays. Each sub-array is either an
+   * associative array whose keys are field names and whose values are field
+   * values to insert, or a non-associative array of values in the same order
+   * as $insertFields.
+   *
+   * Whether multiple insert sets will be run in a single query or multiple
+   * queries is left to individual drivers to implement in whatever manner is
+   * most appropriate. The order of values in each sub-array must match the
+   * order of fields in $insertFields.
+   *
+   * @var array
+   */
+  protected $insertValues = array();
+
+  /**
+   * A SelectQuery object to fetch the rows that should be inserted.
+   *
+   * @var SelectQueryInterface
+   */
+  protected $fromQuery;
+
+  /**
+   * Constructs an InsertQuery object.
+   *
+   * @param DatabaseConnection $connection
+   *   A DatabaseConnection object.
+   * @param string $table
+   *   Name of the table to associate with this query.
+   * @param array $options
+   *   Array of database options.
+   */
+  public function __construct($connection, $table, array $options = array()) {
+    if (!isset($options['return'])) {
+      $options['return'] = Database::RETURN_INSERT_ID;
+    }
+    parent::__construct($connection, $options);
+    $this->table = $table;
+  }
+
+  /**
+   * Adds a set of field->value pairs to be inserted.
+   *
+   * This method may only be called once. Calling it a second time will be
+   * ignored. To queue up multiple sets of values to be inserted at once,
+   * use the values() method.
+   *
+   * @param $fields
+   *   An array of fields on which to insert. This array may be indexed or
+   *   associative. If indexed, the array is taken to be the list of fields.
+   *   If associative, the keys of the array are taken to be the fields and
+   *   the values are taken to be corresponding values to insert. If a
+   *   $values argument is provided, $fields must be indexed.
+   * @param $values
+   *   An array of fields to insert into the database. The values must be
+   *   specified in the same order as the $fields array.
+   *
+   * @return InsertQuery
+   *   The called object.
+   */
+  public function fields(array $fields, array $values = array()) {
+    if (empty($this->insertFields)) {
+      if (empty($values)) {
+        if (!is_numeric(key($fields))) {
+          $values = array_values($fields);
+          $fields = array_keys($fields);
+        }
+      }
+      $this->insertFields = $fields;
+      if (!empty($values)) {
+        $this->insertValues[] = $values;
+      }
+    }
+
+    return $this;
+  }
+
+  /**
+   * Adds another set of values to the query to be inserted.
+   *
+   * If $values is a numeric-keyed array, it will be assumed to be in the same
+   * order as the original fields() call. If it is associative, it may be
+   * in any order as long as the keys of the array match the names of the
+   * fields.
+   *
+   * @param $values
+   *   An array of values to add to the query.
+   *
+   * @return InsertQuery
+   *   The called object.
+   */
+  public function values(array $values) {
+    if (is_numeric(key($values))) {
+      $this->insertValues[] = $values;
+    }
+    else {
+      // Reorder the submitted values to match the fields array.
+      foreach ($this->insertFields as $key) {
+        $insert_values[$key] = $values[$key];
+      }
+      // For consistency, the values array is always numerically indexed.
+      $this->insertValues[] = array_values($insert_values);
+    }
+    return $this;
+  }
+
+  /**
+   * Specifies fields for which the database defaults should be used.
+   *
+   * If you want to force a given field to use the database-defined default,
+   * not NULL or undefined, use this method to instruct the database to use
+   * default values explicitly. In most cases this will not be necessary
+   * unless you are inserting a row that is all default values, as you cannot
+   * specify no values in an INSERT query.
+   *
+   * Specifying a field both in fields() and in useDefaults() is an error
+   * and will not execute.
+   *
+   * @param $fields
+   *   An array of values for which to use the default values
+   *   specified in the table definition.
+   *
+   * @return InsertQuery
+   *   The called object.
+   */
+  public function useDefaults(array $fields) {
+    $this->defaultFields = $fields;
+    return $this;
+  }
+
+  /**
+   * Sets the fromQuery on this InsertQuery object.
+   *
+   * @param SelectQueryInterface $query
+   *   The query to fetch the rows that should be inserted.
+   *
+   * @return InsertQuery
+   *   The called object.
+   */
+  public function from(SelectQueryInterface $query) {
+    $this->fromQuery = $query;
+    return $this;
+  }
+
+  /**
+   * Executes the insert query.
+   *
+   * @return
+   *   The last insert ID of the query, if one exists. If the query
+   *   was given multiple sets of values to insert, the return value is
+   *   undefined. If no fields are specified, this method will do nothing and
+   *   return NULL. That makes it safe to use in multi-insert loops.
+   */
+  public function execute() {
+    // If validation fails, simply return NULL. Note that validation routines
+    // in preExecute() may throw exceptions instead.
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      $sql = (string) $this;
+      // The SelectQuery may contain arguments, load and pass them through.
+      return $this->connection->query($sql, $this->fromQuery->getArguments(), $this->queryOptions);
+    }
+
+    $last_insert_id = 0;
+
+    // Each insert happens in its own query in the degenerate case. However,
+    // we wrap it in a transaction so that it is atomic where possible. On many
+    // databases, such as SQLite, this is also a notable performance boost.
+    $transaction = $this->connection->startTransaction();
+
+    try {
+      $sql = (string) $this;
+      foreach ($this->insertValues as $insert_values) {
+        $last_insert_id = $this->connection->query($sql, $insert_values, $this->queryOptions);
+      }
+    }
+    catch (Exception $e) {
+      // One of the INSERTs failed, rollback the whole batch.
+      $transaction->rollback();
+      // Rethrow the exception for the calling code.
+      throw $e;
+    }
+
+    // Re-initialize the values array so that we can re-use this query.
+    $this->insertValues = array();
+
+    // Transaction commits here where $transaction looses scope.
+
+    return $last_insert_id;
+  }
+
+  /**
+   * Implements PHP magic __toString method to convert the query to a string.
+   *
+   * @return string
+   *   The prepared statement.
+   */
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Default fields are always placed first for consistency.
+    $insert_fields = array_merge($this->defaultFields, $this->insertFields);
+
+    if (!empty($this->fromQuery)) {
+      return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+    }
+
+    // For simplicity, we will use the $placeholders array to inject
+    // default keywords even though they are not, strictly speaking,
+    // placeholders for prepared statements.
+    $placeholders = array();
+    $placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
+    $placeholders = array_pad($placeholders, count($this->insertFields), '?');
+
+    return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')';
+  }
+
+  /**
+   * Preprocesses and validates the query.
+   *
+   * @return
+   *   TRUE if the validation was successful, FALSE if not.
+   *
+   * @throws FieldsOverlapException
+   * @throws NoFieldsException
+   */
+  public function preExecute() {
+    // Confirm that the user did not try to specify an identical
+    // field and default field.
+    if (array_intersect($this->insertFields, $this->defaultFields)) {
+      throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.');
+    }
+
+    if (!empty($this->fromQuery)) {
+      // We have to assume that the used aliases match the insert fields.
+      // Regular fields are added to the query before expressions, maintain the
+      // same order for the insert fields.
+      // This behavior can be overridden by calling fields() manually as only the
+      // first call to fields() does have an effect.
+      $this->fields(array_merge(array_keys($this->fromQuery->getFields()), array_keys($this->fromQuery->getExpressions())));
+    }
+
+    // Don't execute query without fields.
+    if (count($this->insertFields) + count($this->defaultFields) == 0) {
+      throw new NoFieldsException('There are no fields available to insert with.');
+    }
+
+    // If no values have been added, silently ignore this query. This can happen
+    // if values are added conditionally, so we don't want to throw an
+    // exception.
+    if (!isset($this->insertValues[0]) && count($this->insertFields) > 0 && empty($this->fromQuery)) {
+      return FALSE;
+    }
+    return TRUE;
+  }
+}
+
+/**
+ * General class for an abstracted DELETE operation.
+ */
+class DeleteQuery extends Query implements QueryConditionInterface {
+
+  /**
+   * The table from which to delete.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * The condition object for this query.
+   *
+   * Condition handling is handled via composition.
+   *
+   * @var DatabaseCondition
+   */
+  protected $condition;
+
+  /**
+   * Constructs a DeleteQuery object.
+   *
+   * @param DatabaseConnection $connection
+   *   A DatabaseConnection object.
+   * @param string $table
+   *   Name of the table to associate with this query.
+   * @param array $options
+   *   Array of database options.
+   */
+  public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
+    $options['return'] = Database::RETURN_AFFECTED;
+    parent::__construct($connection, $options);
+    $this->table = $table;
+
+    $this->condition = new DatabaseCondition('AND');
+  }
+
+  /**
+   * Implements QueryConditionInterface::condition().
+   */
+  public function condition($field, $value = NULL, $operator = NULL) {
+    $this->condition->condition($field, $value, $operator);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNull().
+   */
+  public function isNull($field) {
+    $this->condition->isNull($field);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNotNull().
+   */
+  public function isNotNull($field) {
+    $this->condition->isNotNull($field);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::exists().
+   */
+  public function exists(SelectQueryInterface $select) {
+    $this->condition->exists($select);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::notExists().
+   */
+  public function notExists(SelectQueryInterface $select) {
+    $this->condition->notExists($select);
+    return $this;
+  }
+  
+  /**
+   * Implements QueryConditionInterface::conditions().
+   */
+  public function &conditions() {
+    return $this->condition->conditions();
+  }
+
+  /**
+   * Implements QueryConditionInterface::arguments().
+   */
+  public function arguments() {
+    return $this->condition->arguments();
+  }
+
+  /**
+   * Implements QueryConditionInterface::where().
+   */
+  public function where($snippet, $args = array()) {
+    $this->condition->where($snippet, $args);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::compile().
+   */
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
+    return $this->condition->compile($connection, $queryPlaceholder);
+  }
+
+  /**
+   * Implements QueryConditionInterface::compiled().
+   */
+  public function compiled() {
+    return $this->condition->compiled();
+  }
+
+  /**
+   * Executes the DELETE query.
+   *
+   * @return
+   *   The return value is dependent on the database connection.
+   */
+  public function execute() {
+    $values = array();
+    if (count($this->condition)) {
+      $this->condition->compile($this->connection, $this);
+      $values = $this->condition->arguments();
+    }
+
+    return $this->connection->query((string) $this, $values, $this->queryOptions);
+  }
+
+  /**
+   * Implements PHP magic __toString method to convert the query to a string.
+   *
+   * @return string
+   *   The prepared statement.
+   */
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    $query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
+
+    if (count($this->condition)) {
+
+      $this->condition->compile($this->connection, $this);
+      $query .= "\nWHERE " . $this->condition;
+    }
+
+    return $query;
+  }
+}
+
+
+/**
+ * General class for an abstracted TRUNCATE operation.
+ */
+class TruncateQuery extends Query {
+
+  /**
+   * The table to truncate.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * Constructs a TruncateQuery object.
+   *
+   * @param DatabaseConnection $connection
+   *   A DatabaseConnection object.
+   * @param string $table
+   *   Name of the table to associate with this query.
+   * @param array $options
+   *   Array of database options.
+   */
+  public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
+    $options['return'] = Database::RETURN_AFFECTED;
+    parent::__construct($connection, $options);
+    $this->table = $table;
+  }
+
+  /**
+   * Implements QueryConditionInterface::compile().
+   */
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
+    return $this->condition->compile($connection, $queryPlaceholder);
+  }
+
+  /**
+   * Implements QueryConditionInterface::compiled().
+   */
+  public function compiled() {
+    return $this->condition->compiled();
+  }
+
+  /**
+   * Executes the TRUNCATE query.
+   *
+   * @return
+   *   Return value is dependent on the database type.
+   */
+  public function execute() {
+    return $this->connection->query((string) $this, array(), $this->queryOptions);
+  }
+
+  /**
+   * Implements PHP magic __toString method to convert the query to a string.
+   *
+   * @return string
+   *   The prepared statement.
+   */
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} ';
+  }
+}
+
+/**
+ * General class for an abstracted UPDATE operation.
+ */
+class UpdateQuery extends Query implements QueryConditionInterface {
+
+  /**
+   * The table to update.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * An array of fields that will be updated.
+   *
+   * @var array
+   */
+  protected $fields = array();
+
+  /**
+   * An array of values to update to.
+   *
+   * @var array
+   */
+  protected $arguments = array();
+
+  /**
+   * The condition object for this query.
+   *
+   * Condition handling is handled via composition.
+   *
+   * @var DatabaseCondition
+   */
+  protected $condition;
+
+  /**
+   * Array of fields to update to an expression in case of a duplicate record.
+   *
+   * This variable is a nested array in the following format:
+   * @code
+   * <some field> => array(
+   *  'condition' => <condition to execute, as a string>,
+   *  'arguments' => <array of arguments for condition, or NULL for none>,
+   * );
+   * @endcode
+   *
+   * @var array
+   */
+  protected $expressionFields = array();
+
+  /**
+   * Constructs an UpdateQuery object.
+   *
+   * @param DatabaseConnection $connection
+   *   A DatabaseConnection object.
+   * @param string $table
+   *   Name of the table to associate with this query.
+   * @param array $options
+   *   Array of database options.
+   */
+  public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
+    $options['return'] = Database::RETURN_AFFECTED;
+    parent::__construct($connection, $options);
+    $this->table = $table;
+
+    $this->condition = new DatabaseCondition('AND');
+  }
+
+  /**
+   * Implements QueryConditionInterface::condition().
+   */
+  public function condition($field, $value = NULL, $operator = NULL) {
+    $this->condition->condition($field, $value, $operator);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNull().
+   */
+  public function isNull($field) {
+    $this->condition->isNull($field);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNotNull().
+   */
+  public function isNotNull($field) {
+    $this->condition->isNotNull($field);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::exists().
+   */
+  public function exists(SelectQueryInterface $select) {
+    $this->condition->exists($select);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::notExists().
+   */
+  public function notExists(SelectQueryInterface $select) {
+    $this->condition->notExists($select);
+    return $this;
+  }
+  
+  /**
+   * Implements QueryConditionInterface::conditions().
+   */
+  public function &conditions() {
+    return $this->condition->conditions();
+  }
+
+  /**
+   * Implements QueryConditionInterface::arguments().
+   */
+  public function arguments() {
+    return $this->condition->arguments();
+  }
+
+  /**
+   * Implements QueryConditionInterface::where().
+   */
+  public function where($snippet, $args = array()) {
+    $this->condition->where($snippet, $args);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::compile().
+   */
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
+    return $this->condition->compile($connection, $queryPlaceholder);
+  }
+
+  /**
+   * Implements QueryConditionInterface::compiled().
+   */
+  public function compiled() {
+    return $this->condition->compiled();
+  }
+
+  /**
+   * Adds a set of field->value pairs to be updated.
+   *
+   * @param $fields
+   *   An associative array of fields to write into the database. The array keys
+   *   are the field names and the values are the values to which to set them.
+   *
+   * @return UpdateQuery
+   *   The called object.
+   */
+  public function fields(array $fields) {
+    $this->fields = $fields;
+    return $this;
+  }
+
+  /**
+   * Specifies fields to be updated as an expression.
+   *
+   * Expression fields are cases such as counter=counter+1. This method takes
+   * precedence over fields().
+   *
+   * @param $field
+   *   The field to set.
+   * @param $expression
+   *   The field will be set to the value of this expression. This parameter
+   *   may include named placeholders.
+   * @param $arguments
+   *   If specified, this is an array of key/value pairs for named placeholders
+   *   corresponding to the expression.
+   *
+   * @return UpdateQuery
+   *   The called object.
+   */
+  public function expression($field, $expression, array $arguments = NULL) {
+    $this->expressionFields[$field] = array(
+      'expression' => $expression,
+      'arguments' => $arguments,
+    );
+
+    return $this;
+  }
+
+  /**
+   * Executes the UPDATE query.
+   *
+   * @return
+   *   The number of rows affected by the update.
+   */
+  public function execute() {
+
+    // Expressions take priority over literal fields, so we process those first
+    // and remove any literal fields that conflict.
+    $fields = $this->fields;
+    $update_values = array();
+    foreach ($this->expressionFields as $field => $data) {
+      if (!empty($data['arguments'])) {
+        $update_values += $data['arguments'];
+      }
+      unset($fields[$field]);
+    }
+
+    // Because we filter $fields the same way here and in __toString(), the
+    // placeholders will all match up properly.
+    $max_placeholder = 0;
+    foreach ($fields as $field => $value) {
+      $update_values[':db_update_placeholder_' . ($max_placeholder++)] = $value;
+    }
+
+    if (count($this->condition)) {
+      $this->condition->compile($this->connection, $this);
+      $update_values = array_merge($update_values, $this->condition->arguments());
+    }
+
+    return $this->connection->query((string) $this, $update_values, $this->queryOptions);
+  }
+
+  /**
+   * Implements PHP magic __toString method to convert the query to a string.
+   *
+   * @return string
+   *   The prepared statement.
+   */
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Expressions take priority over literal fields, so we process those first
+    // and remove any literal fields that conflict.
+    $fields = $this->fields;
+    $update_fields = array();
+    foreach ($this->expressionFields as $field => $data) {
+      $update_fields[] = $field . '=' . $data['expression'];
+      unset($fields[$field]);
+    }
+
+    $max_placeholder = 0;
+    foreach ($fields as $field => $value) {
+      $update_fields[] = $field . '=:db_update_placeholder_' . ($max_placeholder++);
+    }
+
+    $query = $comments . 'UPDATE {' . $this->connection->escapeTable($this->table) . '} SET ' . implode(', ', $update_fields);
+
+    if (count($this->condition)) {
+      $this->condition->compile($this->connection, $this);
+      // There is an implicit string cast on $this->condition.
+      $query .= "\nWHERE " . $this->condition;
+    }
+
+    return $query;
+  }
+
+}
+
+/**
+ * General class for an abstracted MERGE query operation.
+ *
+ * An ANSI SQL:2003 compatible database would run the following query:
+ *
+ * @code
+ * MERGE INTO table_name_1 USING table_name_2 ON (condition)
+ *   WHEN MATCHED THEN
+ *   UPDATE SET column1 = value1 [, column2 = value2 ...]
+ *   WHEN NOT MATCHED THEN
+ *   INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...
+ * @endcode
+ *
+ * Other databases (most notably MySQL, PostgreSQL and SQLite) will emulate
+ * this statement by running a SELECT and then INSERT or UPDATE.
+ *
+ * By default, the two table names are identical and they are passed into the
+ * the constructor. table_name_2 can be specified by the
+ * MergeQuery::conditionTable() method. It can be either a string or a
+ * subquery.
+ *
+ * The condition is built exactly like SelectQuery or UpdateQuery conditions,
+ * the UPDATE query part is built similarly like an UpdateQuery and finally the
+ * INSERT query part is built similarly like an InsertQuery. However, both
+ * UpdateQuery and InsertQuery has a fields method so
+ * MergeQuery::updateFields() and MergeQuery::insertFields() needs to be called
+ * instead. MergeQuery::fields() can also be called which calls both of these
+ * methods as the common case is to use the same column-value pairs for both
+ * INSERT and UPDATE. However, this is not mandatory. Another convinient
+ * wrapper is MergeQuery::key() which adds the same column-value pairs to the
+ * condition and the INSERT query part.
+ *
+ * Several methods (key(), fields(), insertFields()) can be called to set a
+ * key-value pair for the INSERT query part. Subsequent calls for the same
+ * fields override the earlier ones. The same is true for UPDATE and key(),
+ * fields() and updateFields().
+ */
+class MergeQuery extends Query implements QueryConditionInterface {
+  /**
+   * Returned by execute() if an INSERT query has been executed.
+   */
+  const STATUS_INSERT = 1;
+
+  /**
+   * Returned by execute() if an UPDATE query has been executed.
+   */
+  const STATUS_UPDATE = 2;
+
+  /**
+   * The table to be used for INSERT and UPDATE.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * The table or subquery to be used for the condition.
+   */
+  protected $conditionTable;
+
+  /**
+   * An array of fields on which to insert.
+   *
+   * @var array
+   */
+  protected $insertFields = array();
+
+  /**
+   * An array of fields which should be set to their database-defined defaults.
+   *
+   * Used on INSERT.
+   *
+   * @var array
+   */
+  protected $defaultFields = array();
+
+  /**
+   * An array of values to be inserted.
+   *
+   * @var string
+   */
+  protected $insertValues = array();
+
+  /**
+   * An array of fields that will be updated.
+   *
+   * @var array
+   */
+  protected $updateFields = array();
+
+  /**
+   * Array of fields to update to an expression in case of a duplicate record.
+   *
+   * This variable is a nested array in the following format:
+   * @code
+   * <some field> => array(
+   *  'condition' => <condition to execute, as a string>,
+   *  'arguments' => <array of arguments for condition, or NULL for none>,
+   * );
+   * @endcode
+   *
+   * @var array
+   */
+  protected $expressionFields = array();
+
+  /**
+   * Flag indicating whether an UPDATE is necessary.
+   *
+   * @var boolean
+   */
+  protected $needsUpdate = FALSE;
+
+  /**
+  * Constructs a MergeQuery object.
+  *
+  * @param DatabaseConnection $connection
+  *   A DatabaseConnection object.
+  * @param string $table
+  *   Name of the table to associate with this query.
+  * @param array $options
+  *   Array of database options.
+  */
+  public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
+    $options['return'] = Database::RETURN_AFFECTED;
+    parent::__construct($connection, $options);
+    $this->table = $table;
+    $this->conditionTable = $table;
+    $this->condition = new DatabaseCondition('AND');
+  }
+
+  /**
+   * Sets the table or subquery to be used for the condition.
+   *
+   * @param $table
+   *   The table name or the subquery to be used. Use a SelectQuery object to
+   *   pass in a subquery.
+   *
+   * @return MergeQuery
+   *   The called object.
+   */
+  protected function conditionTable($table) {
+    $this->conditionTable = $table;
+    return $this;
+  }
+
+  /**
+   * Adds a set of field->value pairs to be updated.
+   *
+   * @param $fields
+   *   An associative array of fields to write into the database. The array keys
+   *   are the field names and the values are the values to which to set them.
+   *
+   * @return MergeQuery
+   *   The called object.
+   */
+  public function updateFields(array $fields) {
+    $this->updateFields = $fields;
+    $this->needsUpdate = TRUE;
+    return $this;
+  }
+
+  /**
+   * Specifies fields to be updated as an expression.
+   *
+   * Expression fields are cases such as counter = counter + 1. This method
+   * takes precedence over MergeQuery::updateFields() and it's wrappers,
+   * MergeQuery::key() and MergeQuery::fields().
+   *
+   * @param $field
+   *   The field to set.
+   * @param $expression
+   *   The field will be set to the value of this expression. This parameter
+   *   may include named placeholders.
+   * @param $arguments
+   *   If specified, this is an array of key/value pairs for named placeholders
+   *   corresponding to the expression.
+   *
+   * @return MergeQuery
+   *   The called object.
+   */
+  public function expression($field, $expression, array $arguments = NULL) {
+    $this->expressionFields[$field] = array(
+      'expression' => $expression,
+      'arguments' => $arguments,
+    );
+    $this->needsUpdate = TRUE;
+    return $this;
+  }
+
+  /**
+   * Adds a set of field->value pairs to be inserted.
+   *
+   * @param $fields
+   *   An array of fields on which to insert. This array may be indexed or
+   *   associative. If indexed, the array is taken to be the list of fields.
+   *   If associative, the keys of the array are taken to be the fields and
+   *   the values are taken to be corresponding values to insert. If a
+   *   $values argument is provided, $fields must be indexed.
+   * @param $values
+   *   An array of fields to insert into the database. The values must be
+   *   specified in the same order as the $fields array.
+   *
+   * @return MergeQuery
+   *   The called object.
+   */
+  public function insertFields(array $fields, array $values = array()) {
+    if ($values) {
+      $fields = array_combine($fields, $values);
+    }
+    $this->insertFields = $fields;
+    return $this;
+  }
+
+  /**
+   * Specifies fields for which the database-defaults should be used.
+   *
+   * If you want to force a given field to use the database-defined default,
+   * not NULL or undefined, use this method to instruct the database to use
+   * default values explicitly. In most cases this will not be necessary
+   * unless you are inserting a row that is all default values, as you cannot
+   * specify no values in an INSERT query.
+   *
+   * Specifying a field both in fields() and in useDefaults() is an error
+   * and will not execute.
+   *
+   * @param $fields
+   *   An array of values for which to use the default values
+   *   specified in the table definition.
+   *
+   * @return MergeQuery
+   *   The called object.
+   */
+  public function useDefaults(array $fields) {
+    $this->defaultFields = $fields;
+    return $this;
+  }
+
+  /**
+   * Sets common field-value pairs in the INSERT and UPDATE query parts.
+   *
+   * This method should only be called once. It may be called either
+   * with a single associative array or two indexed arrays. If called
+   * with an associative array, the keys are taken to be the fields
+   * and the values are taken to be the corresponding values to set.
+   * If called with two arrays, the first array is taken as the fields
+   * and the second array is taken as the corresponding values.
+   *
+   * @param $fields
+   *   An array of fields to insert, or an associative array of fields and
+   *   values. The keys of the array are taken to be the fields and the values
+   *   are taken to be corresponding values to insert.
+   * @param $values
+   *   An array of values to set into the database. The values must be
+   *   specified in the same order as the $fields array.
+   *
+   * @return MergeQuery
+   *   The called object.
+   */
+  public function fields(array $fields, array $values = array()) {
+    if ($values) {
+      $fields = array_combine($fields, $values);
+    }
+    foreach ($fields as $key => $value) {
+      $this->insertFields[$key] = $value;
+      $this->updateFields[$key] = $value;
+    }
+    $this->needsUpdate = TRUE;
+    return $this;
+  }
+
+  /**
+   * Sets the key field(s) to be used as conditions for this query.
+   *
+   * This method should only be called once. It may be called either
+   * with a single associative array or two indexed arrays. If called
+   * with an associative array, the keys are taken to be the fields
+   * and the values are taken to be the corresponding values to set.
+   * If called with two arrays, the first array is taken as the fields
+   * and the second array is taken as the corresponding values.
+   *
+   * The fields are copied to the condition of the query and the INSERT part.
+   * If no other method is called, the UPDATE will become a no-op.
+   *
+   * @param $fields
+   *   An array of fields to set, or an associative array of fields and values.
+   * @param $values
+   *   An array of values to set into the database. The values must be
+   *   specified in the same order as the $fields array.
+   *
+   * @return MergeQuery
+   *   The called object.
+   */
+  public function key(array $fields, array $values = array()) {
+    if ($values) {
+      $fields = array_combine($fields, $values);
+    }
+    foreach ($fields as $key => $value) {
+      $this->insertFields[$key] = $value;
+      $this->condition($key, $value);
+    }
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::condition().
+   */
+  public function condition($field, $value = NULL, $operator = NULL) {
+    $this->condition->condition($field, $value, $operator);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNull().
+   */
+  public function isNull($field) {
+    $this->condition->isNull($field);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNotNull().
+   */
+  public function isNotNull($field) {
+    $this->condition->isNotNull($field);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::exists().
+   */
+  public function exists(SelectQueryInterface $select) {
+    $this->condition->exists($select);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::notExists().
+   */
+  public function notExists(SelectQueryInterface $select) {
+    $this->condition->notExists($select);
+    return $this;
+  }
+  
+  /**
+   * Implements QueryConditionInterface::conditions().
+   */
+  public function &conditions() {
+    return $this->condition->conditions();
+  }
+
+  /**
+   * Implements QueryConditionInterface::arguments().
+   */
+  public function arguments() {
+    return $this->condition->arguments();
+  }
+
+  /**
+   * Implements QueryConditionInterface::where().
+   */
+  public function where($snippet, $args = array()) {
+    $this->condition->where($snippet, $args);
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::compile().
+   */
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
+    return $this->condition->compile($connection, $queryPlaceholder);
+  }
+
+  /**
+   * Implements QueryConditionInterface::compiled().
+   */
+  public function compiled() {
+    return $this->condition->compiled();
+  }
+
+  /**
+   * Implements PHP magic __toString method to convert the query to a string.
+   *
+   * In the degenerate case, there is no string-able query as this operation
+   * is potentially two queries.
+   *
+   * @return string
+   *   The prepared query statement.
+   */
+  public function __toString() {
+  }
+
+  public function execute() {
+    // Wrap multiple queries in a transaction, if the database supports it.
+    $transaction = $this->connection->startTransaction();
+    try {
+      if (!count($this->condition)) {
+        throw new InvalidMergeQueryException(t('Invalid merge query: no conditions'));
+      }
+      $select = $this->connection->select($this->conditionTable)
+        ->condition($this->condition)
+        ->forUpdate();
+      $select->addExpression('1');
+      if (!$select->execute()->fetchField()) {
+        try {
+          $insert = $this->connection->insert($this->table)->fields($this->insertFields);
+          if ($this->defaultFields) {
+            $insert->useDefaults($this->defaultFields);
+          }
+          $insert->execute();
+          return MergeQuery::STATUS_INSERT;
+        }
+        catch (Exception $e) {
+          // The insert query failed, maybe it's because a racing insert query
+          // beat us in inserting the same row. Retry the select query, if it
+          // returns a row, ignore the error and continue with the update
+          // query below.
+          if (!$select->execute()->fetchField()) {
+            throw $e;
+          }
+        }
+      }
+      if ($this->needsUpdate) {
+        $update = $this->connection->update($this->table)
+          ->fields($this->updateFields)
+          ->condition($this->condition);
+        if ($this->expressionFields) {
+          foreach ($this->expressionFields as $field => $data) {
+            $update->expression($field, $data['expression'], $data['arguments']);
+          }
+        }
+        $update->execute();
+        return MergeQuery::STATUS_UPDATE;
+      }
+    }
+    catch (Exception $e) {
+      // Something really wrong happened here, bubble up the exception to the
+      // caller.
+      $transaction->rollback();
+      throw $e;
+    }
+    // Transaction commits here where $transaction looses scope.
+  }
+}
+
+/**
+ * Generic class for a series of conditions in a query.
+ */
+class DatabaseCondition implements QueryConditionInterface, Countable {
+
+  /**
+   * Array of conditions.
+   *
+   * @var array
+   */
+  protected $conditions = array();
+
+  /**
+   * Array of arguments.
+   *
+   * @var array
+   */
+  protected $arguments = array();
+
+  /**
+   * Whether the conditions have been changed.
+   *
+   * TRUE if the condition has been changed since the last compile.
+   * FALSE if the condition has been compiled and not changed.
+   *
+   * @var bool
+   */
+  protected $changed = TRUE;
+
+  /**
+   * The identifier of the query placeholder this condition has been compiled against.
+   */
+  protected $queryPlaceholderIdentifier;
+
+  /**
+   * Constructs a DataBaseCondition object.
+   *
+   * @param string $conjunction
+   *   The operator to use to combine conditions: 'AND' or 'OR'.
+   */
+  public function __construct($conjunction) {
+    $this->conditions['#conjunction'] = $conjunction;
+  }
+
+  /**
+   * Implements Countable::count().
+   *
+   * Returns the size of this conditional. The size of the conditional is the
+   * size of its conditional array minus one, because one element is the the
+   * conjunction.
+   */
+  public function count() {
+    return count($this->conditions) - 1;
+  }
+
+  /**
+   * Implements QueryConditionInterface::condition().
+   */
+  public function condition($field, $value = NULL, $operator = NULL) {
+    if (!isset($operator)) {
+      if (is_array($value)) {
+        $operator = 'IN';
+      }
+      elseif (!isset($value)) {
+        $operator = 'IS NULL';
+      }
+      else {
+        $operator = '=';
+      }
+    }
+    $this->conditions[] = array(
+      'field' => $field,
+      'value' => $value,
+      'operator' => $operator,
+    );
+
+    $this->changed = TRUE;
+
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::where().
+   */
+  public function where($snippet, $args = array()) {
+    $this->conditions[] = array(
+      'field' => $snippet,
+      'value' => $args,
+      'operator' => NULL,
+    );
+    $this->changed = TRUE;
+
+    return $this;
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNull().
+   */
+  public function isNull($field) {
+    return $this->condition($field);
+  }
+
+  /**
+   * Implements QueryConditionInterface::isNotNull().
+   */
+  public function isNotNull($field) {
+    return $this->condition($field, NULL, 'IS NOT NULL');
+  }
+
+  /**
+   * Implements QueryConditionInterface::exists().
+   */
+  public function exists(SelectQueryInterface $select) {
+    return $this->condition('', $select, 'EXISTS');
+  }
+  
+  /**
+   * Implements QueryConditionInterface::notExists().
+   */
+  public function notExists(SelectQueryInterface $select) {
+    return $this->condition('', $select, 'NOT EXISTS');
+  }
+  
+  /**
+   * Implements QueryConditionInterface::conditions().
+   */
+  public function &conditions() {
+    return $this->conditions;
+  }
+
+  /**
+   * Implements QueryConditionInterface::arguments().
+   */
+  public function arguments() {
+    // If the caller forgot to call compile() first, refuse to run.
+    if ($this->changed) {
+      return NULL;
+    }
+    return $this->arguments;
+  }
+
+  /**
+   * Implements QueryConditionInterface::compile().
+   */
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
+    // Re-compile if this condition changed or if we are compiled against a
+    // different query placeholder object.
+    if ($this->changed || isset($this->queryPlaceholderIdentifier) && ($this->queryPlaceholderIdentifier != $queryPlaceholder->uniqueIdentifier())) {
+      $this->queryPlaceholderIdentifier = $queryPlaceholder->uniqueIdentifier();
+
+      $condition_fragments = array();
+      $arguments = array();
+
+      $conditions = $this->conditions;
+      $conjunction = $conditions['#conjunction'];
+      unset($conditions['#conjunction']);
+      foreach ($conditions as $condition) {
+        if (empty($condition['operator'])) {
+          // This condition is a literal string, so let it through as is.
+          $condition_fragments[] = ' (' . $condition['field'] . ') ';
+          $arguments += $condition['value'];
+        }
+        else {
+          // It's a structured condition, so parse it out accordingly.
+          // Note that $condition['field'] will only be an object for a dependent
+          // DatabaseCondition object, not for a dependent subquery.
+          if ($condition['field'] instanceof QueryConditionInterface) {
+            // Compile the sub-condition recursively and add it to the list.
+            $condition['field']->compile($connection, $queryPlaceholder);
+            $condition_fragments[] = '(' . (string) $condition['field'] . ')';
+            $arguments += $condition['field']->arguments();
+          }
+          else {
+            // For simplicity, we treat all operators as the same data structure.
+            // In the typical degenerate case, this won't get changed.
+            $operator_defaults = array(
+              'prefix' => '',
+              'postfix' => '',
+              'delimiter' => '',
+              'operator' => $condition['operator'],
+              'use_value' => TRUE,
+            );
+            $operator = $connection->mapConditionOperator($condition['operator']);
+            if (!isset($operator)) {
+              $operator = $this->mapConditionOperator($condition['operator']);
+            }
+            $operator += $operator_defaults;
+
+            $placeholders = array();
+            if ($condition['value'] instanceof SelectQueryInterface) {
+              $condition['value']->compile($connection, $queryPlaceholder);
+              $placeholders[] = (string) $condition['value'];
+              $arguments += $condition['value']->arguments();
+              // Subqueries are the actual value of the operator, we don't
+              // need to add another below.
+              $operator['use_value'] = FALSE;
+            }
+            // We assume that if there is a delimiter, then the value is an
+            // array. If not, it is a scalar. For simplicity, we first convert
+            // up to an array so that we can build the placeholders in the same way.
+            elseif (!$operator['delimiter']) {
+              $condition['value'] = array($condition['value']);
+            }
+            if ($operator['use_value']) {
+              foreach ($condition['value'] as $value) {
+                $placeholder = ':db_condition_placeholder_' . $queryPlaceholder->nextPlaceholder();
+                $arguments[$placeholder] = $value;
+                $placeholders[] = $placeholder;
+              }
+            }
+            $condition_fragments[] = ' (' . $connection->escapeField($condition['field']) . ' ' . $operator['operator'] . ' ' . $operator['prefix'] . implode($operator['delimiter'], $placeholders) . $operator['postfix'] . ') ';
+          }
+        }
+      }
+
+      $this->changed = FALSE;
+      $this->stringVersion = implode($conjunction, $condition_fragments);
+      $this->arguments = $arguments;
+    }
+  }
+
+  /**
+   * Implements QueryConditionInterface::compiled().
+   */
+  public function compiled() {
+    return !$this->changed;
+  }
+
+  /**
+   * Implements PHP magic __toString method to convert the conditions to string.
+   *
+   * @return string
+   *   A string version of the conditions.
+   */
+  public function __toString() {
+    // If the caller forgot to call compile() first, refuse to run.
+    if ($this->changed) {
+      return NULL;
+    }
+    return $this->stringVersion;
+  }
+
+  /**
+   * PHP magic __clone() method.
+   *
+   * Only copies fields that implement QueryConditionInterface. Also sets
+   * $this->changed to TRUE.
+   */
+  function __clone() {
+    $this->changed = TRUE;
+    foreach ($this->conditions as $key => $condition) {
+      if ($key !== '#conjunction') {
+        if ($condition['field'] instanceOf QueryConditionInterface) {
+          $this->conditions[$key]['field'] = clone($condition['field']);
+        }
+        if ($condition['value'] instanceOf SelectQueryInterface) {
+          $this->conditions[$key]['value'] = clone($condition['value']);
+        }
+      }
+    }
+  }
+
+  /**
+   * Gets any special processing requirements for the condition operator.
+   *
+   * Some condition types require special processing, such as IN, because
+   * the value data they pass in is not a simple value. This is a simple
+   * overridable lookup function.
+   *
+   * @param $operator
+   *   The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive.
+   *
+   * @return
+   *   The extra handling directives for the specified operator, or NULL.
+   */
+  protected function mapConditionOperator($operator) {
+    // $specials does not use drupal_static as its value never changes.
+    static $specials = array(
+      'BETWEEN' => array('delimiter' => ' AND '),
+      'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
+      'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
+      'EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
+      'NOT EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
+      'IS NULL' => array('use_value' => FALSE),
+      'IS NOT NULL' => array('use_value' => FALSE),
+      // Use backslash for escaping wildcard characters.
+      'LIKE' => array('postfix' => " ESCAPE '\\\\'"),
+      'NOT LIKE' => array('postfix' => " ESCAPE '\\\\'"),
+      // These ones are here for performance reasons.
+      '=' => array(),
+      '<' => array(),
+      '>' => array(),
+      '>=' => array(),
+      '<=' => array(),
+    );
+    if (isset($specials[$operator])) {
+      $return = $specials[$operator];
+    }
+    else {
+      // We need to upper case because PHP index matches are case sensitive but
+      // do not need the more expensive drupal_strtoupper because SQL statements are ASCII.
+      $operator = strtoupper($operator);
+      $return = isset($specials[$operator]) ? $specials[$operator] : array();
+    }
+
+    $return += array('operator' => $operator);
+
+    return $return;
+  }
+
+}
+
+/**
+ * @} End of "addtogroup database".
+ */

+ 727 - 0
includes/database/schema.inc

@@ -0,0 +1,727 @@
+<?php
+
+/**
+ * @file
+ * Generic Database schema code.
+ */
+
+require_once dirname(__FILE__) . '/query.inc';
+
+/**
+ * @defgroup schemaapi Schema API
+ * @{
+ * API to handle database schemas.
+ *
+ * A Drupal schema definition is an array structure representing one or
+ * more tables and their related keys and indexes. A schema is defined by
+ * hook_schema(), which usually lives in a modulename.install file.
+ *
+ * By implementing hook_schema() and specifying the tables your module
+ * declares, you can easily create and drop these tables on all
+ * supported database engines. You don't have to deal with the
+ * different SQL dialects for table creation and alteration of the
+ * supported database engines.
+ *
+ * hook_schema() should return an array with a key for each table that
+ * the module defines.
+ *
+ * The following keys are defined:
+ *   - 'description': A string in non-markup plain text describing this table
+ *     and its purpose. References to other tables should be enclosed in
+ *     curly-brackets. For example, the node_revisions table
+ *     description field might contain "Stores per-revision title and
+ *     body data for each {node}."
+ *   - 'fields': An associative array ('fieldname' => specification)
+ *     that describes the table's database columns. The specification
+ *     is also an array. The following specification parameters are defined:
+ *     - 'description': A string in non-markup plain text describing this field
+ *       and its purpose. References to other tables should be enclosed in
+ *       curly-brackets. For example, the node table vid field
+ *       description might contain "Always holds the largest (most
+ *       recent) {node_revision}.vid value for this nid."
+ *     - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
+ *       'float', 'numeric', or 'serial'. Most types just map to the according
+ *       database engine specific datatypes. Use 'serial' for auto incrementing
+ *       fields. This will expand to 'INT auto_increment' on MySQL.
+ *     - 'mysql_type', 'pgsql_type', 'sqlite_type', etc.: If you need to
+ *       use a record type not included in the officially supported list
+ *       of types above, you can specify a type for each database
+ *       backend. In this case, you can leave out the type parameter,
+ *       but be advised that your schema will fail to load on backends that
+ *       do not have a type specified. A possible solution can be to
+ *       use the "text" type as a fallback.
+ *     - 'serialize': A boolean indicating whether the field will be stored as
+ *       a serialized string.
+ *     - 'size': The data size: 'tiny', 'small', 'medium', 'normal',
+ *       'big'. This is a hint about the largest value the field will
+ *       store and determines which of the database engine specific
+ *       datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT).
+ *       'normal', the default, selects the base type (e.g. on MySQL,
+ *       INT, VARCHAR, BLOB, etc.).
+ *       Not all sizes are available for all data types. See
+ *       DatabaseSchema::getFieldTypeMap() for possible combinations.
+ *     - 'not null': If true, no NULL values will be allowed in this
+ *       database column. Defaults to false.
+ *     - 'default': The field's default value. The PHP type of the
+ *       value matters: '', '0', and 0 are all different. If you
+ *       specify '0' as the default value for a type 'int' field it
+ *       will not work because '0' is a string containing the
+ *       character "zero", not an integer.
+ *     - 'length': The maximal length of a type 'char', 'varchar' or 'text'
+ *       field. Ignored for other field types.
+ *     - 'unsigned': A boolean indicating whether a type 'int', 'float'
+ *       and 'numeric' only is signed or unsigned. Defaults to
+ *       FALSE. Ignored for other field types.
+ *     - 'precision', 'scale': For type 'numeric' fields, indicates
+ *       the precision (total number of significant digits) and scale
+ *       (decimal digits right of the decimal point). Both values are
+ *       mandatory. Ignored for other field types.
+ *     - 'binary': A boolean indicating that MySQL should force 'char',
+ *       'varchar' or 'text' fields to use case-sensitive binary collation.
+ *       This has no effect on other database types for which case sensitivity
+ *       is already the default behavior.
+ *     All parameters apart from 'type' are optional except that type
+ *     'numeric' columns must specify 'precision' and 'scale', and type
+ *     'varchar' must specify the 'length' parameter.
+ *  - 'primary key': An array of one or more key column specifiers (see below)
+ *    that form the primary key.
+ *  - 'unique keys': An associative array of unique keys ('keyname' =>
+ *    specification). Each specification is an array of one or more
+ *    key column specifiers (see below) that form a unique key on the table.
+ *  - 'foreign keys': An associative array of relations ('my_relation' =>
+ *    specification). Each specification is an array containing the name of
+ *    the referenced table ('table'), and an array of column mappings
+ *    ('columns'). Column mappings are defined by key pairs ('source_column' =>
+ *    'referenced_column').
+ *  - 'indexes':  An associative array of indexes ('indexname' =>
+ *    specification). Each specification is an array of one or more
+ *    key column specifiers (see below) that form an index on the
+ *    table.
+ *
+ * A key column specifier is either a string naming a column or an
+ * array of two elements, column name and length, specifying a prefix
+ * of the named column.
+ *
+ * As an example, here is a SUBSET of the schema definition for
+ * Drupal's 'node' table. It show four fields (nid, vid, type, and
+ * title), the primary key on field 'nid', a unique key named 'vid' on
+ * field 'vid', and two indexes, one named 'nid' on field 'nid' and
+ * one named 'node_title_type' on the field 'title' and the first four
+ * bytes of the field 'type':
+ *
+ * @code
+ * $schema['node'] = array(
+ *   'description' => 'The base table for nodes.',
+ *   'fields' => array(
+ *     'nid'       => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
+ *     'vid'       => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0),
+ *     'type'      => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''),
+ *     'language'  => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''),
+ *     'title'     => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''),
+ *     'uid'       => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ *     'status'    => array('type' => 'int', 'not null' => TRUE, 'default' => 1),
+ *     'created'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ *     'changed'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ *     'comment'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ *     'promote'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ *     'moderate'  => array('type' => 'int', 'not null' => TRUE,'default' => 0),
+ *     'sticky'    => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ *     'tnid'      => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
+ *     'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ *   ),
+ *   'indexes' => array(
+ *     'node_changed'        => array('changed'),
+ *     'node_created'        => array('created'),
+ *     'node_moderate'       => array('moderate'),
+ *     'node_frontpage'      => array('promote', 'status', 'sticky', 'created'),
+ *     'node_status_type'    => array('status', 'type', 'nid'),
+ *     'node_title_type'     => array('title', array('type', 4)),
+ *     'node_type'           => array(array('type', 4)),
+ *     'uid'                 => array('uid'),
+ *     'tnid'                => array('tnid'),
+ *     'translate'           => array('translate'),
+ *   ),
+ *   'unique keys' => array(
+ *     'vid' => array('vid'),
+ *   ),
+ *   'foreign keys' => array(
+ *     'node_revision' => array(
+ *       'table' => 'node_revision',
+ *       'columns' => array('vid' => 'vid'),
+ *      ),
+ *     'node_author' => array(
+ *       'table' => 'users',
+ *       'columns' => array('uid' => 'uid'),
+ *      ),
+ *    ),
+ *   'primary key' => array('nid'),
+ * );
+ * @endcode
+ *
+ * @see drupal_install_schema()
+ */
+
+abstract class DatabaseSchema implements QueryPlaceholderInterface {
+
+  protected $connection;
+
+  /**
+   * The placeholder counter.
+   */
+  protected $placeholder = 0;
+
+  /**
+   * Definition of prefixInfo array structure.
+   *
+   * Rather than redefining DatabaseSchema::getPrefixInfo() for each driver,
+   * by defining the defaultSchema variable only MySQL has to re-write the
+   * method.
+   *
+   * @see DatabaseSchema::getPrefixInfo()
+   */
+  protected $defaultSchema = 'public';
+
+  /**
+   * A unique identifier for this query object.
+   */
+  protected $uniqueIdentifier;
+
+  public function __construct($connection) {
+    $this->uniqueIdentifier = uniqid('', TRUE);
+    $this->connection = $connection;
+  }
+
+  /**
+   * Implements the magic __clone function.
+   */
+  public function __clone() {
+    $this->uniqueIdentifier = uniqid('', TRUE);
+  }
+
+  /**
+   * Implements QueryPlaceHolderInterface::uniqueIdentifier().
+   */
+  public function uniqueIdentifier() {
+    return $this->uniqueIdentifier;
+  }
+
+  /**
+   * Implements QueryPlaceHolderInterface::nextPlaceholder().
+   */
+  public function nextPlaceholder() {
+    return $this->placeholder++;
+  }
+
+  /**
+   * Get information about the table name and schema from the prefix.
+   *
+   * @param
+   *   Name of table to look prefix up for. Defaults to 'default' because thats
+   *   default key for prefix.
+   * @param $add_prefix
+   *   Boolean that indicates whether the given table name should be prefixed.
+   *
+   * @return
+   *   A keyed array with information about the schema, table name and prefix.
+   */
+  protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
+    $info = array(
+      'schema' => $this->defaultSchema,
+      'prefix' => $this->connection->tablePrefix($table),
+    );
+    if ($add_prefix) {
+      $table = $info['prefix'] . $table;
+    }
+    // If the prefix contains a period in it, then that means the prefix also
+    // contains a schema reference in which case we will change the schema key
+    // to the value before the period in the prefix. Everything after the dot
+    // will be prefixed onto the front of the table.
+    if (($pos = strpos($table, '.')) !== FALSE) {
+      // Grab everything before the period.
+      $info['schema'] = substr($table, 0, $pos);
+      // Grab everything after the dot.
+      $info['table'] = substr($table, ++$pos);
+    }
+    else {
+      $info['table'] = $table;
+    }
+    return $info;
+  }
+
+  /**
+   * Create names for indexes, primary keys and constraints.
+   *
+   * This prevents using {} around non-table names like indexes and keys.
+   */
+  function prefixNonTable($table) {
+    $args = func_get_args();
+    $info = $this->getPrefixInfo($table);
+    $args[0] = $info['table'];
+    return implode('_', $args);
+  }
+
+  /**
+   * Build a condition to match a table name against a standard information_schema.
+   *
+   * The information_schema is a SQL standard that provides information about the
+   * database server and the databases, schemas, tables, columns and users within
+   * it. This makes information_schema a useful tool to use across the drupal
+   * database drivers and is used by a few different functions. The function below
+   * describes the conditions to be meet when querying information_schema.tables
+   * for drupal tables or information associated with drupal tables. Even though
+   * this is the standard method, not all databases follow standards and so this
+   * method should be overwritten by a database driver if the database provider
+   * uses alternate methods. Because information_schema.tables is used in a few
+   * different functions, a database driver will only need to override this function
+   * to make all the others work. For example see includes/databases/mysql/schema.inc.
+   *
+   * @param $table_name
+   *   The name of the table in question.
+   * @param $operator
+   *   The operator to apply on the 'table' part of the condition.
+   * @param $add_prefix
+   *   Boolean to indicate whether the table name needs to be prefixed.
+   *
+   * @return QueryConditionInterface
+   *   A DatabaseCondition object.
+   */
+  protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
+    $info = $this->connection->getConnectionOptions();
+
+    // Retrive the table name and schema
+    $table_info = $this->getPrefixInfo($table_name, $add_prefix);
+
+    $condition = new DatabaseCondition('AND');
+    $condition->condition('table_catalog', $info['database']);
+    $condition->condition('table_schema', $table_info['schema']);
+    $condition->condition('table_name', $table_info['table'], $operator);
+    return $condition;
+  }
+
+  /**
+   * Check if a table exists.
+   *
+   * @param $table
+   *   The name of the table in drupal (no prefixing).
+   *
+   * @return
+   *   TRUE if the given table exists, otherwise FALSE.
+   */
+  public function tableExists($table) {
+    $condition = $this->buildTableNameCondition($table);
+    $condition->compile($this->connection, $this);
+    // Normally, we would heartily discourage the use of string
+    // concatenation for conditionals like this however, we
+    // couldn't use db_select() here because it would prefix
+    // information_schema.tables and the query would fail.
+    // Don't use {} around information_schema.tables table.
+    return (bool) $this->connection->query("SELECT 1 FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
+  }
+
+  /**
+   * Find all tables that are like the specified base table name.
+   *
+   * @param $table_expression
+   *   An SQL expression, for example "simpletest%" (without the quotes).
+   *   BEWARE: this is not prefixed, the caller should take care of that.
+   *
+   * @return
+   *   Array, both the keys and the values are the matching tables.
+   */
+  public function findTables($table_expression) {
+    $condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE);
+
+    $condition->compile($this->connection, $this);
+    // Normally, we would heartily discourage the use of string
+    // concatenation for conditionals like this however, we
+    // couldn't use db_select() here because it would prefix
+    // information_schema.tables and the query would fail.
+    // Don't use {} around information_schema.tables table.
+    return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
+  }
+
+  /**
+   * Check if a column exists in the given table.
+   *
+   * @param $table
+   *   The name of the table in drupal (no prefixing).
+   * @param $name
+   *   The name of the column.
+   *
+   * @return
+   *   TRUE if the given column exists, otherwise FALSE.
+   */
+  public function fieldExists($table, $column) {
+    $condition = $this->buildTableNameCondition($table);
+    $condition->condition('column_name', $column);
+    $condition->compile($this->connection, $this);
+    // Normally, we would heartily discourage the use of string
+    // concatenation for conditionals like this however, we
+    // couldn't use db_select() here because it would prefix
+    // information_schema.tables and the query would fail.
+    // Don't use {} around information_schema.columns table.
+    return (bool) $this->connection->query("SELECT 1 FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
+  }
+
+  /**
+   * Returns a mapping of Drupal schema field names to DB-native field types.
+   *
+   * Because different field types do not map 1:1 between databases, Drupal has
+   * its own normalized field type names. This function returns a driver-specific
+   * mapping table from Drupal names to the native names for each database.
+   *
+   * @return array
+   *   An array of Schema API field types to driver-specific field types.
+   */
+  abstract public function getFieldTypeMap();
+
+  /**
+   * Rename a table.
+   *
+   * @param $table
+   *   The table to be renamed.
+   * @param $new_name
+   *   The new name for the table.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table doesn't exist.
+   * @throws DatabaseSchemaObjectExistsException
+   *   If a table with the specified new name already exists.
+   */
+  abstract public function renameTable($table, $new_name);
+
+  /**
+   * Drop a table.
+   *
+   * @param $table
+   *   The table to be dropped.
+   *
+   * @return
+   *   TRUE if the table was successfully dropped, FALSE if there was no table
+   *   by that name to begin with.
+   */
+  abstract public function dropTable($table);
+
+  /**
+   * Add a new field to a table.
+   *
+   * @param $table
+   *   Name of the table to be altered.
+   * @param $field
+   *   Name of the field to be added.
+   * @param $spec
+   *   The field specification array, as taken from a schema definition.
+   *   The specification may also contain the key 'initial', the newly
+   *   created field will be set to the value of the key in all rows.
+   *   This is most useful for creating NOT NULL columns with no default
+   *   value in existing tables.
+   * @param $keys_new
+   *   Optional keys and indexes specification to be created on the
+   *   table along with adding the field. The format is the same as a
+   *   table specification but without the 'fields' element. If you are
+   *   adding a type 'serial' field, you MUST specify at least one key
+   *   or index including it in this array. See db_change_field() for more
+   *   explanation why.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table doesn't exist.
+   * @throws DatabaseSchemaObjectExistsException
+   *   If the specified table already has a field by that name.
+   */
+  abstract public function addField($table, $field, $spec, $keys_new = array());
+
+  /**
+   * Drop a field.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $field
+   *   The field to be dropped.
+   *
+   * @return
+   *   TRUE if the field was successfully dropped, FALSE if there was no field
+   *   by that name to begin with.
+   */
+  abstract public function dropField($table, $field);
+
+  /**
+   * Set the default value for a field.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $field
+   *   The field to be altered.
+   * @param $default
+   *   Default value to be set. NULL for 'default NULL'.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table or field doesn't exist.
+   */
+  abstract public function fieldSetDefault($table, $field, $default);
+
+  /**
+   * Set a field to have no default value.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $field
+   *   The field to be altered.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table or field doesn't exist.
+   */
+  abstract public function fieldSetNoDefault($table, $field);
+
+  /**
+   * Checks if an index exists in the given table.
+   *
+   * @param $table
+   *   The name of the table in drupal (no prefixing).
+   * @param $name
+   *   The name of the index in drupal (no prefixing).
+   *
+   * @return
+   *   TRUE if the given index exists, otherwise FALSE.
+   */
+  abstract public function indexExists($table, $name);
+
+  /**
+   * Add a primary key.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $fields
+   *   Fields for the primary key.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table doesn't exist.
+   * @throws DatabaseSchemaObjectExistsException
+   *   If the specified table already has a primary key.
+   */
+  abstract public function addPrimaryKey($table, $fields);
+
+  /**
+   * Drop the primary key.
+   *
+   * @param $table
+   *   The table to be altered.
+   *
+   * @return
+   *   TRUE if the primary key was successfully dropped, FALSE if there was no
+   *   primary key on this table to begin with.
+   */
+  abstract public function dropPrimaryKey($table);
+
+  /**
+   * Add a unique key.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $name
+   *   The name of the key.
+   * @param $fields
+   *   An array of field names.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table doesn't exist.
+   * @throws DatabaseSchemaObjectExistsException
+   *   If the specified table already has a key by that name.
+   */
+  abstract public function addUniqueKey($table, $name, $fields);
+
+  /**
+   * Drop a unique key.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $name
+   *   The name of the key.
+   *
+   * @return
+   *   TRUE if the key was successfully dropped, FALSE if there was no key by
+   *   that name to begin with.
+   */
+  abstract public function dropUniqueKey($table, $name);
+
+  /**
+   * Add an index.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $name
+   *   The name of the index.
+   * @param $fields
+   *   An array of field names.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table doesn't exist.
+   * @throws DatabaseSchemaObjectExistsException
+   *   If the specified table already has an index by that name.
+   */
+  abstract public function addIndex($table, $name, $fields);
+
+  /**
+   * Drop an index.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $name
+   *   The name of the index.
+   *
+   * @return
+   *   TRUE if the index was successfully dropped, FALSE if there was no index
+   *   by that name to begin with.
+   */
+  abstract public function dropIndex($table, $name);
+
+  /**
+   * Change a field definition.
+   *
+   * IMPORTANT NOTE: To maintain database portability, you have to explicitly
+   * recreate all indices and primary keys that are using the changed field.
+   *
+   * That means that you have to drop all affected keys and indexes with
+   * db_drop_{primary_key,unique_key,index}() before calling db_change_field().
+   * To recreate the keys and indices, pass the key definitions as the
+   * optional $keys_new argument directly to db_change_field().
+   *
+   * For example, suppose you have:
+   * @code
+   * $schema['foo'] = array(
+   *   'fields' => array(
+   *     'bar' => array('type' => 'int', 'not null' => TRUE)
+   *   ),
+   *   'primary key' => array('bar')
+   * );
+   * @endcode
+   * and you want to change foo.bar to be type serial, leaving it as the
+   * primary key. The correct sequence is:
+   * @code
+   * db_drop_primary_key('foo');
+   * db_change_field('foo', 'bar', 'bar',
+   *   array('type' => 'serial', 'not null' => TRUE),
+   *   array('primary key' => array('bar')));
+   * @endcode
+   *
+   * The reasons for this are due to the different database engines:
+   *
+   * On PostgreSQL, changing a field definition involves adding a new field
+   * and dropping an old one which* causes any indices, primary keys and
+   * sequences (from serial-type fields) that use the changed field to be dropped.
+   *
+   * On MySQL, all type 'serial' fields must be part of at least one key
+   * or index as soon as they are created. You cannot use
+   * db_add_{primary_key,unique_key,index}() for this purpose because
+   * the ALTER TABLE command will fail to add the column without a key
+   * or index specification. The solution is to use the optional
+   * $keys_new argument to create the key or index at the same time as
+   * field.
+   *
+   * You could use db_add_{primary_key,unique_key,index}() in all cases
+   * unless you are converting a field to be type serial. You can use
+   * the $keys_new argument in all cases.
+   *
+   * @param $table
+   *   Name of the table.
+   * @param $field
+   *   Name of the field to change.
+   * @param $field_new
+   *   New name for the field (set to the same as $field if you don't want to change the name).
+   * @param $spec
+   *   The field specification for the new field.
+   * @param $keys_new
+   *   Optional keys and indexes specification to be created on the
+   *   table along with changing the field. The format is the same as a
+   *   table specification but without the 'fields' element.
+   *
+   * @throws DatabaseSchemaObjectDoesNotExistException
+   *   If the specified table or source field doesn't exist.
+   * @throws DatabaseSchemaObjectExistsException
+   *   If the specified destination field already exists.
+   */
+  abstract public function changeField($table, $field, $field_new, $spec, $keys_new = array());
+
+  /**
+   * Create a new table from a Drupal table definition.
+   *
+   * @param $name
+   *   The name of the table to create.
+   * @param $table
+   *   A Schema API table definition array.
+   *
+   * @throws DatabaseSchemaObjectExistsException
+   *   If the specified table already exists.
+   */
+  public function createTable($name, $table) {
+    if ($this->tableExists($name)) {
+      throw new DatabaseSchemaObjectExistsException(t('Table %name already exists.', array('%name' => $name)));
+    }
+    $statements = $this->createTableSql($name, $table);
+    foreach ($statements as $statement) {
+      $this->connection->query($statement);
+    }
+  }
+
+  /**
+   * Return an array of field names from an array of key/index column specifiers.
+   *
+   * This is usually an identity function but if a key/index uses a column prefix
+   * specification, this function extracts just the name.
+   *
+   * @param $fields
+   *   An array of key/index column specifiers.
+   *
+   * @return
+   *   An array of field names.
+   */
+  public function fieldNames($fields) {
+    $return = array();
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = $field[0];
+      }
+      else {
+        $return[] = $field;
+      }
+    }
+    return $return;
+  }
+
+  /**
+   * Prepare a table or column comment for database query.
+   *
+   * @param $comment
+   *   The comment string to prepare.
+   * @param $length
+   *   Optional upper limit on the returned string length.
+   *
+   * @return
+   *   The prepared comment.
+   */
+  public function prepareComment($comment, $length = NULL) {
+    return $this->connection->quote($comment);
+  }
+}
+
+/**
+ * Exception thrown if an object being created already exists.
+ *
+ * For example, this exception should be thrown whenever there is an attempt to
+ * create a new database table, field, or index that already exists in the
+ * database schema.
+ */
+class DatabaseSchemaObjectExistsException extends Exception {}
+
+/**
+ * Exception thrown if an object being modified doesn't exist yet.
+ *
+ * For example, this exception should be thrown whenever there is an attempt to
+ * modify a database table, field, or index that does not currently exist in
+ * the database schema.
+ */
+class DatabaseSchemaObjectDoesNotExistException extends Exception {}
+
+/**
+ * @} End of "defgroup schemaapi".
+ */
+

+ 1613 - 0
includes/database/select.inc

@@ -0,0 +1,1613 @@
+<?php
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+require_once dirname(__FILE__) . '/query.inc';
+
+/**
+ * Interface for extendable query objects.
+ *
+ * "Extenders" follow the "Decorator" OOP design pattern.  That is, they wrap
+ * and "decorate" another object.  In our case, they implement the same interface
+ * as select queries and wrap a select query, to which they delegate almost all
+ * operations.  Subclasses of this class may implement additional methods or
+ * override existing methods as appropriate.  Extenders may also wrap other
+ * extender objects, allowing for arbitrarily complex "enhanced" queries.
+ */
+interface QueryExtendableInterface {
+
+  /**
+   * Enhance this object by wrapping it in an extender object.
+   *
+   * @param $extender_name
+   *   The base name of the extending class.  The base name will be checked
+   *   against the current database connection to allow driver-specific subclasses
+   *   as well, using the same logic as the query objects themselves.  For example,
+   *   PagerDefault_mysql is the MySQL-specific override for PagerDefault.
+   * @return QueryExtendableInterface
+   *   The extender object, which now contains a reference to this object.
+   */
+  public function extend($extender_name);
+}
+
+/**
+ * Interface definition for a Select Query object.
+ */
+interface SelectQueryInterface extends QueryConditionInterface, QueryAlterableInterface, QueryExtendableInterface, QueryPlaceholderInterface {
+
+  /* Alter accessors to expose the query data to alter hooks. */
+
+  /**
+   * Returns a reference to the fields array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the fields
+   * array directly to make their changes. If just adding fields, however, the
+   * use of addField() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getFields();
+   * @endcode
+   *
+   * @return
+   *   A reference to the fields array structure.
+   */
+  public function &getFields();
+
+  /**
+   * Returns a reference to the expressions array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the expressions
+   * array directly to make their changes. If just adding expressions, however, the
+   * use of addExpression() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getExpressions();
+   * @endcode
+   *
+   * @return
+   *   A reference to the expression array structure.
+   */
+  public function &getExpressions();
+
+  /**
+   * Returns a reference to the order by array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the order-by
+   * array directly to make their changes. If just adding additional ordering
+   * fields, however, the use of orderBy() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getOrderBy();
+   * @endcode
+   *
+   * @return
+   *   A reference to the expression array structure.
+   */
+  public function &getOrderBy();
+
+  /**
+   * Returns a reference to the group-by array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the group-by
+   * array directly to make their changes. If just adding additional grouping
+   * fields, however, the use of groupBy() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getGroupBy();
+   * @endcode
+   *
+   * @return
+   *   A reference to the group-by array structure.
+   */
+  public function &getGroupBy();
+
+  /**
+   * Returns a reference to the tables array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the tables
+   * array directly to make their changes. If just adding tables, however, the
+   * use of the join() methods is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getTables();
+   * @endcode
+   *
+   * @return
+   *   A reference to the tables array structure.
+   */
+  public function &getTables();
+
+  /**
+   * Returns a reference to the union queries for this query. This include
+   * queries for UNION, UNION ALL, and UNION DISTINCT.
+   *
+   * Because this method returns by reference, alter hooks may edit the tables
+   * array directly to make their changes. If just adding union queries,
+   * however, the use of the union() method is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getUnion();
+   * @endcode
+   *
+   * @return
+   *   A reference to the union query array structure.
+   */
+  public function &getUnion();
+
+  /**
+   * Compiles and returns an associative array of the arguments for this prepared statement.
+   *
+   * @param $queryPlaceholder
+   *   When collecting the arguments of a subquery, the main placeholder
+   *   object should be passed as this parameter.
+   *
+   * @return
+   *   An associative array of all placeholder arguments for this query.
+   */
+  public function getArguments(QueryPlaceholderInterface $queryPlaceholder = NULL);
+
+  /* Query building operations */
+
+  /**
+   * Sets this query to be DISTINCT.
+   *
+   * @param $distinct
+   *   TRUE to flag this query DISTINCT, FALSE to disable it.
+   * @return SelectQueryInterface
+   *   The called object.
+   */
+  public function distinct($distinct = TRUE);
+
+  /**
+   * Adds a field to the list to be SELECTed.
+   *
+   * @param $table_alias
+   *   The name of the table from which the field comes, as an alias. Generally
+   *   you will want to use the return value of join() here to ensure that it is
+   *   valid.
+   * @param $field
+   *   The name of the field.
+   * @param $alias
+   *   The alias for this field. If not specified, one will be generated
+   *   automatically based on the $table_alias and $field. The alias will be
+   *   checked for uniqueness, so the requested alias may not be the alias
+   *   that is assigned in all cases.
+   * @return
+   *   The unique alias that was assigned for this field.
+   */
+  public function addField($table_alias, $field, $alias = NULL);
+
+  /**
+   * Add multiple fields from the same table to be SELECTed.
+   *
+   * This method does not return the aliases set for the passed fields. In the
+   * majority of cases that is not a problem, as the alias will be the field
+   * name. However, if you do need to know the alias you can call getFields()
+   * and examine the result to determine what alias was created. Alternatively,
+   * simply use addField() for the few fields you care about and this method for
+   * the rest.
+   *
+   * @param $table_alias
+   *   The name of the table from which the field comes, as an alias. Generally
+   *   you will want to use the return value of join() here to ensure that it is
+   *   valid.
+   * @param $fields
+   *   An indexed array of fields present in the specified table that should be
+   *   included in this query. If not specified, $table_alias.* will be generated
+   *   without any aliases.
+   * @return SelectQueryInterface
+   *   The called object.
+   */
+  public function fields($table_alias, array $fields = array());
+
+  /**
+   * Adds an expression to the list of "fields" to be SELECTed.
+   *
+   * An expression can be any arbitrary string that is valid SQL. That includes
+   * various functions, which may in some cases be database-dependent. This
+   * method makes no effort to correct for database-specific functions.
+   *
+   * @param $expression
+   *   The expression string. May contain placeholders.
+   * @param $alias
+   *   The alias for this expression. If not specified, one will be generated
+   *   automatically in the form "expression_#". The alias will be checked for
+   *   uniqueness, so the requested alias may not be the alias that is assigned
+   *   in all cases.
+   * @param $arguments
+   *   Any placeholder arguments needed for this expression.
+   * @return
+   *   The unique alias that was assigned for this expression.
+   */
+  public function addExpression($expression, $alias = NULL, $arguments = array());
+
+  /**
+   * Default Join against another table in the database.
+   *
+   * This method is a convenience method for innerJoin().
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table. The token %alias can be used in this string to be replaced with
+   *   the actual alias. This is useful when $alias is modified by the database
+   *   system, for example, when joining the same table more than once.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function join($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Inner Join against another table in the database.
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table. The token %alias can be used in this string to be replaced with
+   *   the actual alias. This is useful when $alias is modified by the database
+   *   system, for example, when joining the same table more than once.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Left Outer Join against another table in the database.
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table. The token %alias can be used in this string to be replaced with
+   *   the actual alias. This is useful when $alias is modified by the database
+   *   system, for example, when joining the same table more than once.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Right Outer Join against another table in the database.
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table. The token %alias can be used in this string to be replaced with
+   *   the actual alias. This is useful when $alias is modified by the database
+   *   system, for example, when joining the same table more than once.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Join against another table in the database.
+   *
+   * This method does the "hard" work of queuing up a table to be joined against.
+   * In some cases, that may include dipping into the Schema API to find the necessary
+   * fields on which to join.
+   *
+   * @param $type
+   *   The type of join. Typically one one of INNER, LEFT OUTER, and RIGHT OUTER.
+   * @param $table
+   *   The table against which to join. May be a string or another SelectQuery
+   *   object. If a query object is passed, it will be used as a subselect.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table. If omitted,
+   *   one will be dynamically generated.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table. The token %alias can be used in this string to be replaced with
+   *   the actual alias. This is useful when $alias is modified by the database
+   *   system, for example, when joining the same table more than once.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Orders the result set by a given field.
+   *
+   * If called multiple times, the query will order by each specified field in the
+   * order this method is called.
+   *
+   * If the query uses DISTINCT or GROUP BY conditions, fields or expressions
+   * that are used for the order must be selected to be compatible with some
+   * databases like PostgreSQL. The PostgreSQL driver can handle simple cases
+   * automatically but it is suggested to explicitly specify them. Additionally,
+   * when ordering on an alias, the alias must be added before orderBy() is
+   * called.
+   *
+   * @param $field
+   *   The field on which to order.
+   * @param $direction
+   *   The direction to sort. Legal values are "ASC" and "DESC".
+   * @return SelectQueryInterface
+   *   The called object.
+   */
+  public function orderBy($field, $direction = 'ASC');
+
+  /**
+   * Orders the result set by a random value.
+   *
+   * This may be stacked with other orderBy() calls. If so, the query will order
+   * by each specified field, including this one, in the order called. Although
+   * this method may be called multiple times on the same query, doing so
+   * is not particularly useful.
+   *
+   * Note: The method used by most drivers may not scale to very large result
+   * sets. If you need to work with extremely large data sets, you may create
+   * your own database driver by subclassing off of an existing driver and
+   * implementing your own randomization mechanism. See
+   *
+   * http://jan.kneschke.de/projects/mysql/order-by-rand/
+   *
+   * for an example of such an alternate sorting mechanism.
+   *
+   * @return SelectQueryInterface
+   *   The called object
+   */
+  public function orderRandom();
+
+  /**
+   * Restricts a query to a given range in the result set.
+   *
+   * If this method is called with no parameters, will remove any range
+   * directives that have been set.
+   *
+   * @param $start
+   *   The first record from the result set to return. If NULL, removes any
+   *   range directives that are set.
+   * @param $length
+   *   The number of records to return from the result set.
+   * @return SelectQueryInterface
+   *   The called object.
+   */
+  public function range($start = NULL, $length = NULL);
+
+  /**
+   * Add another Select query to UNION to this one.
+   *
+   * Union queries consist of two or more queries whose
+   * results are effectively concatenated together. Queries
+   * will be UNIONed in the order they are specified, with
+   * this object's query coming first. Duplicate columns will
+   * be discarded. All forms of UNION are supported, using
+   * the second '$type' argument.
+   *
+   * Note: All queries UNIONed together must have the same
+   * field structure, in the same order. It is up to the
+   * caller to ensure that they match properly. If they do
+   * not, an SQL syntax error will result.
+   *
+   * @param $query
+   *   The query to UNION to this query.
+   * @param $type
+   *   The type of UNION to add to the query. Defaults to plain
+   *   UNION.
+   * @return SelectQueryInterface
+   *   The called object.
+   */
+  public function union(SelectQueryInterface $query, $type = '');
+
+  /**
+   * Groups the result set by the specified field.
+   *
+   * @param $field
+   *   The field on which to group. This should be the field as aliased.
+   * @return SelectQueryInterface
+   *   The called object.
+   */
+  public function groupBy($field);
+
+  /**
+   * Get the equivalent COUNT query of this query as a new query object.
+   *
+   * @return SelectQueryInterface
+   *   A new SelectQuery object with no fields or expressions besides COUNT(*).
+   */
+  public function countQuery();
+
+  /**
+   * Indicates if preExecute() has already been called on that object.
+   *
+   * @return
+   *   TRUE is this query has already been prepared, FALSE otherwise.
+   */
+  public function isPrepared();
+
+  /**
+   * Generic preparation and validation for a SELECT query.
+   *
+   * @return
+   *   TRUE if the validation was successful, FALSE if not.
+   */
+  public function preExecute(SelectQueryInterface $query = NULL);
+
+  /**
+   * Helper function to build most common HAVING conditional clauses.
+   *
+   * This method can take a variable number of parameters. If called with two
+   * parameters, they are taken as $field and $value with $operator having a value
+   * of IN if $value is an array and = otherwise.
+   *
+   * @param $field
+   *   The name of the field to check. If you would like to add a more complex
+   *   condition involving operators or functions, use having().
+   * @param $value
+   *   The value to test the field against. In most cases, this is a scalar. For more
+   *   complex options, it is an array. The meaning of each element in the array is
+   *   dependent on the $operator.
+   * @param $operator
+   *   The comparison operator, such as =, <, or >=. It also accepts more complex
+   *   options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is an array
+   *   = otherwise.
+   * @return QueryConditionInterface
+   *   The called object.
+   */
+  public function havingCondition($field, $value = NULL, $operator = NULL);
+
+  /**
+   * Clone magic method.
+   *
+   * Select queries have dependent objects that must be deep-cloned.  The
+   * connection object itself, however, should not be cloned as that would
+   * duplicate the connection itself.
+   */
+  public function __clone();
+
+  /**
+   * Add FOR UPDATE to the query.
+   *
+   * FOR UPDATE prevents the rows retrieved by the SELECT statement from being
+   * modified or deleted by other transactions until the current transaction
+   * ends. Other transactions that attempt UPDATE, DELETE, or SELECT FOR UPDATE
+   * of these rows will be blocked until the current transaction ends.
+   *
+   * @param $set
+   *   IF TRUE, FOR UPDATE will be added to the query, if FALSE then it won't.
+   *
+   * @return QueryConditionInterface
+   *   The called object.
+   */
+  public function forUpdate($set = TRUE);
+}
+
+/**
+ * The base extender class for Select queries.
+ */
+class SelectQueryExtender implements SelectQueryInterface {
+
+  /**
+   * The SelectQuery object we are extending/decorating.
+   *
+   * @var SelectQueryInterface
+   */
+  protected $query;
+
+  /**
+   * The connection object on which to run this query.
+   *
+   * @var DatabaseConnection
+   */
+  protected $connection;
+
+  /**
+   * A unique identifier for this query object.
+   */
+  protected $uniqueIdentifier;
+
+  /**
+   * The placeholder counter.
+   */
+  protected $placeholder = 0;
+
+  public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
+    $this->uniqueIdentifier = uniqid('', TRUE);
+    $this->query = $query;
+    $this->connection = $connection;
+  }
+
+  /**
+   * Implements QueryPlaceholderInterface::uniqueIdentifier().
+   */
+  public function uniqueIdentifier() {
+    return $this->uniqueIdentifier;
+  }
+
+  /**
+   * Implements QueryPlaceholderInterface::nextPlaceholder().
+   */
+  public function nextPlaceholder() {
+    return $this->placeholder++;
+  }
+
+  /* Implementations of QueryAlterableInterface. */
+
+  public function addTag($tag) {
+    $this->query->addTag($tag);
+    return $this;
+  }
+
+  public function hasTag($tag) {
+    return $this->query->hasTag($tag);
+  }
+
+  public function hasAllTags() {
+    $args = func_get_args();
+    return call_user_func_array(array($this->query, 'hasAllTags'), $args);
+  }
+
+  public function hasAnyTag() {
+    $args = func_get_args();
+    return call_user_func_array(array($this->query, 'hasAnyTags'), $args);
+  }
+
+  public function addMetaData($key, $object) {
+    $this->query->addMetaData($key, $object);
+    return $this;
+  }
+
+  public function getMetaData($key) {
+    return $this->query->getMetaData($key);
+  }
+
+  /* Implementations of QueryConditionInterface for the WHERE clause. */
+
+  public function condition($field, $value = NULL, $operator = NULL) {
+    $this->query->condition($field, $value, $operator);
+    return $this;
+  }
+
+  public function &conditions() {
+    return $this->query->conditions();
+  }
+
+  public function arguments() {
+    return $this->query->arguments();
+  }
+
+  public function where($snippet, $args = array()) {
+    $this->query->where($snippet, $args);
+    return $this;
+  }
+
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
+    return $this->query->compile($connection, $queryPlaceholder);
+  }
+
+  public function compiled() {
+    return $this->query->compiled();
+  }
+
+  /* Implementations of QueryConditionInterface for the HAVING clause. */
+
+  public function havingCondition($field, $value = NULL, $operator = '=') {
+    $this->query->havingCondition($field, $value, $operator);
+    return $this;
+  }
+
+  public function &havingConditions() {
+    return $this->query->havingConditions();
+  }
+
+  public function havingArguments() {
+    return $this->query->havingArguments();
+  }
+
+  public function having($snippet, $args = array()) {
+    $this->query->having($snippet, $args);
+    return $this;
+  }
+
+  public function havingCompile(DatabaseConnection $connection) {
+    return $this->query->havingCompile($connection);
+  }
+
+  /* Implementations of QueryExtendableInterface. */
+
+  public function extend($extender_name) {
+    // The extender can be anywhere so this needs to go to the registry, which
+    // is surely loaded by now.
+    $class = $this->connection->getDriverClass($extender_name, array(), TRUE);
+    return new $class($this, $this->connection);
+  }
+
+  /* Alter accessors to expose the query data to alter hooks. */
+
+  public function &getFields() {
+    return $this->query->getFields();
+  }
+
+  public function &getExpressions() {
+    return $this->query->getExpressions();
+  }
+
+  public function &getOrderBy() {
+    return $this->query->getOrderBy();
+  }
+
+  public function &getGroupBy() {
+    return $this->query->getGroupBy();
+  }
+
+  public function &getTables() {
+    return $this->query->getTables();
+  }
+
+  public function &getUnion() {
+    return $this->query->getUnion();
+  }
+
+  public function getArguments(QueryPlaceholderInterface $queryPlaceholder = NULL) {
+    return $this->query->getArguments($queryPlaceholder);
+  }
+
+  public function isPrepared() {
+    return $this->query->isPrepared();
+  }
+
+  public function preExecute(SelectQueryInterface $query = NULL) {
+    // If no query object is passed in, use $this.
+    if (!isset($query)) {
+      $query = $this;
+    }
+
+    return $this->query->preExecute($query);
+  }
+
+  public function execute() {
+    // By calling preExecute() here, we force it to preprocess the extender
+    // object rather than just the base query object.  That means
+    // hook_query_alter() gets access to the extended object.
+    if (!$this->preExecute($this)) {
+      return NULL;
+    }
+
+    return $this->query->execute();
+  }
+
+  public function distinct($distinct = TRUE) {
+    $this->query->distinct($distinct);
+    return $this;
+  }
+
+  public function addField($table_alias, $field, $alias = NULL) {
+    return $this->query->addField($table_alias, $field, $alias);
+  }
+
+  public function fields($table_alias, array $fields = array()) {
+    $this->query->fields($table_alias, $fields);
+    return $this;
+  }
+
+  public function addExpression($expression, $alias = NULL, $arguments = array()) {
+    return $this->query->addExpression($expression, $alias, $arguments);
+  }
+
+  public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->join($table, $alias, $condition, $arguments);
+  }
+
+  public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->innerJoin($table, $alias, $condition, $arguments);
+  }
+
+  public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->leftJoin($table, $alias, $condition, $arguments);
+  }
+
+  public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->rightJoin($table, $alias, $condition, $arguments);
+  }
+
+  public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->addJoin($type, $table, $alias, $condition, $arguments);
+  }
+
+  public function orderBy($field, $direction = 'ASC') {
+    $this->query->orderBy($field, $direction);
+    return $this;
+  }
+
+  public function orderRandom() {
+    $this->query->orderRandom();
+    return $this;
+  }
+
+  public function range($start = NULL, $length = NULL) {
+    $this->query->range($start, $length);
+    return $this;
+  }
+
+  public function union(SelectQueryInterface $query, $type = '') {
+    $this->query->union($query, $type);
+    return $this;
+  }
+
+  public function groupBy($field) {
+    $this->query->groupBy($field);
+    return $this;
+  }
+
+  public function forUpdate($set = TRUE) {
+    $this->query->forUpdate($set);
+    return $this;
+  }
+
+  public function countQuery() {
+    return $this->query->countQuery();
+  }
+
+  function isNull($field) {
+    $this->query->isNull($field);
+    return $this;
+  }
+
+  function isNotNull($field) {
+    $this->query->isNotNull($field);
+    return $this;
+  }
+
+  public function exists(SelectQueryInterface $select) {
+    $this->query->exists($select);
+    return $this;
+  }
+
+  public function notExists(SelectQueryInterface $select) {
+    $this->query->notExists($select);
+    return $this;
+  }
+
+  public function __toString() {
+    return (string) $this->query;
+  }
+
+  public function __clone() {
+    $this->uniqueIdentifier = uniqid('', TRUE);
+
+    // We need to deep-clone the query we're wrapping, which in turn may
+    // deep-clone other objects.  Exciting!
+    $this->query = clone($this->query);
+  }
+
+  /**
+   * Magic override for undefined methods.
+   *
+   * If one extender extends another extender, then methods in the inner extender
+   * will not be exposed on the outer extender.  That's because we cannot know
+   * in advance what those methods will be, so we cannot provide wrapping
+   * implementations as we do above.  Instead, we use this slower catch-all method
+   * to handle any additional methods.
+   */
+  public function __call($method, $args) {
+    $return = call_user_func_array(array($this->query, $method), $args);
+
+    // Some methods will return the called object as part of a fluent interface.
+    // Others will return some useful value.  If it's a value, then the caller
+    // probably wants that value.  If it's the called object, then we instead
+    // return this object.  That way we don't "lose" an extender layer when
+    // chaining methods together.
+    if ($return instanceof SelectQueryInterface) {
+      return $this;
+    }
+    else {
+      return $return;
+    }
+  }
+}
+
+/**
+ * Query builder for SELECT statements.
+ */
+class SelectQuery extends Query implements SelectQueryInterface {
+
+  /**
+   * The fields to SELECT.
+   *
+   * @var array
+   */
+  protected $fields = array();
+
+  /**
+   * The expressions to SELECT as virtual fields.
+   *
+   * @var array
+   */
+  protected $expressions = array();
+
+  /**
+   * The tables against which to JOIN.
+   *
+   * This property is a nested array. Each entry is an array representing
+   * a single table against which to join. The structure of each entry is:
+   *
+   * array(
+   *   'type' => $join_type (one of INNER, LEFT OUTER, RIGHT OUTER),
+   *   'table' => $table,
+   *   'alias' => $alias_of_the_table,
+   *   'condition' => $condition_clause_on_which_to_join,
+   *   'arguments' => $array_of_arguments_for_placeholders_in_the condition.
+   *   'all_fields' => TRUE to SELECT $alias.*, FALSE or NULL otherwise.
+   * )
+   *
+   * If $table is a string, it is taken as the name of a table. If it is
+   * a SelectQuery object, it is taken as a subquery.
+   *
+   * @var array
+   */
+  protected $tables = array();
+
+  /**
+   * The fields by which to order this query.
+   *
+   * This is an associative array. The keys are the fields to order, and the value
+   * is the direction to order, either ASC or DESC.
+   *
+   * @var array
+   */
+  protected $order = array();
+
+  /**
+   * The fields by which to group.
+   *
+   * @var array
+   */
+  protected $group = array();
+
+  /**
+   * The conditional object for the WHERE clause.
+   *
+   * @var DatabaseCondition
+   */
+  protected $where;
+
+  /**
+   * The conditional object for the HAVING clause.
+   *
+   * @var DatabaseCondition
+   */
+  protected $having;
+
+  /**
+   * Whether or not this query should be DISTINCT
+   *
+   * @var boolean
+   */
+  protected $distinct = FALSE;
+
+  /**
+   * The range limiters for this query.
+   *
+   * @var array
+   */
+  protected $range;
+
+  /**
+   * An array whose elements specify a query to UNION, and the UNION type. The
+   * 'type' key may be '', 'ALL', or 'DISTINCT' to represent a 'UNION',
+   * 'UNION ALL', or 'UNION DISTINCT' statement, respectively.
+   *
+   * All entries in this array will be applied from front to back, with the
+   * first query to union on the right of the original query, the second union
+   * to the right of the first, etc.
+   *
+   * @var array
+   */
+  protected $union = array();
+
+  /**
+   * Indicates if preExecute() has already been called.
+   * @var boolean
+   */
+  protected $prepared = FALSE;
+
+  /**
+   * The FOR UPDATE status
+   */
+  protected $forUpdate = FALSE;
+
+  public function __construct($table, $alias = NULL, DatabaseConnection $connection, $options = array()) {
+    $options['return'] = Database::RETURN_STATEMENT;
+    parent::__construct($connection, $options);
+    $this->where = new DatabaseCondition('AND');
+    $this->having = new DatabaseCondition('AND');
+    $this->addJoin(NULL, $table, $alias);
+  }
+
+  /* Implementations of QueryAlterableInterface. */
+
+  public function addTag($tag) {
+    $this->alterTags[$tag] = 1;
+    return $this;
+  }
+
+  public function hasTag($tag) {
+    return isset($this->alterTags[$tag]);
+  }
+
+  public function hasAllTags() {
+    $args = func_get_args();
+    return !(boolean)array_diff($args, array_keys($this->alterTags));
+  }
+
+  public function hasAnyTag() {
+    $args = func_get_args();
+    return (boolean)array_intersect($args, array_keys($this->alterTags));
+  }
+
+  public function addMetaData($key, $object) {
+    $this->alterMetaData[$key] = $object;
+    return $this;
+  }
+
+  public function getMetaData($key) {
+    return isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL;
+  }
+
+  /* Implementations of QueryConditionInterface for the WHERE clause. */
+
+  public function condition($field, $value = NULL, $operator = NULL) {
+    $this->where->condition($field, $value, $operator);
+    return $this;
+  }
+
+  public function &conditions() {
+    return $this->where->conditions();
+  }
+
+  public function arguments() {
+    if (!$this->compiled()) {
+      return NULL;
+    }
+
+    $args = $this->where->arguments() + $this->having->arguments();
+
+    foreach ($this->tables as $table) {
+      if ($table['arguments']) {
+        $args += $table['arguments'];
+      }
+      // If this table is a subquery, grab its arguments recursively.
+      if ($table['table'] instanceof SelectQueryInterface) {
+        $args += $table['table']->arguments();
+      }
+    }
+
+    foreach ($this->expressions as $expression) {
+      if ($expression['arguments']) {
+        $args += $expression['arguments'];
+      }
+    }
+
+    // If there are any dependent queries to UNION,
+    // incorporate their arguments recursively.
+    foreach ($this->union as $union) {
+      $args += $union['query']->arguments();
+    }
+
+    return $args;
+  }
+
+  public function where($snippet, $args = array()) {
+    $this->where->where($snippet, $args);
+    return $this;
+  }
+
+  public function isNull($field) {
+    $this->where->isNull($field);
+    return $this;
+  }
+
+  public function isNotNull($field) {
+    $this->where->isNotNull($field);
+    return $this;
+  }
+
+  public function exists(SelectQueryInterface $select) {
+    $this->where->exists($select);
+    return $this;
+  }
+
+  public function notExists(SelectQueryInterface $select) {
+    $this->where->notExists($select);
+    return $this;
+  }
+
+  public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
+    $this->where->compile($connection, $queryPlaceholder);
+    $this->having->compile($connection, $queryPlaceholder);
+
+    foreach ($this->tables as $table) {
+      // If this table is a subquery, compile it recursively.
+      if ($table['table'] instanceof SelectQueryInterface) {
+        $table['table']->compile($connection, $queryPlaceholder);
+      }
+    }
+
+    // If there are any dependent queries to UNION, compile it recursively.
+    foreach ($this->union as $union) {
+      $union['query']->compile($connection, $queryPlaceholder);
+    }
+  }
+
+  public function compiled() {
+    if (!$this->where->compiled() || !$this->having->compiled()) {
+      return FALSE;
+    }
+
+    foreach ($this->tables as $table) {
+      // If this table is a subquery, check its status recursively.
+      if ($table['table'] instanceof SelectQueryInterface) {
+        if (!$table['table']->compiled()) {
+          return FALSE;
+        }
+      }
+    }
+
+    foreach ($this->union as $union) {
+      if (!$union['query']->compiled()) {
+        return FALSE;
+      }
+    }
+
+    return TRUE;
+  }
+
+  /* Implementations of QueryConditionInterface for the HAVING clause. */
+
+  public function havingCondition($field, $value = NULL, $operator = NULL) {
+    $this->having->condition($field, $value, $operator);
+    return $this;
+  }
+
+  public function &havingConditions() {
+    return $this->having->conditions();
+  }
+
+  public function havingArguments() {
+    return $this->having->arguments();
+  }
+
+  public function having($snippet, $args = array()) {
+    $this->having->where($snippet, $args);
+    return $this;
+  }
+
+  public function havingCompile(DatabaseConnection $connection) {
+    return $this->having->compile($connection, $this);
+  }
+
+  /* Implementations of QueryExtendableInterface. */
+
+  public function extend($extender_name) {
+    $override_class = $extender_name . '_' . $this->connection->driver();
+    if (class_exists($override_class)) {
+      $extender_name = $override_class;
+    }
+    return new $extender_name($this, $this->connection);
+  }
+
+  public function havingIsNull($field) {
+    $this->having->isNull($field);
+    return $this;
+  }
+
+  public function havingIsNotNull($field) {
+    $this->having->isNotNull($field);
+    return $this;
+  }
+
+  public function havingExists(SelectQueryInterface $select) {
+    $this->having->exists($select);
+    return $this;
+  }
+
+  public function havingNotExists(SelectQueryInterface $select) {
+    $this->having->notExists($select);
+    return $this;
+  }
+
+  public function forUpdate($set = TRUE) {
+    if (isset($set)) {
+      $this->forUpdate = $set;
+    }
+    return $this;
+  }
+
+  /* Alter accessors to expose the query data to alter hooks. */
+
+  public function &getFields() {
+    return $this->fields;
+  }
+
+  public function &getExpressions() {
+    return $this->expressions;
+  }
+
+  public function &getOrderBy() {
+    return $this->order;
+  }
+
+  public function &getGroupBy() {
+    return $this->group;
+  }
+
+  public function &getTables() {
+    return $this->tables;
+  }
+
+  public function &getUnion() {
+    return $this->union;
+  }
+
+  public function getArguments(QueryPlaceholderInterface $queryPlaceholder = NULL) {
+    if (!isset($queryPlaceholder)) {
+      $queryPlaceholder = $this;
+    }
+    $this->compile($this->connection, $queryPlaceholder);
+    return $this->arguments();
+  }
+
+  /**
+   * Indicates if preExecute() has already been called on that object.
+   */
+  public function isPrepared() {
+    return $this->prepared;
+  }
+
+  /**
+   * Generic preparation and validation for a SELECT query.
+   *
+   * @return
+   *   TRUE if the validation was successful, FALSE if not.
+   */
+  public function preExecute(SelectQueryInterface $query = NULL) {
+    // If no query object is passed in, use $this.
+    if (!isset($query)) {
+      $query = $this;
+    }
+
+    // Only execute this once.
+    if ($query->isPrepared()) {
+      return TRUE;
+    }
+
+    // Modules may alter all queries or only those having a particular tag.
+    if (isset($this->alterTags)) {
+      $hooks = array('query');
+      foreach ($this->alterTags as $tag => $value) {
+        $hooks[] = 'query_' . $tag;
+      }
+      drupal_alter($hooks, $query);
+    }
+
+    $this->prepared = TRUE;
+
+    // Now also prepare any sub-queries.
+    foreach ($this->tables as $table) {
+      if ($table['table'] instanceof SelectQueryInterface) {
+        $table['table']->preExecute();
+      }
+    }
+
+    foreach ($this->union as $union) {
+      $union['query']->preExecute();
+    }
+
+    return $this->prepared;
+  }
+
+  public function execute() {
+    // If validation fails, simply return NULL.
+    // Note that validation routines in preExecute() may throw exceptions instead.
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    $args = $this->getArguments();
+    return $this->connection->query((string) $this, $args, $this->queryOptions);
+  }
+
+  public function distinct($distinct = TRUE) {
+    $this->distinct = $distinct;
+    return $this;
+  }
+
+  public function addField($table_alias, $field, $alias = NULL) {
+    // If no alias is specified, first try the field name itself.
+    if (empty($alias)) {
+      $alias = $field;
+    }
+
+    // If that's already in use, try the table name and field name.
+    if (!empty($this->fields[$alias])) {
+      $alias = $table_alias . '_' . $field;
+    }
+
+    // If that is already used, just add a counter until we find an unused alias.
+    $alias_candidate = $alias;
+    $count = 2;
+    while (!empty($this->fields[$alias_candidate])) {
+      $alias_candidate = $alias . '_' . $count++;
+    }
+    $alias = $alias_candidate;
+
+    $this->fields[$alias] = array(
+      'field' => $field,
+      'table' => $table_alias,
+      'alias' => $alias,
+    );
+
+    return $alias;
+  }
+
+  public function fields($table_alias, array $fields = array()) {
+
+    if ($fields) {
+      foreach ($fields as $field) {
+        // We don't care what alias was assigned.
+        $this->addField($table_alias, $field);
+      }
+    }
+    else {
+      // We want all fields from this table.
+      $this->tables[$table_alias]['all_fields'] = TRUE;
+    }
+
+    return $this;
+  }
+
+  public function addExpression($expression, $alias = NULL, $arguments = array()) {
+    if (empty($alias)) {
+      $alias = 'expression';
+    }
+
+    $alias_candidate = $alias;
+    $count = 2;
+    while (!empty($this->expressions[$alias_candidate])) {
+      $alias_candidate = $alias . '_' . $count++;
+    }
+    $alias = $alias_candidate;
+
+    $this->expressions[$alias] = array(
+      'expression' => $expression,
+      'alias' => $alias,
+      'arguments' => $arguments,
+    );
+
+    return $alias;
+  }
+
+  public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->addJoin('INNER', $table, $alias, $condition, $arguments);
+  }
+
+  public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->addJoin('INNER', $table, $alias, $condition, $arguments);
+  }
+
+  public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments);
+  }
+
+  public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->addJoin('RIGHT OUTER', $table, $alias, $condition, $arguments);
+  }
+
+  public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) {
+
+    if (empty($alias)) {
+      if ($table instanceof SelectQueryInterface) {
+        $alias = 'subquery';
+      }
+      else {
+        $alias = $table;
+      }
+    }
+
+    $alias_candidate = $alias;
+    $count = 2;
+    while (!empty($this->tables[$alias_candidate])) {
+      $alias_candidate = $alias . '_' . $count++;
+    }
+    $alias = $alias_candidate;
+
+    if (is_string($condition)) {
+      $condition = str_replace('%alias', $alias, $condition);
+    }
+
+    $this->tables[$alias] = array(
+      'join type' => $type,
+      'table' => $table,
+      'alias' => $alias,
+      'condition' => $condition,
+      'arguments' => $arguments,
+    );
+
+    return $alias;
+  }
+
+  public function orderBy($field, $direction = 'ASC') {
+    $this->order[$field] = $direction;
+    return $this;
+  }
+
+  public function orderRandom() {
+    $alias = $this->addExpression('RAND()', 'random_field');
+    $this->orderBy($alias);
+    return $this;
+  }
+
+  public function range($start = NULL, $length = NULL) {
+    $this->range = func_num_args() ? array('start' => $start, 'length' => $length) : array();
+    return $this;
+  }
+
+  public function union(SelectQueryInterface $query, $type = '') {
+    // Handle UNION aliasing.
+    switch ($type) {
+      // Fold UNION DISTINCT to UNION for better cross database support.
+      case 'DISTINCT':
+      case '':
+        $type = 'UNION';
+        break;
+
+      case 'ALL':
+        $type = 'UNION ALL';
+      default:
+    }
+
+    $this->union[] = array(
+      'type' => $type,
+      'query' => $query,
+    );
+
+    return $this;
+  }
+
+  public function groupBy($field) {
+    $this->group[$field] = $field;
+    return $this;
+  }
+
+  public function countQuery() {
+    // Create our new query object that we will mutate into a count query.
+    $count = clone($this);
+
+    $group_by = $count->getGroupBy();
+    $having = $count->havingConditions();
+
+    if (!$count->distinct && !isset($having[0])) {
+      // When not executing a distinct query, we can zero-out existing fields
+      // and expressions that are not used by a GROUP BY or HAVING. Fields
+      // listed in a GROUP BY or HAVING clause need to be present in the
+      // query.
+      $fields =& $count->getFields();
+      foreach (array_keys($fields) as $field) {
+        if (empty($group_by[$field])) {
+          unset($fields[$field]);
+        }
+      }
+
+      $expressions =& $count->getExpressions();
+      foreach (array_keys($expressions) as $field) {
+        if (empty($group_by[$field])) {
+          unset($expressions[$field]);
+        }
+      }
+
+      // Also remove 'all_fields' statements, which are expanded into tablename.*
+      // when the query is executed.
+      foreach ($count->tables as $alias => &$table) {
+        unset($table['all_fields']);
+      }
+    }
+
+    // If we've just removed all fields from the query, make sure there is at
+    // least one so that the query still runs.
+    $count->addExpression('1');
+
+    // Ordering a count query is a waste of cycles, and breaks on some
+    // databases anyway.
+    $orders = &$count->getOrderBy();
+    $orders = array();
+
+    if ($count->distinct && !empty($group_by)) {
+      // If the query is distinct and contains a GROUP BY, we need to remove the
+      // distinct because SQL99 does not support counting on distinct multiple fields.
+      $count->distinct = FALSE;
+    }
+
+    $query = $this->connection->select($count);
+    $query->addExpression('COUNT(*)');
+
+    return $query;
+  }
+
+  public function __toString() {
+    // For convenience, we compile the query ourselves if the caller forgot
+    // to do it. This allows constructs like "(string) $query" to work. When
+    // the query will be executed, it will be recompiled using the proper
+    // placeholder generator anyway.
+    if (!$this->compiled()) {
+      $this->compile($this->connection, $this);
+    }
+
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // SELECT
+    $query = $comments . 'SELECT ';
+    if ($this->distinct) {
+      $query .= 'DISTINCT ';
+    }
+
+    // FIELDS and EXPRESSIONS
+    $fields = array();
+    foreach ($this->tables as $alias => $table) {
+      if (!empty($table['all_fields'])) {
+        $fields[] = $this->connection->escapeTable($alias) . '.*';
+      }
+    }
+    foreach ($this->fields as $alias => $field) {
+      // Always use the AS keyword for field aliases, as some
+      // databases require it (e.g., PostgreSQL).
+      $fields[] = (isset($field['table']) ? $this->connection->escapeTable($field['table']) . '.' : '') . $this->connection->escapeField($field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']);
+    }
+    foreach ($this->expressions as $alias => $expression) {
+      $fields[] = $expression['expression'] . ' AS ' . $this->connection->escapeAlias($expression['alias']);
+    }
+    $query .= implode(', ', $fields);
+
+
+    // FROM - We presume all queries have a FROM, as any query that doesn't won't need the query builder anyway.
+    $query .= "\nFROM ";
+    foreach ($this->tables as $alias => $table) {
+      $query .= "\n";
+      if (isset($table['join type'])) {
+        $query .= $table['join type'] . ' JOIN ';
+      }
+
+      // If the table is a subquery, compile it and integrate it into this query.
+      if ($table['table'] instanceof SelectQueryInterface) {
+        // Run preparation steps on this sub-query before converting to string.
+        $subquery = $table['table'];
+        $subquery->preExecute();
+        $table_string = '(' . (string) $subquery . ')';
+      }
+      else {
+        $table_string = '{' . $this->connection->escapeTable($table['table']) . '}';
+      }
+
+      // Don't use the AS keyword for table aliases, as some
+      // databases don't support it (e.g., Oracle).
+      $query .=  $table_string . ' ' . $this->connection->escapeTable($table['alias']);
+
+      if (!empty($table['condition'])) {
+        $query .= ' ON ' . $table['condition'];
+      }
+    }
+
+    // WHERE
+    if (count($this->where)) {
+      // There is an implicit string cast on $this->condition.
+      $query .= "\nWHERE " . $this->where;
+    }
+
+    // GROUP BY
+    if ($this->group) {
+      $query .= "\nGROUP BY " . implode(', ', $this->group);
+    }
+
+    // HAVING
+    if (count($this->having)) {
+      // There is an implicit string cast on $this->having.
+      $query .= "\nHAVING " . $this->having;
+    }
+
+    // ORDER BY
+    if ($this->order) {
+      $query .= "\nORDER BY ";
+      $fields = array();
+      foreach ($this->order as $field => $direction) {
+        $fields[] = $field . ' ' . $direction;
+      }
+      $query .= implode(', ', $fields);
+    }
+
+    // RANGE
+    // There is no universal SQL standard for handling range or limit clauses.
+    // Fortunately, all core-supported databases use the same range syntax.
+    // Databases that need a different syntax can override this method and
+    // do whatever alternate logic they need to.
+    if (!empty($this->range)) {
+      $query .= "\nLIMIT " . (int) $this->range['length'] . " OFFSET " . (int) $this->range['start'];
+    }
+
+    // UNION is a little odd, as the select queries to combine are passed into
+    // this query, but syntactically they all end up on the same level.
+    if ($this->union) {
+      foreach ($this->union as $union) {
+        $query .= ' ' . $union['type'] . ' ' . (string) $union['query'];
+      }
+    }
+
+    if ($this->forUpdate) {
+      $query .= ' FOR UPDATE';
+    }
+
+    return $query;
+  }
+
+  public function __clone() {
+    // On cloning, also clone the dependent objects. However, we do not
+    // want to clone the database connection object as that would duplicate the
+    // connection itself.
+
+    $this->where = clone($this->where);
+    $this->having = clone($this->having);
+    foreach ($this->union as $key => $aggregate) {
+      $this->union[$key]['query'] = clone($aggregate['query']);
+    }
+  }
+}
+
+/**
+ * @} End of "addtogroup database".
+ */

+ 519 - 0
includes/database/sqlite/database.inc

@@ -0,0 +1,519 @@
+<?php
+
+/**
+ * @file
+ * Database interface code for SQLite embedded database engine.
+ */
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+include_once DRUPAL_ROOT . '/includes/database/prefetch.inc';
+
+/**
+ * Specific SQLite implementation of DatabaseConnection.
+ */
+class DatabaseConnection_sqlite extends DatabaseConnection {
+
+  /**
+   * Whether this database connection supports savepoints.
+   *
+   * Version of sqlite lower then 3.6.8 can't use savepoints.
+   * See http://www.sqlite.org/releaselog/3_6_8.html
+   *
+   * @var boolean
+   */
+  protected $savepointSupport = FALSE;
+
+  /**
+   * Whether or not the active transaction (if any) will be rolled back.
+   *
+   * @var boolean
+   */
+  protected $willRollback;
+
+  /**
+   * All databases attached to the current database. This is used to allow
+   * prefixes to be safely handled without locking the table
+   *
+   * @var array
+   */
+  protected $attachedDatabases = array();
+
+  /**
+   * Whether or not a table has been dropped this request: the destructor will
+   * only try to get rid of unnecessary databases if there is potential of them
+   * being empty.
+   *
+   * This variable is set to public because DatabaseSchema_sqlite needs to
+   * access it. However, it should not be manually set.
+   *
+   * @var boolean
+   */
+  var $tableDropped = FALSE;
+
+  public function __construct(array $connection_options = array()) {
+    // We don't need a specific PDOStatement class here, we simulate it below.
+    $this->statementClass = NULL;
+
+    // This driver defaults to transaction support, except if explicitly passed FALSE.
+    $this->transactionSupport = $this->transactionalDDLSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
+
+    $this->connectionOptions = $connection_options;
+
+    // Allow PDO options to be overridden.
+    $connection_options += array(
+      'pdo' => array(),
+    );
+    $connection_options['pdo'] += array(
+      // Convert numeric values to strings when fetching.
+      PDO::ATTR_STRINGIFY_FETCHES => TRUE,
+    );
+    parent::__construct('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
+
+    // Attach one database for each registered prefix.
+    $prefixes = $this->prefixes;
+    foreach ($prefixes as $table => &$prefix) {
+      // Empty prefix means query the main database -- no need to attach anything.
+      if (!empty($prefix)) {
+        // Only attach the database once.
+        if (!isset($this->attachedDatabases[$prefix])) {
+          $this->attachedDatabases[$prefix] = $prefix;
+          $this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'] . '-' . $prefix, ':prefix' => $prefix));
+        }
+
+        // Add a ., so queries become prefix.table, which is proper syntax for
+        // querying an attached database.
+        $prefix .= '.';
+      }
+    }
+    // Regenerate the prefixes replacement table.
+    $this->setPrefix($prefixes);
+
+    // Detect support for SAVEPOINT.
+    $version = $this->query('SELECT sqlite_version()')->fetchField();
+    $this->savepointSupport = (version_compare($version, '3.6.8') >= 0);
+
+    // Create functions needed by SQLite.
+    $this->sqliteCreateFunction('if', array($this, 'sqlFunctionIf'));
+    $this->sqliteCreateFunction('greatest', array($this, 'sqlFunctionGreatest'));
+    $this->sqliteCreateFunction('pow', 'pow', 2);
+    $this->sqliteCreateFunction('length', 'strlen', 1);
+    $this->sqliteCreateFunction('md5', 'md5', 1);
+    $this->sqliteCreateFunction('concat', array($this, 'sqlFunctionConcat'));
+    $this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
+    $this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
+    $this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand'));
+
+    // Execute sqlite init_commands.
+    if (isset($connection_options['init_commands'])) {
+      $this->exec(implode('; ', $connection_options['init_commands']));
+    }
+  }
+
+  /**
+   * Destructor for the SQLite connection.
+   *
+   * We prune empty databases on destruct, but only if tables have been
+   * dropped. This is especially needed when running the test suite, which
+   * creates and destroy databases several times in a row.
+   */
+  public function __destruct() {
+    if ($this->tableDropped && !empty($this->attachedDatabases)) {
+      foreach ($this->attachedDatabases as $prefix) {
+        // Check if the database is now empty, ignore the internal SQLite tables.
+        try {
+          $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField();
+
+          // We can prune the database file if it doesn't have any tables.
+          if ($count == 0) {
+            // Detach the database.
+            $this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
+            // Destroy the database file.
+            unlink($this->connectionOptions['database'] . '-' . $prefix);
+          }
+        }
+        catch (Exception $e) {
+          // Ignore the exception and continue. There is nothing we can do here
+          // to report the error or fail safe.
+        }
+      }
+    }
+  }
+
+  /**
+   * SQLite compatibility implementation for the IF() SQL function.
+   */
+  public function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
+    return $condition ? $expr1 : $expr2;
+  }
+
+  /**
+   * SQLite compatibility implementation for the GREATEST() SQL function.
+   */
+  public function sqlFunctionGreatest() {
+    $args = func_get_args();
+    foreach ($args as $k => $v) {
+      if (!isset($v)) {
+        unset($args);
+      }
+    }
+    if (count($args)) {
+      return max($args);
+    }
+    else {
+      return NULL;
+    }
+  }
+
+  /**
+   * SQLite compatibility implementation for the CONCAT() SQL function.
+   */
+  public function sqlFunctionConcat() {
+    $args = func_get_args();
+    return implode('', $args);
+  }
+
+  /**
+   * SQLite compatibility implementation for the SUBSTRING() SQL function.
+   */
+  public function sqlFunctionSubstring($string, $from, $length) {
+    return substr($string, $from - 1, $length);
+  }
+
+  /**
+   * SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function.
+   */
+  public function sqlFunctionSubstringIndex($string, $delimiter, $count) {
+    // If string is empty, simply return an empty string.
+    if (empty($string)) {
+      return '';
+    }
+    $end = 0;
+    for ($i = 0; $i < $count; $i++) {
+      $end = strpos($string, $delimiter, $end + 1);
+      if ($end === FALSE) {
+        $end = strlen($string);
+      }
+    }
+    return substr($string, 0, $end);
+  }
+
+  /**
+   * SQLite compatibility implementation for the RAND() SQL function.
+   */
+  public function sqlFunctionRand($seed = NULL) {
+    if (isset($seed)) {
+      mt_srand($seed);
+    }
+    return mt_rand() / mt_getrandmax();
+  }
+
+  /**
+   * SQLite-specific implementation of DatabaseConnection::prepare().
+   *
+   * We don't use prepared statements at all at this stage. We just create
+   * a DatabaseStatement_sqlite object, that will create a PDOStatement
+   * using the semi-private PDOPrepare() method below.
+   */
+  public function prepare($query, $options = array()) {
+    return new DatabaseStatement_sqlite($this, $query, $options);
+  }
+
+  /**
+   * NEVER CALL THIS FUNCTION: YOU MIGHT DEADLOCK YOUR PHP PROCESS.
+   *
+   * This is a wrapper around the parent PDO::prepare method. However, as
+   * the PDO SQLite driver only closes SELECT statements when the PDOStatement
+   * destructor is called and SQLite does not allow data change (INSERT,
+   * UPDATE etc) on a table which has open SELECT statements, you should never
+   * call this function and keep a PDOStatement object alive as that can lead
+   * to a deadlock. This really, really should be private, but as
+   * DatabaseStatement_sqlite needs to call it, we have no other choice but to
+   * expose this function to the world.
+   */
+  public function PDOPrepare($query, array $options = array()) {
+    return parent::prepare($query, $options);
+  }
+
+  public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+    return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
+  }
+
+  public function queryTemporary($query, array $args = array(), array $options = array()) {
+    // Generate a new temporary table name and protect it from prefixing.
+    // SQLite requires that temporary tables to be non-qualified.
+    $tablename = $this->generateTemporaryTableName();
+    $prefixes = $this->prefixes;
+    $prefixes[$tablename] = '';
+    $this->setPrefix($prefixes);
+
+    $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options);
+    return $tablename;
+  }
+
+  public function driver() {
+    return 'sqlite';
+  }
+
+  public function databaseType() {
+    return 'sqlite';
+  }
+
+  public function mapConditionOperator($operator) {
+    // We don't want to override any of the defaults.
+    static $specials = array(
+      'LIKE' => array('postfix' => " ESCAPE '\\'"),
+      'NOT LIKE' => array('postfix' => " ESCAPE '\\'"),
+    );
+    return isset($specials[$operator]) ? $specials[$operator] : NULL;
+  }
+
+  public function prepareQuery($query) {
+    return $this->prepare($this->prefixTables($query));
+  }
+
+  public function nextId($existing_id = 0) {
+    $transaction = $this->startTransaction();
+    // We can safely use literal queries here instead of the slower query
+    // builder because if a given database breaks here then it can simply
+    // override nextId. However, this is unlikely as we deal with short strings
+    // and integers and no known databases require special handling for those
+    // simple cases. If another transaction wants to write the same row, it will
+    // wait until this transaction commits.
+    $stmt = $this->query('UPDATE {sequences} SET value = GREATEST(value, :existing_id) + 1', array(
+      ':existing_id' => $existing_id,
+    ));
+    if (!$stmt->rowCount()) {
+      $this->query('INSERT INTO {sequences} (value) VALUES (:existing_id + 1)', array(
+        ':existing_id' => $existing_id,
+      ));
+    }
+    // The transaction gets committed when the transaction object gets destroyed
+    // because it gets out of scope.
+    return $this->query('SELECT value FROM {sequences}')->fetchField();
+  }
+
+  public function rollback($savepoint_name = 'drupal_transaction') {
+    if ($this->savepointSupport) {
+      return parent::rollBack($savepoint_name);
+    }
+
+    if (!$this->inTransaction()) {
+      throw new DatabaseTransactionNoActiveException();
+    }
+    // A previous rollback to an earlier savepoint may mean that the savepoint
+    // in question has already been rolled back.
+    if (!in_array($savepoint_name, $this->transactionLayers)) {
+      return;
+    }
+
+    // We need to find the point we're rolling back to, all other savepoints
+    // before are no longer needed.
+    while ($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint == $savepoint_name) {
+        // Mark whole stack of transactions as needed roll back.
+        $this->willRollback = TRUE;
+        // If it is the last the transaction in the stack, then it is not a
+        // savepoint, it is the transaction itself so we will need to roll back
+        // the transaction rather than a savepoint.
+        if (empty($this->transactionLayers)) {
+          break;
+        }
+        return;
+      }
+    }
+    if ($this->supportsTransactions()) {
+      PDO::rollBack();
+    }
+  }
+
+  public function pushTransaction($name) {
+    if ($this->savepointSupport) {
+      return parent::pushTransaction($name);
+    }
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (isset($this->transactionLayers[$name])) {
+      throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
+    }
+    if (!$this->inTransaction()) {
+      PDO::beginTransaction();
+    }
+    $this->transactionLayers[$name] = $name;
+  }
+
+  public function popTransaction($name) {
+    if ($this->savepointSupport) {
+      return parent::popTransaction($name);
+    }
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (!$this->inTransaction()) {
+      throw new DatabaseTransactionNoActiveException();
+    }
+
+    // Commit everything since SAVEPOINT $name.
+    while($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint != $name) continue;
+
+      // If there are no more layers left then we should commit or rollback.
+      if (empty($this->transactionLayers)) {
+        // If there was any rollback() we should roll back whole transaction.
+        if ($this->willRollback) {
+          $this->willRollback = FALSE;
+          PDO::rollBack();
+        }
+        elseif (!PDO::commit()) {
+          throw new DatabaseTransactionCommitFailedException();
+        }
+      }
+      else {
+        break;
+      }
+    }
+  }
+
+}
+
+/**
+ * Specific SQLite implementation of DatabaseConnection.
+ *
+ * See DatabaseConnection_sqlite::PDOPrepare() for reasons why we must prefetch
+ * the data instead of using PDOStatement.
+ *
+ * @see DatabaseConnection_sqlite::PDOPrepare()
+ */
+class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface {
+
+  /**
+   * SQLite specific implementation of getStatement().
+   *
+   * The PDO SQLite layer doesn't replace numeric placeholders in queries
+   * correctly, and this makes numeric expressions (such as COUNT(*) >= :count)
+   * fail. We replace numeric placeholders in the query ourselves to work
+   * around this bug.
+   *
+   * See http://bugs.php.net/bug.php?id=45259 for more details.
+   */
+  protected function getStatement($query, &$args = array()) {
+    if (count($args)) {
+      // Check if $args is a simple numeric array.
+      if (range(0, count($args) - 1) === array_keys($args)) {
+        // In that case, we have unnamed placeholders.
+        $count = 0;
+        $new_args = array();
+        foreach ($args as $value) {
+          if (is_float($value) || is_int($value)) {
+            if (is_float($value)) {
+              // Force the conversion to float so as not to loose precision
+              // in the automatic cast.
+              $value = sprintf('%F', $value);
+            }
+            $query = substr_replace($query, $value, strpos($query, '?'), 1);
+          }
+          else {
+            $placeholder = ':db_statement_placeholder_' . $count++;
+            $query = substr_replace($query, $placeholder, strpos($query, '?'), 1);
+            $new_args[$placeholder] = $value;
+          }
+        }
+        $args = $new_args;
+      }
+      else {
+        // Else, this is using named placeholders.
+        foreach ($args as $placeholder => $value) {
+          if (is_float($value) || is_int($value)) {
+            if (is_float($value)) {
+              // Force the conversion to float so as not to loose precision
+              // in the automatic cast.
+              $value = sprintf('%F', $value);
+            }
+
+            // We will remove this placeholder from the query as PDO throws an
+            // exception if the number of placeholders in the query and the
+            // arguments does not match.
+            unset($args[$placeholder]);
+            // PDO allows placeholders to not be prefixed by a colon. See
+            // http://marc.info/?l=php-internals&m=111234321827149&w=2 for
+            // more.
+            if ($placeholder[0] != ':') {
+              $placeholder = ":$placeholder";
+            }
+            // When replacing the placeholders, make sure we search for the
+            // exact placeholder. For example, if searching for
+            // ':db_placeholder_1', do not replace ':db_placeholder_11'.
+            $query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query);
+          }
+        }
+      }
+    }
+
+    return $this->dbh->PDOPrepare($query);
+  }
+
+  public function execute($args = array(), $options = array()) {
+    try {
+      $return = parent::execute($args, $options);
+    }
+    catch (PDOException $e) {
+      if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
+        // The schema has changed. SQLite specifies that we must resend the query.
+        $return = parent::execute($args, $options);
+      }
+      else {
+        // Rethrow the exception.
+        throw $e;
+      }
+    }
+
+    // In some weird cases, SQLite will prefix some column names by the name
+    // of the table. We post-process the data, by renaming the column names
+    // using the same convention as MySQL and PostgreSQL.
+    $rename_columns = array();
+    foreach ($this->columnNames as $k => $column) {
+      // In some SQLite versions, SELECT DISTINCT(field) will return "(field)"
+      // instead of "field".
+      if (preg_match("/^\((.*)\)$/", $column, $matches)) {
+        $rename_columns[$column] = $matches[1];
+        $this->columnNames[$k] = $matches[1];
+        $column = $matches[1];
+      }
+
+      // Remove "table." prefixes.
+      if (preg_match("/^.*\.(.*)$/", $column, $matches)) {
+        $rename_columns[$column] = $matches[1];
+        $this->columnNames[$k] = $matches[1];
+      }
+    }
+    if ($rename_columns) {
+      // DatabaseStatementPrefetch already extracted the first row,
+      // put it back into the result set.
+      if (isset($this->currentRow)) {
+        $this->data[0] = &$this->currentRow;
+      }
+
+      // Then rename all the columns across the result set.
+      foreach ($this->data as $k => $row) {
+        foreach ($rename_columns as $old_column => $new_column) {
+          $this->data[$k][$new_column] = $this->data[$k][$old_column];
+          unset($this->data[$k][$old_column]);
+        }
+      }
+
+      // Finally, extract the first row again.
+      $this->currentRow = $this->data[0];
+      unset($this->data[0]);
+    }
+
+    return $return;
+  }
+}
+
+/**
+ * @} End of "addtogroup database".
+ */

+ 51 - 0
includes/database/sqlite/install.inc

@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * SQLite specific install functions
+ */
+
+class DatabaseTasks_sqlite extends DatabaseTasks {
+  protected $pdoDriver = 'sqlite';
+
+  public function name() {
+    return st('SQLite');
+  }
+
+  /**
+   * Minimum engine version.
+   *
+   * @todo: consider upping to 3.6.8 in Drupal 8 to get SAVEPOINT support.
+   */
+  public function minimumVersion() {
+    return '3.3.7';
+  }
+
+  public function getFormOptions($database) {
+    $form = parent::getFormOptions($database);
+
+    // Remove the options that only apply to client/server style databases.
+    unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
+
+    // Make the text more accurate for SQLite.
+    $form['database']['#title'] = st('Database file');
+    $form['database']['#description'] = st('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name()));
+    $default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite';
+    $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
+    return $form;
+  }
+
+  public function validateDatabaseSettings($database) {
+    // Perform standard validation.
+    $errors = parent::validateDatabaseSettings($database);
+
+    // Verify the database is writable.
+    $db_directory = new SplFileInfo(dirname($database['database']));
+    if (!$db_directory->isWritable()) {
+      $errors[$database['driver'] . '][database'] = st('The directory you specified is not writable by the web server.');
+    }
+
+    return $errors;
+  }
+}
+

+ 139 - 0
includes/database/sqlite/query.inc

@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * @file
+ * Query code for SQLite embedded database engine.
+ */
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * SQLite specific implementation of InsertQuery.
+ *
+ * We ignore all the default fields and use the clever SQLite syntax:
+ *   INSERT INTO table DEFAULT VALUES
+ * for degenerated "default only" queries.
+ */
+class InsertQuery_sqlite extends InsertQuery {
+
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+    if (count($this->insertFields)) {
+      return parent::execute();
+    }
+    else {
+      return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', array(), $this->queryOptions);
+    }
+  }
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Produce as many generic placeholders as necessary.
+    $placeholders = array_fill(0, count($this->insertFields), '?');
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') ' . $this->fromQuery;
+    }
+
+    return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')';
+  }
+
+}
+
+/**
+ * SQLite specific implementation of UpdateQuery.
+ *
+ * SQLite counts all the rows that match the conditions as modified, even if they
+ * will not be affected by the query. We workaround this by ensuring that
+ * we don't select those rows.
+ *
+ * A query like this one:
+ *   UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1
+ * will become:
+ *   UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1 AND (col1 <> 'newcol1' OR col2 <> 'newcol2')
+ */
+class UpdateQuery_sqlite extends UpdateQuery {
+  public function execute() {
+    if (!empty($this->queryOptions['sqlite_return_matched_rows'])) {
+      return parent::execute();
+    }
+
+    // Get the fields used in the update query.
+    $fields = $this->expressionFields + $this->fields;
+
+    // Add the inverse of the fields to the condition.
+    $condition = new DatabaseCondition('OR');
+    foreach ($fields as $field => $data) {
+      if (is_array($data)) {
+        // The field is an expression.
+        $condition->where($field . ' <> ' . $data['expression']);
+        $condition->isNull($field);
+      }
+      elseif (!isset($data)) {
+        // The field will be set to NULL.
+        $condition->isNotNull($field);
+      }
+      else {
+        $condition->condition($field, $data, '<>');
+        $condition->isNull($field);
+      }
+    }
+    if (count($condition)) {
+      $condition->compile($this->connection, $this);
+      $this->condition->where((string) $condition, $condition->arguments());
+    }
+    return parent::execute();
+  }
+
+}
+
+/**
+ * SQLite specific implementation of DeleteQuery.
+ *
+ * When the WHERE is omitted from a DELETE statement and the table being deleted
+ * has no triggers, SQLite uses an optimization to erase the entire table content
+ * without having to visit each row of the table individually.
+ *
+ * Prior to SQLite 3.6.5, SQLite does not return the actual number of rows deleted
+ * by that optimized "truncate" optimization.
+ */
+class DeleteQuery_sqlite extends DeleteQuery {
+  public function execute() {
+    if (!count($this->condition)) {
+      $total_rows = $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '}')->fetchField();
+      parent::execute();
+      return $total_rows;
+    }
+    else {
+      return parent::execute();
+    }
+  }
+}
+
+/**
+ * SQLite specific implementation of TruncateQuery.
+ *
+ * SQLite doesn't support TRUNCATE, but a DELETE query with no condition has
+ * exactly the effect (it is implemented by DROPing the table).
+ */
+class TruncateQuery_sqlite extends TruncateQuery {
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
+  }
+}
+
+/**
+ * @} End of "addtogroup database".
+ */

+ 683 - 0
includes/database/sqlite/schema.inc

@@ -0,0 +1,683 @@
+<?php
+
+/**
+ * @file
+ * Database schema code for SQLite databases.
+ */
+
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+class DatabaseSchema_sqlite extends DatabaseSchema {
+
+  /**
+   * Override DatabaseSchema::$defaultSchema
+   */
+  protected $defaultSchema = 'main';
+
+  public function tableExists($table) {
+    $info = $this->getPrefixInfo($table);
+
+    // Don't use {} around sqlite_master table.
+    return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', array(':type' => 'table', ':name' => $info['table']))->fetchField();
+  }
+
+  public function fieldExists($table, $column) {
+    $schema = $this->introspectSchema($table);
+    return !empty($schema['fields'][$column]);
+  }
+
+  /**
+   * Generate SQL to create a new table from a Drupal schema definition.
+   *
+   * @param $name
+   *   The name of the table to create.
+   * @param $table
+   *   A Schema API table definition array.
+   * @return
+   *   An array of SQL statements to create the table.
+   */
+  public function createTableSql($name, $table) {
+    $sql = array();
+    $sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumsSql($name, $table) . "\n);\n";
+    return array_merge($sql, $this->createIndexSql($name, $table));
+  }
+
+  /**
+   * Build the SQL expression for indexes.
+   */
+  protected function createIndexSql($tablename, $schema) {
+    $sql = array();
+    $info = $this->getPrefixInfo($tablename);
+    if (!empty($schema['unique keys'])) {
+      foreach ($schema['unique keys'] as $key => $fields) {
+        $sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
+      }
+    }
+    if (!empty($schema['indexes'])) {
+      foreach ($schema['indexes'] as $key => $fields) {
+        $sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
+      }
+    }
+    return $sql;
+  }
+
+  /**
+   * Build the SQL expression for creating columns.
+   */
+  protected function createColumsSql($tablename, $schema) {
+    $sql_array = array();
+
+    // Add the SQL statement for each field.
+    foreach ($schema['fields'] as $name => $field) {
+      if (isset($field['type']) && $field['type'] == 'serial') {
+        if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) {
+          unset($schema['primary key'][$key]);
+        }
+      }
+      $sql_array[] = $this->createFieldSql($name, $this->processField($field));
+    }
+
+    // Process keys.
+    if (!empty($schema['primary key'])) {
+      $sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")";
+    }
+
+    return implode(", \n", $sql_array);
+  }
+
+  /**
+   * Build the SQL expression for keys.
+   */
+  protected function createKeySql($fields) {
+    $return = array();
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = $field[0];
+      }
+      else {
+        $return[] = $field;
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  /**
+   * Set database-engine specific properties for a field.
+   *
+   * @param $field
+   *   A field description array, as specified in the schema documentation.
+   */
+  protected function processField($field) {
+    if (!isset($field['size'])) {
+      $field['size'] = 'normal';
+    }
+
+    // Set the correct database-engine specific datatype.
+    // In case one is already provided, force it to uppercase.
+    if (isset($field['sqlite_type'])) {
+      $field['sqlite_type'] = drupal_strtoupper($field['sqlite_type']);
+    }
+    else {
+      $map = $this->getFieldTypeMap();
+      $field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']];
+    }
+
+    if (isset($field['type']) && $field['type'] == 'serial') {
+      $field['auto_increment'] = TRUE;
+    }
+
+    return $field;
+  }
+
+  /**
+   * Create an SQL string for a field to be used in table creation or alteration.
+   *
+   * Before passing a field out of a schema definition into this function it has
+   * to be processed by db_processField().
+   *
+   * @param $name
+   *    Name of the field.
+   * @param $spec
+   *    The field specification, as per the schema data structure format.
+   */
+  protected function createFieldSql($name, $spec) {
+    if (!empty($spec['auto_increment'])) {
+      $sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT";
+      if (!empty($spec['unsigned'])) {
+        $sql .= ' CHECK (' . $name . '>= 0)';
+      }
+    }
+    else {
+      $sql = $name . ' ' . $spec['sqlite_type'];
+
+      if (in_array($spec['sqlite_type'], array('VARCHAR', 'TEXT')) && isset($spec['length'])) {
+        $sql .= '(' . $spec['length'] . ')';
+      }
+
+      if (isset($spec['not null'])) {
+        if ($spec['not null']) {
+          $sql .= ' NOT NULL';
+        }
+        else {
+          $sql .= ' NULL';
+        }
+      }
+
+      if (!empty($spec['unsigned'])) {
+        $sql .= ' CHECK (' . $name . '>= 0)';
+      }
+
+      if (isset($spec['default'])) {
+        if (is_string($spec['default'])) {
+          $spec['default'] = "'" . $spec['default'] . "'";
+        }
+        $sql .= ' DEFAULT ' . $spec['default'];
+      }
+
+      if (empty($spec['not null']) && !isset($spec['default'])) {
+        $sql .= ' DEFAULT NULL';
+      }
+    }
+    return $sql;
+  }
+
+  /**
+   * This maps a generic data type in combination with its data size
+   * to the engine-specific data type.
+   */
+  public function getFieldTypeMap() {
+    // Put :normal last so it gets preserved by array_flip. This makes
+    // it much easier for modules (such as schema.module) to map
+    // database types back into schema types.
+    // $map does not use drupal_static as its value never changes.
+    static $map = array(
+      'varchar:normal'  => 'VARCHAR',
+      'char:normal'     => 'CHAR',
+
+      'text:tiny'       => 'TEXT',
+      'text:small'      => 'TEXT',
+      'text:medium'     => 'TEXT',
+      'text:big'        => 'TEXT',
+      'text:normal'     => 'TEXT',
+
+      'serial:tiny'     => 'INTEGER',
+      'serial:small'    => 'INTEGER',
+      'serial:medium'   => 'INTEGER',
+      'serial:big'      => 'INTEGER',
+      'serial:normal'   => 'INTEGER',
+
+      'int:tiny'        => 'INTEGER',
+      'int:small'       => 'INTEGER',
+      'int:medium'      => 'INTEGER',
+      'int:big'         => 'INTEGER',
+      'int:normal'      => 'INTEGER',
+
+      'float:tiny'      => 'FLOAT',
+      'float:small'     => 'FLOAT',
+      'float:medium'    => 'FLOAT',
+      'float:big'       => 'FLOAT',
+      'float:normal'    => 'FLOAT',
+
+      'numeric:normal'  => 'NUMERIC',
+
+      'blob:big'        => 'BLOB',
+      'blob:normal'     => 'BLOB',
+    );
+    return $map;
+  }
+
+  public function renameTable($table, $new_name) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
+    }
+    if ($this->tableExists($new_name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
+    }
+
+    $schema = $this->introspectSchema($table);
+
+    // SQLite doesn't allow you to rename tables outside of the current
+    // database. So the syntax '...RENAME TO database.table' would fail.
+    // So we must determine the full table name here rather than surrounding
+    // the table with curly braces incase the db_prefix contains a reference
+    // to a database outside of our existsing database.
+    $info = $this->getPrefixInfo($new_name);
+    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
+
+    // Drop the indexes, there is no RENAME INDEX command in SQLite.
+    if (!empty($schema['unique keys'])) {
+      foreach ($schema['unique keys'] as $key => $fields) {
+        $this->dropIndex($table, $key);
+      }
+    }
+    if (!empty($schema['indexes'])) {
+      foreach ($schema['indexes'] as $index => $fields) {
+        $this->dropIndex($table, $index);
+      }
+    }
+
+    // Recreate the indexes.
+    $statements = $this->createIndexSql($new_name, $schema);
+    foreach ($statements as $statement) {
+      $this->connection->query($statement);
+    }
+  }
+
+  public function dropTable($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+    $this->connection->tableDropped = TRUE;
+    $this->connection->query('DROP TABLE {' . $table . '}');
+    return TRUE;
+  }
+
+  public function addField($table, $field, $specification, $keys_new = array()) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+    }
+    if ($this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+    }
+
+    // SQLite doesn't have a full-featured ALTER TABLE statement. It only
+    // supports adding new fields to a table, in some simple cases. In most
+    // cases, we have to create a new table and copy the data over.
+    if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
+      // When we don't have to create new keys and we are not creating a
+      // NOT NULL column without a default value, we can use the quicker version.
+      $query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
+      $this->connection->query($query);
+
+      // Apply the initial value if set.
+      if (isset($specification['initial'])) {
+        $this->connection->update($table)
+          ->fields(array($field => $specification['initial']))
+          ->execute();
+      }
+    }
+    else {
+      // We cannot add the field directly. Use the slower table alteration
+      // method, starting from the old schema.
+      $old_schema = $this->introspectSchema($table);
+      $new_schema = $old_schema;
+
+      // Add the new field.
+      $new_schema['fields'][$field] = $specification;
+
+      // Build the mapping between the old fields and the new fields.
+      $mapping = array();
+      if (isset($specification['initial'])) {
+        // If we have a initial value, copy it over.
+        $mapping[$field] = array(
+          'expression' => ':newfieldinitial',
+          'arguments' => array(':newfieldinitial' => $specification['initial']),
+        );
+      }
+      else {
+        // Else use the default of the field.
+        $mapping[$field] = NULL;
+      }
+
+      // Add the new indexes.
+      $new_schema += $keys_new;
+
+      $this->alterTable($table, $old_schema, $new_schema, $mapping);
+    }
+  }
+
+  /**
+   * Create a table with a new schema containing the old content.
+   *
+   * As SQLite does not support ALTER TABLE (with a few exceptions) it is
+   * necessary to create a new table and copy over the old content.
+   *
+   * @param $table
+   *   Name of the table to be altered.
+   * @param $old_schema
+   *   The old schema array for the table.
+   * @param $new_schema
+   *   The new schema array for the table.
+   * @param $mapping
+   *   An optional mapping between the fields of the old specification and the
+   *   fields of the new specification. An associative array, whose keys are
+   *   the fields of the new table, and values can take two possible forms:
+   *     - a simple string, which is interpreted as the name of a field of the
+   *       old table,
+   *     - an associative array with two keys 'expression' and 'arguments',
+   *       that will be used as an expression field.
+   */
+  protected function alterTable($table, $old_schema, $new_schema, array $mapping = array()) {
+    $i = 0;
+    do {
+      $new_table = $table . '_' . $i++;
+    } while ($this->tableExists($new_table));
+
+    $this->createTable($new_table, $new_schema);
+
+    // Build a SQL query to migrate the data from the old table to the new.
+    $select = $this->connection->select($table);
+
+    // Complete the mapping.
+    $possible_keys = array_keys($new_schema['fields']);
+    $mapping += array_combine($possible_keys, $possible_keys);
+
+    // Now add the fields.
+    foreach ($mapping as $field_alias => $field_source) {
+      // Just ignore this field (ie. use it's default value).
+      if (!isset($field_source)) {
+        continue;
+      }
+
+      if (is_array($field_source)) {
+        $select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
+      }
+      else {
+        $select->addField($table, $field_source, $field_alias);
+      }
+    }
+
+    // Execute the data migration query.
+    $this->connection->insert($new_table)
+      ->from($select)
+      ->execute();
+
+    $old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField();
+    $new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField();
+    if ($old_count == $new_count) {
+      $this->dropTable($table);
+      $this->renameTable($new_table, $table);
+    }
+  }
+
+  /**
+   * Find out the schema of a table.
+   *
+   * This function uses introspection methods provided by the database to
+   * create a schema array. This is useful, for example, during update when
+   * the old schema is not available.
+   *
+   * @param $table
+   *   Name of the table.
+   * @return
+   *   An array representing the schema, from drupal_get_schema().
+   * @see drupal_get_schema()
+   */
+  protected function introspectSchema($table) {
+    $mapped_fields = array_flip($this->getFieldTypeMap());
+    $schema = array(
+      'fields' => array(),
+      'primary key' => array(),
+      'unique keys' => array(),
+      'indexes' => array(),
+    );
+
+    $info = $this->getPrefixInfo($table);
+    $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')');
+    foreach ($result as $row) {
+      if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) {
+        $type = $matches[1];
+        $length = $matches[2];
+      }
+      else {
+        $type = $row->type;
+        $length = NULL;
+      }
+      if (isset($mapped_fields[$type])) {
+        list($type, $size) = explode(':', $mapped_fields[$type]);
+        $schema['fields'][$row->name] = array(
+          'type' => $type,
+          'size' => $size,
+          'not null' => !empty($row->notnull),
+          'default' => trim($row->dflt_value, "'"),
+        );
+        if ($length) {
+          $schema['fields'][$row->name]['length'] = $length;
+        }
+        if ($row->pk) {
+          $schema['primary key'][] = $row->name;
+        }
+      }
+      else {
+        new Exception("Unable to parse the column type " . $row->type);
+      }
+    }
+    $indexes = array();
+    $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')');
+    foreach ($result as $row) {
+      if (strpos($row->name, 'sqlite_autoindex_') !== 0) {
+        $indexes[] = array(
+          'schema_key' => $row->unique ? 'unique keys' : 'indexes',
+          'name' => $row->name,
+        );
+      }
+    }
+    foreach ($indexes as $index) {
+      $name = $index['name'];
+      // Get index name without prefix.
+      $index_name = substr($name, strlen($info['table']) + 1);
+      $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')');
+      foreach ($result as $row) {
+        $schema[$index['schema_key']][$index_name][] = $row->name;
+      }
+    }
+    return $schema;
+  }
+
+  public function dropField($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      return FALSE;
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    unset($new_schema['fields'][$field]);
+    foreach ($new_schema['indexes'] as $index => $fields) {
+      foreach ($fields as $key => $field_name) {
+        if ($field_name == $field) {
+          unset($new_schema['indexes'][$index][$key]);
+        }
+      }
+      // If this index has no more fields then remove it.
+      if (empty($new_schema['indexes'][$index])) {
+        unset($new_schema['indexes'][$index]);
+      }
+    }
+    $this->alterTable($table, $old_schema, $new_schema);
+    return TRUE;
+  }
+
+  public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
+    }
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    // Map the old field to the new field.
+    if ($field != $field_new) {
+      $mapping[$field_new] = $field;
+    }
+    else {
+      $mapping = array();
+    }
+
+    // Remove the previous definition and swap in the new one.
+    unset($new_schema['fields'][$field]);
+    $new_schema['fields'][$field_new] = $spec;
+
+    // Map the former indexes to the new column name.
+    $new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping);
+    foreach (array('unique keys', 'indexes') as $k) {
+      foreach ($new_schema[$k] as &$key_definition) {
+        $key_definition = $this->mapKeyDefinition($key_definition, $mapping);
+      }
+    }
+
+    // Add in the keys from $keys_new.
+    if (isset($keys_new['primary key'])) {
+      $new_schema['primary key'] = $keys_new['primary key'];
+    }
+    foreach (array('unique keys', 'indexes') as $k) {
+      if (!empty($keys_new[$k])) {
+        $new_schema[$k] = $keys_new[$k] + $new_schema[$k];
+      }
+    }
+
+    $this->alterTable($table, $old_schema, $new_schema, $mapping);
+  }
+
+  /**
+   * Utility method: rename columns in an index definition according to a new mapping.
+   *
+   * @param $key_definition
+   *   The key definition.
+   * @param $mapping
+   *   The new mapping.
+   */
+  protected function mapKeyDefinition(array $key_definition, array $mapping) {
+    foreach ($key_definition as &$field) {
+      // The key definition can be an array($field, $length).
+      if (is_array($field)) {
+        $field = &$field[0];
+      }
+      if (isset($mapping[$field])) {
+        $field = $mapping[$field];
+      }
+    }
+    return $key_definition;
+  }
+
+  public function addIndex($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
+    }
+
+    $schema['indexes'][$name] = $fields;
+    $statements = $this->createIndexSql($table, $schema);
+    foreach ($statements as $statement) {
+      $this->connection->query($statement);
+    }
+  }
+
+  public function indexExists($table, $name) {
+    $info = $this->getPrefixInfo($table);
+
+    return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != '';
+  }
+
+  public function dropIndex($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $info = $this->getPrefixInfo($table);
+
+    $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
+    return TRUE;
+  }
+
+  public function addUniqueKey($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
+    }
+
+    $schema['unique keys'][$name] = $fields;
+    $statements = $this->createIndexSql($table, $schema);
+    foreach ($statements as $statement) {
+      $this->connection->query($statement);
+    }
+  }
+
+  public function dropUniqueKey($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $info = $this->getPrefixInfo($table);
+
+    $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
+    return TRUE;
+  }
+
+  public function addPrimaryKey($table, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    if (!empty($new_schema['primary key'])) {
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+    }
+
+    $new_schema['primary key'] = $fields;
+    $this->alterTable($table, $old_schema, $new_schema);
+  }
+
+  public function dropPrimaryKey($table) {
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    if (empty($new_schema['primary key'])) {
+      return FALSE;
+    }
+
+    unset($new_schema['primary key']);
+    $this->alterTable($table, $old_schema, $new_schema);
+    return TRUE;
+  }
+
+  public function fieldSetDefault($table, $field, $default) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    $new_schema['fields'][$field]['default'] = $default;
+    $this->alterTable($table, $old_schema, $new_schema);
+  }
+
+  public function fieldSetNoDefault($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    unset($new_schema['fields'][$field]['default']);
+    $this->alterTable($table, $old_schema, $new_schema);
+  }
+
+  public function findTables($table_expression) {
+    // Don't add the prefix, $table_expression already includes the prefix.
+    $info = $this->getPrefixInfo($table_expression, FALSE);
+
+    // Can't use query placeholders for the schema because the query would have
+    // to be :prefixsqlite_master, which does not work.
+    $result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
+      ':type' => 'table',
+      ':table_name' => $info['table'],
+    ));
+    return $result->fetchAllKeyed(0, 0);
+  }
+}

+ 27 - 0
includes/database/sqlite/select.inc

@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Select builder for SQLite embedded database engine.
+ */
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * SQLite specific query builder for SELECT statements.
+ */
+class SelectQuery_sqlite extends SelectQuery {
+  public function forUpdate($set = TRUE) {
+    // SQLite does not support FOR UPDATE so nothing to do.
+    return $this;
+  }
+}
+
+/**
+ * @} End of "addtogroup database".
+ */
+
+

+ 196 - 0
includes/date.inc

@@ -0,0 +1,196 @@
+<?php
+
+/**
+ * @file
+ * Initializes the list of date formats and their locales.
+ */
+
+/**
+ * Provides a default system list of date formats for system_date_formats().
+ */
+function system_default_date_formats() {
+  $formats = array();
+
+  // Short date formats.
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'Y-m-d H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'm/d/Y - H:i',
+    'locales' => array('en-us'),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'd/m/Y - H:i',
+    'locales' => array('en-gb', 'en-hk', 'en-ie', 'el-gr', 'es-es', 'fr-be', 'fr-fr', 'fr-lu', 'it-it', 'nl-be', 'pt-pt'),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'Y/m/d - H:i',
+    'locales' => array('en-ca', 'fr-ca', 'no-no', 'sv-se'),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'd.m.Y - H:i',
+    'locales' => array('de-ch', 'de-de', 'de-lu', 'fi-fi', 'fr-ch', 'is-is', 'pl-pl', 'ro-ro', 'ru-ru'),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'm/d/Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'd/m/Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'Y/m/d - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'M j Y - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'j M Y - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'Y M j - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'M j Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'j M Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'short',
+    'format' => 'Y M j - g:ia',
+    'locales' => array(),
+  );
+
+  // Medium date formats.
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, Y-m-d H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, m/d/Y - H:i',
+    'locales' => array('en-us'),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, d/m/Y - H:i',
+    'locales' => array('en-gb', 'en-hk', 'en-ie', 'el-gr', 'es-es', 'fr-be', 'fr-fr', 'fr-lu', 'it-it', 'nl-be', 'pt-pt'),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, Y/m/d - H:i',
+    'locales' => array('en-ca', 'fr-ca', 'no-no', 'sv-se'),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'F j, Y - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'j F, Y - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'Y, F j - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, m/d/Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, d/m/Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, Y/m/d - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'F j, Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'j F Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'Y, F j - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'j. F Y - G:i',
+    'locales' => array(),
+  );
+
+  // Long date formats.
+  $formats[] = array(
+    'type' => 'long',
+    'format' => 'l, F j, Y - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'long',
+    'format' => 'l, j F, Y - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'long',
+    'format' => 'l, Y,  F j - H:i',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'long',
+    'format' => 'l, F j, Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'long',
+    'format' => 'l, j F Y - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'long',
+    'format' => 'l, Y,  F j - g:ia',
+    'locales' => array(),
+  );
+  $formats[] = array(
+    'type' => 'long',
+    'format' => 'l, j. F Y - G:i',
+    'locales' => array(),
+  );
+
+  return $formats;
+}

+ 1365 - 0
includes/entity.inc

@@ -0,0 +1,1365 @@
+<?php
+
+/**
+ * Interface for entity controller classes.
+ *
+ * All entity controller classes specified via the 'controller class' key
+ * returned by hook_entity_info() or hook_entity_info_alter() have to implement
+ * this interface.
+ *
+ * Most simple, SQL-based entity controllers will do better by extending
+ * DrupalDefaultEntityController instead of implementing this interface
+ * directly.
+ */
+interface DrupalEntityControllerInterface {
+
+  /**
+   * Constructor.
+   *
+   * @param $entityType
+   *   The entity type for which the instance is created.
+   */
+  public function __construct($entityType);
+
+  /**
+   * Resets the internal, static entity cache.
+   *
+   * @param $ids
+   *   (optional) If specified, the cache is reset for the entities with the
+   *   given ids only.
+   */
+  public function resetCache(array $ids = NULL);
+
+  /**
+   * Loads one or more entities.
+   *
+   * @param $ids
+   *   An array of entity IDs, or FALSE to load all entities.
+   * @param $conditions
+   *   An array of conditions in the form 'field' => $value.
+   *
+   * @return
+   *   An array of entity objects indexed by their ids. When no results are
+   *   found, an empty array is returned.
+   */
+  public function load($ids = array(), $conditions = array());
+}
+
+/**
+ * Default implementation of DrupalEntityControllerInterface.
+ *
+ * This class can be used as-is by most simple entity types. Entity types
+ * requiring special handling can extend the class.
+ */
+class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
+
+  /**
+   * Static cache of entities.
+   *
+   * @var array
+   */
+  protected $entityCache;
+
+  /**
+   * Entity type for this controller instance.
+   *
+   * @var string
+   */
+  protected $entityType;
+
+  /**
+   * Array of information about the entity.
+   *
+   * @var array
+   *
+   * @see entity_get_info()
+   */
+  protected $entityInfo;
+
+  /**
+   * Additional arguments to pass to hook_TYPE_load().
+   *
+   * Set before calling DrupalDefaultEntityController::attachLoad().
+   *
+   * @var array
+   */
+  protected $hookLoadArguments;
+
+  /**
+   * Name of the entity's ID field in the entity database table.
+   *
+   * @var string
+   */
+  protected $idKey;
+
+  /**
+   * Name of entity's revision database table field, if it supports revisions.
+   *
+   * Has the value FALSE if this entity does not use revisions.
+   *
+   * @var string
+   */
+  protected $revisionKey;
+
+  /**
+   * The table that stores revisions, if the entity supports revisions.
+   *
+   * @var string
+   */
+  protected $revisionTable;
+
+  /**
+   * Whether this entity type should use the static cache.
+   *
+   * Set by entity info.
+   *
+   * @var boolean
+   */
+  protected $cache;
+
+  /**
+   * Constructor: sets basic variables.
+   */
+  public function __construct($entityType) {
+    $this->entityType = $entityType;
+    $this->entityInfo = entity_get_info($entityType);
+    $this->entityCache = array();
+    $this->hookLoadArguments = array();
+    $this->idKey = $this->entityInfo['entity keys']['id'];
+
+    // Check if the entity type supports revisions.
+    if (!empty($this->entityInfo['entity keys']['revision'])) {
+      $this->revisionKey = $this->entityInfo['entity keys']['revision'];
+      $this->revisionTable = $this->entityInfo['revision table'];
+    }
+    else {
+      $this->revisionKey = FALSE;
+    }
+
+    // Check if the entity type supports static caching of loaded entities.
+    $this->cache = !empty($this->entityInfo['static cache']);
+  }
+
+  /**
+   * Implements DrupalEntityControllerInterface::resetCache().
+   */
+  public function resetCache(array $ids = NULL) {
+    if (isset($ids)) {
+      foreach ($ids as $id) {
+        unset($this->entityCache[$id]);
+      }
+    }
+    else {
+      $this->entityCache = array();
+    }
+  }
+
+  /**
+   * Implements DrupalEntityControllerInterface::load().
+   */
+  public function load($ids = array(), $conditions = array()) {
+    $entities = array();
+
+    // Revisions are not statically cached, and require a different query to
+    // other conditions, so separate the revision id into its own variable.
+    if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
+      $revision_id = $conditions[$this->revisionKey];
+      unset($conditions[$this->revisionKey]);
+    }
+    else {
+      $revision_id = FALSE;
+    }
+
+    // Create a new variable which is either a prepared version of the $ids
+    // array for later comparison with the entity cache, or FALSE if no $ids
+    // were passed. The $ids array is reduced as items are loaded from cache,
+    // and we need to know if it's empty for this reason to avoid querying the
+    // database when all requested entities are loaded from cache.
+    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
+    // Try to load entities from the static cache, if the entity type supports
+    // static caching.
+    if ($this->cache && !$revision_id) {
+      $entities += $this->cacheGet($ids, $conditions);
+      // If any entities were loaded, remove them from the ids still to load.
+      if ($passed_ids) {
+        $ids = array_keys(array_diff_key($passed_ids, $entities));
+      }
+    }
+
+    // Load any remaining entities from the database. This is the case if $ids
+    // is set to FALSE (so we load all entities), if there are any ids left to
+    // load, if loading a revision, or if $conditions was passed without $ids.
+    if ($ids === FALSE || $ids || $revision_id || ($conditions && !$passed_ids)) {
+      // Build the query.
+      $query = $this->buildQuery($ids, $conditions, $revision_id);
+      $queried_entities = $query
+        ->execute()
+        ->fetchAllAssoc($this->idKey);
+    }
+
+    // Pass all entities loaded from the database through $this->attachLoad(),
+    // which attaches fields (if supported by the entity type) and calls the
+    // entity type specific load callback, for example hook_node_load().
+    if (!empty($queried_entities)) {
+      $this->attachLoad($queried_entities, $revision_id);
+      $entities += $queried_entities;
+    }
+
+    if ($this->cache) {
+      // Add entities to the cache if we are not loading a revision.
+      if (!empty($queried_entities) && !$revision_id) {
+        $this->cacheSet($queried_entities);
+      }
+    }
+
+    // Ensure that the returned array is ordered the same as the original
+    // $ids array if this was passed in and remove any invalid ids.
+    if ($passed_ids) {
+      // Remove any invalid ids from the array.
+      $passed_ids = array_intersect_key($passed_ids, $entities);
+      foreach ($entities as $entity) {
+        $passed_ids[$entity->{$this->idKey}] = $entity;
+      }
+      $entities = $passed_ids;
+    }
+
+    return $entities;
+  }
+
+  /**
+   * Builds the query to load the entity.
+   *
+   * This has full revision support. For entities requiring special queries,
+   * the class can be extended, and the default query can be constructed by
+   * calling parent::buildQuery(). This is usually necessary when the object
+   * being loaded needs to be augmented with additional data from another
+   * table, such as loading node type into comments or vocabulary machine name
+   * into terms, however it can also support $conditions on different tables.
+   * See CommentController::buildQuery() or TaxonomyTermController::buildQuery()
+   * for examples.
+   *
+   * @param $ids
+   *   An array of entity IDs, or FALSE to load all entities.
+   * @param $conditions
+   *   An array of conditions in the form 'field' => $value.
+   * @param $revision_id
+   *   The ID of the revision to load, or FALSE if this query is asking for the
+   *   most current revision(s).
+   *
+   * @return SelectQuery
+   *   A SelectQuery object for loading the entity.
+   */
+  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
+    $query = db_select($this->entityInfo['base table'], 'base');
+
+    $query->addTag($this->entityType . '_load_multiple');
+
+    if ($revision_id) {
+      $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $revision_id));
+    }
+    elseif ($this->revisionKey) {
+      $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
+    }
+
+    // Add fields from the {entity} table.
+    $entity_fields = $this->entityInfo['schema_fields_sql']['base table'];
+
+    if ($this->revisionKey) {
+      // Add all fields from the {entity_revision} table.
+      $entity_revision_fields = drupal_map_assoc($this->entityInfo['schema_fields_sql']['revision table']);
+      // The id field is provided by entity, so remove it.
+      unset($entity_revision_fields[$this->idKey]);
+
+      // Remove all fields from the base table that are also fields by the same
+      // name in the revision table.
+      $entity_field_keys = array_flip($entity_fields);
+      foreach ($entity_revision_fields as $key => $name) {
+        if (isset($entity_field_keys[$name])) {
+          unset($entity_fields[$entity_field_keys[$name]]);
+        }
+      }
+      $query->fields('revision', $entity_revision_fields);
+    }
+
+    $query->fields('base', $entity_fields);
+
+    if ($ids) {
+      $query->condition("base.{$this->idKey}", $ids, 'IN');
+    }
+    if ($conditions) {
+      foreach ($conditions as $field => $value) {
+        $query->condition('base.' . $field, $value);
+      }
+    }
+    return $query;
+  }
+
+  /**
+   * Attaches data to entities upon loading.
+   *
+   * This will attach fields, if the entity is fieldable. It calls
+   * hook_entity_load() for modules which need to add data to all entities.
+   * It also calls hook_TYPE_load() on the loaded entities. For example
+   * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
+   * expects special parameters apart from the queried entities, you can set
+   * $this->hookLoadArguments prior to calling the method.
+   * See NodeController::attachLoad() for an example.
+   *
+   * @param $queried_entities
+   *   Associative array of query results, keyed on the entity ID.
+   * @param $revision_id
+   *   ID of the revision that was loaded, or FALSE if the most current revision
+   *   was loaded.
+   */
+  protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
+    // Attach fields.
+    if ($this->entityInfo['fieldable']) {
+      if ($revision_id) {
+        field_attach_load_revision($this->entityType, $queried_entities);
+      }
+      else {
+        field_attach_load($this->entityType, $queried_entities);
+      }
+    }
+
+    // Call hook_entity_load().
+    foreach (module_implements('entity_load') as $module) {
+      $function = $module . '_entity_load';
+      $function($queried_entities, $this->entityType);
+    }
+    // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
+    // always the queried entities, followed by additional arguments set in
+    // $this->hookLoadArguments.
+    $args = array_merge(array($queried_entities), $this->hookLoadArguments);
+    foreach (module_implements($this->entityInfo['load hook']) as $module) {
+      call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args);
+    }
+  }
+
+  /**
+   * Gets entities from the static cache.
+   *
+   * @param $ids
+   *   If not empty, return entities that match these IDs.
+   * @param $conditions
+   *   If set, return entities that match all of these conditions.
+   *
+   * @return
+   *   Array of entities from the entity cache.
+   */
+  protected function cacheGet($ids, $conditions = array()) {
+    $entities = array();
+    // Load any available entities from the internal cache.
+    if (!empty($this->entityCache)) {
+      if ($ids) {
+        $entities += array_intersect_key($this->entityCache, array_flip($ids));
+      }
+      // If loading entities only by conditions, fetch all available entities
+      // from the cache. Entities which don't match are removed later.
+      elseif ($conditions) {
+        $entities = $this->entityCache;
+      }
+    }
+
+    // Exclude any entities loaded from cache if they don't match $conditions.
+    // This ensures the same behavior whether loading from memory or database.
+    if ($conditions) {
+      foreach ($entities as $entity) {
+        $entity_values = (array) $entity;
+        if (array_diff_assoc($conditions, $entity_values)) {
+          unset($entities[$entity->{$this->idKey}]);
+        }
+      }
+    }
+    return $entities;
+  }
+
+  /**
+   * Stores entities in the static entity cache.
+   *
+   * @param $entities
+   *   Entities to store in the cache.
+   */
+  protected function cacheSet($entities) {
+    $this->entityCache += $entities;
+  }
+}
+
+/**
+ * Exception thrown by EntityFieldQuery() on unsupported query syntax.
+ *
+ * Some storage modules might not support the full range of the syntax for
+ * conditions, and will raise an EntityFieldQueryException when an unsupported
+ * condition was specified.
+ */
+class EntityFieldQueryException extends Exception {}
+
+/**
+ * Retrieves entities matching a given set of conditions.
+ *
+ * This class allows finding entities based on entity properties (for example,
+ * node->changed), field values, and generic entity meta data (bundle,
+ * entity type, entity id, and revision ID). It is not possible to query across
+ * multiple entity types. For example, there is no facility to find published
+ * nodes written by users created in the last hour, as this would require
+ * querying both node->status and user->created.
+ *
+ * Normally we would not want to have public properties on the object, as that
+ * allows the object's state to become inconsistent too easily. However, this
+ * class's standard use case involves primarily code that does need to have
+ * direct access to the collected properties in order to handle alternate
+ * execution routines. We therefore use public properties for simplicity. Note
+ * that code that is simply creating and running a field query should still use
+ * the appropriate methods to add conditions on the query.
+ *
+ * Storage engines are not required to support every type of query. By default,
+ * an EntityFieldQueryException will be raised if an unsupported condition is
+ * specified or if the query has field conditions or sorts that are stored in
+ * different field storage engines. However, this logic can be overridden in
+ * hook_entity_query_alter().
+ *
+ * Also note that this query does not automatically respect entity access
+ * restrictions. Node access control is performed by the SQL storage engine but
+ * other storage engines might not do this.
+ */
+class EntityFieldQuery {
+
+  /**
+   * Indicates that both deleted and non-deleted fields should be returned.
+   *
+   * @see EntityFieldQuery::deleted()
+   */
+  const RETURN_ALL = NULL;
+
+  /**
+   * TRUE if the query has already been altered, FALSE if it hasn't.
+   *
+   * Used in alter hooks to check for cloned queries that have already been
+   * altered prior to the clone (for example, the pager count query).
+   *
+   * @var boolean
+   */
+  public $altered = FALSE;
+
+  /**
+   * Associative array of entity-generic metadata conditions.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::entityCondition()
+   */
+  public $entityConditions = array();
+
+  /**
+   * List of field conditions.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::fieldCondition()
+   */
+  public $fieldConditions = array();
+
+  /**
+   * List of field meta conditions (language and delta).
+   *
+   * Field conditions operate on columns specified by hook_field_schema(),
+   * the meta conditions operate on columns added by the system: delta
+   * and language. These can not be mixed with the field conditions because
+   * field columns can have any name including delta and language.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::fieldLanguageCondition()
+   * @see EntityFieldQuery::fieldDeltaCondition()
+   */
+  public $fieldMetaConditions = array();
+
+  /**
+   * List of property conditions.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::propertyCondition()
+   */
+  public $propertyConditions = array();
+
+  /**
+   * List of order clauses.
+   *
+   * @var array
+   */
+  public $order = array();
+
+  /**
+   * The query range.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::range()
+   */
+  public $range = array();
+
+  /**
+   * The query pager data.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::pager()
+   */
+  public $pager = array();
+
+  /**
+   * Query behavior for deleted data.
+   *
+   * TRUE to return only deleted data, FALSE to return only non-deleted data,
+   * EntityFieldQuery::RETURN_ALL to return everything.
+   *
+   * @see EntityFieldQuery::deleted()
+   */
+  public $deleted = FALSE;
+
+  /**
+   * A list of field arrays used.
+   *
+   * Field names passed to EntityFieldQuery::fieldCondition() and
+   * EntityFieldQuery::fieldOrderBy() are run through field_info_field() before
+   * stored in this array. This way, the elements of this array are field
+   * arrays.
+   *
+   * @var array
+   */
+  public $fields = array();
+
+  /**
+   * TRUE if this is a count query, FALSE if it isn't.
+   *
+   * @var boolean
+   */
+  public $count = FALSE;
+
+  /**
+   * Flag indicating whether this is querying current or all revisions.
+   *
+   * @var int
+   *
+   * @see EntityFieldQuery::age()
+   */
+  public $age = FIELD_LOAD_CURRENT;
+
+  /**
+   * A list of the tags added to this query.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::addTag()
+   */
+  public $tags = array();
+
+  /**
+   * A list of metadata added to this query.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::addMetaData()
+   */
+  public $metaData = array();
+
+  /**
+   * The ordered results.
+   *
+   * @var array
+   *
+   * @see EntityFieldQuery::execute().
+   */
+  public $orderedResults = array();
+
+  /**
+   * The method executing the query, if it is overriding the default.
+   *
+   * @var string
+   *
+   * @see EntityFieldQuery::execute().
+   */
+  public $executeCallback = '';
+
+  /**
+   * Adds a condition on entity-generic metadata.
+   *
+   * If the overall query contains only entity conditions or ordering, or if
+   * there are property conditions, then specifying the entity type is
+   * mandatory. If there are field conditions or ordering but no property
+   * conditions or ordering, then specifying an entity type is optional. While
+   * the field storage engine might support field conditions on more than one
+   * entity type, there is no way to query across multiple entity base tables by
+   * default. To specify the entity type, pass in 'entity_type' for $name,
+   * the type as a string for $value, and no $operator (it's disregarded).
+   *
+   * 'bundle', 'revision_id' and 'entity_id' have no such restrictions.
+   *
+   * Note: The "comment" entity type does not support bundle conditions.
+   *
+   * @param $name
+   *   'entity_type', 'bundle', 'revision_id' or 'entity_id'.
+   * @param $value
+   *   The value for $name. In most cases, this is a scalar. For more complex
+   *   options, it is an array. The meaning of each element in the array is
+   *   dependent on $operator.
+   * @param $operator
+   *   Possible values:
+   *   - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
+   *     operators expect $value to be a literal of the same type as the
+   *     column.
+   *   - 'IN', 'NOT IN': These operators expect $value to be an array of
+   *     literals of the same type as the column.
+   *   - 'BETWEEN': This operator expects $value to be an array of two literals
+   *     of the same type as the column.
+   *   The operator can be omitted, and will default to 'IN' if the value is an
+   *   array, or to '=' otherwise.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function entityCondition($name, $value, $operator = NULL) {
+    // The '!=' operator is deprecated in favour of the '<>' operator since the
+    // latter is ANSI SQL compatible.
+    if ($operator == '!=') {
+      $operator = '<>';
+    }
+    $this->entityConditions[$name] = array(
+      'value' => $value,
+      'operator' => $operator,
+    );
+    return $this;
+  }
+
+  /**
+   * Adds a condition on field values.
+   * 
+   * Note that entities with empty field values will be excluded from the
+   * EntityFieldQuery results when using this method.
+   *
+   * @param $field
+   *   Either a field name or a field array.
+   * @param $column
+   *   The column that should hold the value to be matched.
+   * @param $value
+   *   The value to test the column value against.
+   * @param $operator
+   *   The operator to be used to test the given value.
+   * @param $delta_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $delta_group.
+   * @param $language_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $language_group.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   *
+   * @see EntityFieldQuery::addFieldCondition
+   * @see EntityFieldQuery::deleted
+   */
+  public function fieldCondition($field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
+    return $this->addFieldCondition($this->fieldConditions, $field, $column, $value, $operator, $delta_group, $language_group);
+  }
+
+  /**
+   * Adds a condition on the field language column.
+   *
+   * @param $field
+   *   Either a field name or a field array.
+   * @param $value
+   *   The value to test the column value against.
+   * @param $operator
+   *   The operator to be used to test the given value.
+   * @param $delta_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $delta_group.
+   * @param $language_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $language_group.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   *
+   * @see EntityFieldQuery::addFieldCondition
+   * @see EntityFieldQuery::deleted
+   */
+  public function fieldLanguageCondition($field, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
+    return $this->addFieldCondition($this->fieldMetaConditions, $field, 'language', $value, $operator, $delta_group, $language_group);
+  }
+
+  /**
+   * Adds a condition on the field delta column.
+   *
+   * @param $field
+   *   Either a field name or a field array.
+   * @param $value
+   *   The value to test the column value against.
+   * @param $operator
+   *   The operator to be used to test the given value.
+   * @param $delta_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $delta_group.
+   * @param $language_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $language_group.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   *
+   * @see EntityFieldQuery::addFieldCondition
+   * @see EntityFieldQuery::deleted
+   */
+  public function fieldDeltaCondition($field, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
+    return $this->addFieldCondition($this->fieldMetaConditions, $field, 'delta', $value, $operator, $delta_group, $language_group);
+  }
+
+  /**
+   * Adds the given condition to the proper condition array.
+   *
+   * @param $conditions
+   *   A reference to an array of conditions.
+   * @param $field
+   *   Either a field name or a field array.
+   * @param $column
+   *   A column defined in the hook_field_schema() of this field. If this is
+   *   omitted then the query will find only entities that have data in this
+   *   field, using the entity and property conditions if there are any.
+   * @param $value
+   *   The value to test the column value against. In most cases, this is a
+   *   scalar. For more complex options, it is an array. The meaning of each
+   *   element in the array is dependent on $operator.
+   * @param $operator
+   *   Possible values:
+   *   - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
+   *     operators expect $value to be a literal of the same type as the
+   *     column.
+   *   - 'IN', 'NOT IN': These operators expect $value to be an array of
+   *     literals of the same type as the column.
+   *   - 'BETWEEN': This operator expects $value to be an array of two literals
+   *     of the same type as the column.
+   *   The operator can be omitted, and will default to 'IN' if the value is an
+   *   array, or to '=' otherwise.
+   * @param $delta_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $delta_group. For example, let's presume a multivalue field which has
+   *   two columns, 'color' and 'shape', and for entity id 1, there are two
+   *   values: red/square and blue/circle. Entity ID 1 does not have values
+   *   corresponding to 'red circle', however if you pass 'red' and 'circle' as
+   *   conditions, it will appear in the  results - by default queries will run
+   *   against any combination of deltas. By passing the conditions with the
+   *   same $delta_group it will ensure that only values attached to the same
+   *   delta are matched, and entity 1 would then be excluded from the results.
+   * @param $language_group
+   *   An arbitrary identifier: conditions in the same group must have the same
+   *   $language_group.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  protected function addFieldCondition(&$conditions, $field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
+    // The '!=' operator is deprecated in favour of the '<>' operator since the
+    // latter is ANSI SQL compatible.
+    if ($operator == '!=') {
+      $operator = '<>';
+    }
+    if (is_scalar($field)) {
+      $field_definition = field_info_field($field);
+      if (empty($field_definition)) {
+        throw new EntityFieldQueryException(t('Unknown field: @field_name', array('@field_name' => $field)));
+      }
+      $field = $field_definition;
+    }
+    // Ensure the same index is used for field conditions as for fields.
+    $index = count($this->fields);
+    $this->fields[$index] = $field;
+    if (isset($column)) {
+      $conditions[$index] = array(
+        'field' => $field,
+        'column' => $column,
+        'value' => $value,
+        'operator' => $operator,
+        'delta_group' => $delta_group,
+        'language_group' => $language_group,
+      );
+    }
+    return $this;
+  }
+
+  /**
+   * Adds a condition on an entity-specific property.
+   *
+   * An $entity_type must be specified by calling
+   * EntityFieldCondition::entityCondition('entity_type', $entity_type) before
+   * executing the query. Also, by default only entities stored in SQL are
+   * supported; however, EntityFieldQuery::executeCallback can be set to handle
+   * different entity storage.
+   *
+   * @param $column
+   *   A column defined in the hook_schema() of the base table of the entity.
+   * @param $value
+   *   The value to test the field against. In most cases, this is a scalar. For
+   *   more complex options, it is an array. The meaning of each element in the
+   *   array is dependent on $operator.
+   * @param $operator
+   *   Possible values:
+   *   - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
+   *     operators expect $value to be a literal of the same type as the
+   *     column.
+   *   - 'IN', 'NOT IN': These operators expect $value to be an array of
+   *     literals of the same type as the column.
+   *   - 'BETWEEN': This operator expects $value to be an array of two literals
+   *     of the same type as the column.
+   *   The operator can be omitted, and will default to 'IN' if the value is an
+   *   array, or to '=' otherwise.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function propertyCondition($column, $value, $operator = NULL) {
+    // The '!=' operator is deprecated in favour of the '<>' operator since the
+    // latter is ANSI SQL compatible.
+    if ($operator == '!=') {
+      $operator = '<>';
+    }
+    $this->propertyConditions[] = array(
+      'column' => $column,
+      'value' => $value,
+      'operator' => $operator,
+    );
+    return $this;
+  }
+
+  /**
+   * Orders the result set by entity-generic metadata.
+   *
+   * If called multiple times, the query will order by each specified column in
+   * the order this method is called.
+   *
+   * Note: The "comment" and "taxonomy_term" entity types don't support ordering
+   * by bundle. For "taxonomy_term", propertyOrderBy('vid') can be used instead.
+   *
+   * @param $name
+   *   'entity_type', 'bundle', 'revision_id' or 'entity_id'.
+   * @param $direction
+   *   The direction to sort. Legal values are "ASC" and "DESC".
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function entityOrderBy($name, $direction = 'ASC') {
+    $this->order[] = array(
+      'type' => 'entity',
+      'specifier' => $name,
+      'direction' => $direction,
+    );
+    return $this;
+  }
+
+  /**
+   * Orders the result set by a given field column.
+   *
+   * If called multiple times, the query will order by each specified column in
+   * the order this method is called. Note that entities with empty field
+   * values will be excluded from the EntityFieldQuery results when using this
+   * method.
+   *
+   * @param $field
+   *   Either a field name or a field array.
+   * @param $column
+   *   A column defined in the hook_field_schema() of this field. entity_id and
+   *   bundle can also be used.
+   * @param $direction
+   *   The direction to sort. Legal values are "ASC" and "DESC".
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function fieldOrderBy($field, $column, $direction = 'ASC') {
+    if (is_scalar($field)) {
+      $field_definition = field_info_field($field);
+      if (empty($field_definition)) {
+        throw new EntityFieldQueryException(t('Unknown field: @field_name', array('@field_name' => $field)));
+      }
+      $field = $field_definition;
+    }
+    // Save the index used for the new field, for later use in field storage.
+    $index = count($this->fields);
+    $this->fields[$index] = $field;
+    $this->order[] = array(
+      'type' => 'field',
+      'specifier' => array(
+        'field' => $field,
+        'index' => $index,
+        'column' => $column,
+      ),
+      'direction' => $direction,
+    );
+    return $this;
+  }
+
+  /**
+   * Orders the result set by an entity-specific property.
+   *
+   * An $entity_type must be specified by calling
+   * EntityFieldCondition::entityCondition('entity_type', $entity_type) before
+   * executing the query.
+   *
+   * If called multiple times, the query will order by each specified column in
+   * the order this method is called.
+   *
+   * @param $column
+   *   The column on which to order.
+   * @param $direction
+   *   The direction to sort. Legal values are "ASC" and "DESC".
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function propertyOrderBy($column, $direction = 'ASC') {
+    $this->order[] = array(
+      'type' => 'property',
+      'specifier' => $column,
+      'direction' => $direction,
+    );
+    return $this;
+  }
+
+  /**
+   * Sets the query to be a count query only.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function count() {
+    $this->count = TRUE;
+    return $this;
+  }
+
+  /**
+   * Restricts a query to a given range in the result set.
+   *
+   * @param $start
+   *   The first entity from the result set to return. If NULL, removes any
+   *   range directives that are set.
+   * @param $length
+   *   The number of entities to return from the result set.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function range($start = NULL, $length = NULL) {
+    $this->range = array(
+      'start' => $start,
+      'length' => $length,
+    );
+    return $this;
+  }
+
+  /**
+   * Enables a pager for the query.
+   *
+   * @param $limit
+   *   An integer specifying the number of elements per page.  If passed a false
+   *   value (FALSE, 0, NULL), the pager is disabled.
+   * @param $element
+   *   An optional integer to distinguish between multiple pagers on one page.
+   *   If not provided, one is automatically calculated.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function pager($limit = 10, $element = NULL) {
+    if (!isset($element)) {
+      $element = PagerDefault::$maxElement++;
+    }
+    elseif ($element >= PagerDefault::$maxElement) {
+      PagerDefault::$maxElement = $element + 1;
+    }
+
+    $this->pager = array(
+      'limit' => $limit,
+      'element' => $element,
+    );
+    return $this;
+  }
+
+  /**
+   * Enables sortable tables for this query.
+   *
+   * @param $headers
+   *   An EFQ Header array based on which the order clause is added to the
+   *   query.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function tableSort(&$headers) {
+    // If 'field' is not initialized, the header columns aren't clickable
+    foreach ($headers as $key =>$header) {
+      if (is_array($header) && isset($header['specifier'])) {
+        $headers[$key]['field'] = '';
+      }
+    }
+
+    $order = tablesort_get_order($headers);
+    $direction = tablesort_get_sort($headers);
+    foreach ($headers as $header) {
+      if (is_array($header) && ($header['data'] == $order['name'])) {
+        if ($header['type'] == 'field') {
+          $this->fieldOrderBy($header['specifier']['field'], $header['specifier']['column'], $direction);
+        }
+        else {
+          $header['direction'] = $direction;
+          $this->order[] = $header;
+        }
+      }
+    }
+
+    return $this;
+  }
+
+  /**
+   * Filters on the data being deleted.
+   *
+   * @param $deleted
+   *   TRUE to only return deleted data, FALSE to return non-deleted data,
+   *   EntityFieldQuery::RETURN_ALL to return everything. Defaults to FALSE.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function deleted($deleted = TRUE) {
+    $this->deleted = $deleted;
+    return $this;
+  }
+
+  /**
+   * Queries the current or every revision.
+   *
+   * Note that this only affects field conditions. Property conditions always
+   * apply to the current revision.
+   * @TODO: Once revision tables have been cleaned up, revisit this.
+   *
+   * @param $age
+   *   - FIELD_LOAD_CURRENT (default): Query the most recent revisions for all
+   *     entities. The results will be keyed by entity type and entity ID.
+   *   - FIELD_LOAD_REVISION: Query all revisions. The results will be keyed by
+   *     entity type and entity revision ID.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function age($age) {
+    $this->age = $age;
+    return $this;
+  }
+
+  /**
+   * Adds a tag to the query.
+   *
+   * Tags are strings that mark a query so that hook_query_alter() and
+   * hook_query_TAG_alter() implementations may decide if they wish to alter
+   * the query. A query may have any number of tags, and they must be valid PHP
+   * identifiers (composed of letters, numbers, and underscores). For example,
+   * queries involving nodes that will be displayed for a user need to add the
+   * tag 'node_access', so that the node module can add access restrictions to
+   * the query.
+   *
+   * If an entity field query has tags, it must also have an entity type
+   * specified, because the alter hook will need the entity base table.
+   *
+   * @param string $tag
+   *   The tag to add.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function addTag($tag) {
+    $this->tags[$tag] = $tag;
+    return $this;
+  }
+
+  /**
+   * Adds additional metadata to the query.
+   *
+   * Sometimes a query may need to provide additional contextual data for the
+   * alter hook. The alter hook implementations may then use that information
+   * to decide if and how to take action.
+   *
+   * @param $key
+   *   The unique identifier for this piece of metadata. Must be a string that
+   *   follows the same rules as any other PHP identifier.
+   * @param $object
+   *   The additional data to add to the query. May be any valid PHP variable.
+   *
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function addMetaData($key, $object) {
+    $this->metaData[$key] = $object;
+    return $this;
+  }
+
+  /**
+   * Executes the query.
+   *
+   * After executing the query, $this->orderedResults will contain a list of
+   * the same stub entities in the order returned by the query. This is only
+   * relevant if there are multiple entity types in the returned value and
+   * a field ordering was requested. In every other case, the returned value
+   * contains everything necessary for processing.
+   *
+   * @return
+   *   Either a number if count() was called or an array of associative arrays
+   *   of stub entities. The outer array keys are entity types, and the inner
+   *   array keys are the relevant ID. (In most cases this will be the entity
+   *   ID. The only exception is when age=FIELD_LOAD_REVISION is used and field
+   *   conditions or sorts are present -- in this case, the key will be the
+   *   revision ID.) The entity type will only exist in the outer array if
+   *   results were found. The inner array values are always stub entities, as
+   *   returned by entity_create_stub_entity(). To traverse the returned array:
+   *   @code
+   *     foreach ($query->execute() as $entity_type => $entities) {
+   *       foreach ($entities as $entity_id => $entity) {
+   *   @endcode
+   *   Note if the entity type is known, then the following snippet will load
+   *   the entities found:
+   *   @code
+   *     $result = $query->execute();
+   *     if (!empty($result[$my_type])) {
+   *       $entities = entity_load($my_type, array_keys($result[$my_type]));
+   *     }
+   *   @endcode
+   */
+  public function execute() {
+    // Give a chance to other modules to alter the query.
+    drupal_alter('entity_query', $this);
+    $this->altered = TRUE;
+
+    // Initialize the pager.
+    $this->initializePager();
+
+    // Execute the query using the correct callback.
+    $result = call_user_func($this->queryCallback(), $this);
+
+    return $result;
+  }
+
+  /**
+   * Determines the query callback to use for this entity query.
+   *
+   * @return
+   *   A callback that can be used with call_user_func().
+   */
+  public function queryCallback() {
+    // Use the override from $this->executeCallback. It can be set either
+    // while building the query, or using hook_entity_query_alter().
+    if (function_exists($this->executeCallback)) {
+      return $this->executeCallback;
+    }
+    // If there are no field conditions and sorts, and no execute callback
+    // then we default to querying entity tables in SQL.
+    if (empty($this->fields)) {
+      return array($this, 'propertyQuery');
+    }
+    // If no override, find the storage engine to be used.
+    foreach ($this->fields as $field) {
+      if (!isset($storage)) {
+        $storage = $field['storage']['module'];
+      }
+      elseif ($storage != $field['storage']['module']) {
+        throw new EntityFieldQueryException(t("Can't handle more than one field storage engine"));
+      }
+    }
+    if ($storage) {
+      // Use hook_field_storage_query() from the field storage.
+      return $storage . '_field_storage_query';
+    }
+    else {
+      throw new EntityFieldQueryException(t("Field storage engine not found."));
+    }
+  }
+
+  /**
+   * Queries entity tables in SQL for property conditions and sorts.
+   *
+   * This method is only used if there are no field conditions and sorts.
+   *
+   * @return
+   *   See EntityFieldQuery::execute().
+   */
+  protected function propertyQuery() {
+    if (empty($this->entityConditions['entity_type'])) {
+      throw new EntityFieldQueryException(t('For this query an entity type must be specified.'));
+    }
+    $entity_type = $this->entityConditions['entity_type']['value'];
+    $entity_info = entity_get_info($entity_type);
+    if (empty($entity_info['base table'])) {
+      throw new EntityFieldQueryException(t('Entity %entity has no base table.', array('%entity' => $entity_type)));
+    }
+    $base_table = $entity_info['base table'];
+    $base_table_schema = drupal_get_schema($base_table);
+    $select_query = db_select($base_table);
+    $select_query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type));
+    // Process the property conditions.
+    foreach ($this->propertyConditions as $property_condition) {
+      $this->addCondition($select_query, $base_table . '.' . $property_condition['column'], $property_condition);
+    }
+    // Process the four possible entity condition.
+    // The id field is always present in entity keys.
+    $sql_field = $entity_info['entity keys']['id'];
+    $id_map['entity_id'] = $sql_field;
+    $select_query->addField($base_table, $sql_field, 'entity_id');
+    if (isset($this->entityConditions['entity_id'])) {
+      $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['entity_id']);
+    }
+
+    // If there is a revision key defined, use it.
+    if (!empty($entity_info['entity keys']['revision'])) {
+      $sql_field = $entity_info['entity keys']['revision'];
+      $select_query->addField($base_table, $sql_field, 'revision_id');
+      if (isset($this->entityConditions['revision_id'])) {
+        $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['revision_id']);
+      }
+    }
+    else {
+      $sql_field = 'revision_id';
+      $select_query->addExpression('NULL', 'revision_id');
+    }
+    $id_map['revision_id'] = $sql_field;
+
+    // Handle bundles.
+    if (!empty($entity_info['entity keys']['bundle'])) {
+      $sql_field = $entity_info['entity keys']['bundle'];
+      $having = FALSE;
+
+      if (!empty($base_table_schema['fields'][$sql_field])) {
+        $select_query->addField($base_table, $sql_field, 'bundle');
+      }
+    }
+    else {
+      $sql_field = 'bundle';
+      $select_query->addExpression(':bundle', 'bundle', array(':bundle' => $entity_type));
+      $having = TRUE;
+    }
+    $id_map['bundle'] = $sql_field;
+    if (isset($this->entityConditions['bundle'])) {
+      if (!empty($entity_info['entity keys']['bundle'])) {
+        $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['bundle'], $having);
+      }
+      else {
+        // This entity has no bundle, so invalidate the query.
+        $select_query->where('1 = 0');
+      }
+    }
+
+    // Order the query.
+    foreach ($this->order as $order) {
+      if ($order['type'] == 'entity') {
+        $key = $order['specifier'];
+        if (!isset($id_map[$key])) {
+          throw new EntityFieldQueryException(t('Do not know how to order on @key for @entity_type', array('@key' => $key, '@entity_type' => $entity_type)));
+        }
+        $select_query->orderBy($id_map[$key], $order['direction']);
+      }
+      elseif ($order['type'] == 'property') {
+        $select_query->orderBy($base_table . '.' . $order['specifier'], $order['direction']);
+      }
+    }
+
+    return $this->finishQuery($select_query);
+  }
+
+  /**
+   * Gets the total number of results and initializes a pager for the query.
+   *
+   * The pager can be disabled by either setting the pager limit to 0, or by
+   * setting this query to be a count query.
+   */
+  function initializePager() {
+    if ($this->pager && !empty($this->pager['limit']) && !$this->count) {
+      $page = pager_find_page($this->pager['element']);
+      $count_query = clone $this;
+      $this->pager['total'] = $count_query->count()->execute();
+      $this->pager['start'] = $page * $this->pager['limit'];
+      pager_default_initialize($this->pager['total'], $this->pager['limit'], $this->pager['element']);
+      $this->range($this->pager['start'], $this->pager['limit']);
+    }
+  }
+
+  /**
+   * Finishes the query.
+   *
+   * Adds tags, metaData, range and returns the requested list or count.
+   *
+   * @param SelectQuery $select_query
+   *   A SelectQuery which has entity_type, entity_id, revision_id and bundle
+   *   fields added.
+   * @param $id_key
+   *   Which field's values to use as the returned array keys.
+   *
+   * @return
+   *   See EntityFieldQuery::execute().
+   */
+  function finishQuery($select_query, $id_key = 'entity_id') {
+    foreach ($this->tags as $tag) {
+      $select_query->addTag($tag);
+    }
+    foreach ($this->metaData as $key => $object) {
+      $select_query->addMetaData($key, $object);
+    }
+    $select_query->addMetaData('entity_field_query', $this);
+    if ($this->range) {
+      $select_query->range($this->range['start'], $this->range['length']);
+    }
+    if ($this->count) {
+      return $select_query->countQuery()->execute()->fetchField();
+    }
+    $return = array();
+    foreach ($select_query->execute() as $partial_entity) {
+      $bundle = isset($partial_entity->bundle) ? $partial_entity->bundle : NULL;
+      $entity = entity_create_stub_entity($partial_entity->entity_type, array($partial_entity->entity_id, $partial_entity->revision_id, $bundle));
+      $return[$partial_entity->entity_type][$partial_entity->$id_key] = $entity;
+      $this->ordered_results[] = $partial_entity;
+    }
+    return $return;
+  }
+
+  /**
+   * Adds a condition to an already built SelectQuery (internal function).
+   *
+   * This is a helper for hook_entity_query() and hook_field_storage_query().
+   *
+   * @param SelectQuery $select_query
+   *   A SelectQuery object.
+   * @param $sql_field
+   *   The name of the field.
+   * @param $condition
+   *   A condition as described in EntityFieldQuery::fieldCondition() and
+   *   EntityFieldQuery::entityCondition().
+   * @param $having
+   *   HAVING or WHERE. This is necessary because SQL can't handle WHERE
+   *   conditions on aliased columns.
+   */
+  public function addCondition(SelectQuery $select_query, $sql_field, $condition, $having = FALSE) {
+    $method = $having ? 'havingCondition' : 'condition';
+    $like_prefix = '';
+    switch ($condition['operator']) {
+      case 'CONTAINS':
+        $like_prefix = '%';
+      case 'STARTS_WITH':
+        $select_query->$method($sql_field, $like_prefix . db_like($condition['value']) . '%', 'LIKE');
+        break;
+      default:
+        $select_query->$method($sql_field, $condition['value'], $condition['operator']);
+    }
+  }
+
+}
+
+/**
+ * Defines an exception thrown when a malformed entity is passed.
+ */
+class EntityMalformedException extends Exception { }

+ 286 - 0
includes/errors.inc

@@ -0,0 +1,286 @@
+<?php
+
+/**
+ * @file
+ * Functions for error handling.
+ */
+
+/**
+ * Maps PHP error constants to watchdog severity levels.
+ *
+ * The error constants are documented at
+ * http://php.net/manual/en/errorfunc.constants.php
+ *
+ * @ingroup logging_severity_levels
+ */
+function drupal_error_levels() {
+  $types = array(
+    E_ERROR => array('Error', WATCHDOG_ERROR),
+    E_WARNING => array('Warning', WATCHDOG_WARNING),
+    E_PARSE => array('Parse error', WATCHDOG_ERROR),
+    E_NOTICE => array('Notice', WATCHDOG_NOTICE),
+    E_CORE_ERROR => array('Core error', WATCHDOG_ERROR),
+    E_CORE_WARNING => array('Core warning', WATCHDOG_WARNING),
+    E_COMPILE_ERROR => array('Compile error', WATCHDOG_ERROR),
+    E_COMPILE_WARNING => array('Compile warning', WATCHDOG_WARNING),
+    E_USER_ERROR => array('User error', WATCHDOG_ERROR),
+    E_USER_WARNING => array('User warning', WATCHDOG_WARNING),
+    E_USER_NOTICE => array('User notice', WATCHDOG_NOTICE),
+    E_STRICT => array('Strict warning', WATCHDOG_DEBUG),
+    E_RECOVERABLE_ERROR => array('Recoverable fatal error', WATCHDOG_ERROR),
+  );
+  // E_DEPRECATED and E_USER_DEPRECATED were added in PHP 5.3.0.
+  if (defined('E_DEPRECATED')) {
+    $types[E_DEPRECATED] = array('Deprecated function', WATCHDOG_DEBUG);
+    $types[E_USER_DEPRECATED] = array('User deprecated function', WATCHDOG_DEBUG);
+  }
+  return $types;
+}
+
+/**
+ * Provides custom PHP error handling.
+ *
+ * @param $error_level
+ *   The level of the error raised.
+ * @param $message
+ *   The error message.
+ * @param $filename
+ *   The filename that the error was raised in.
+ * @param $line
+ *   The line number the error was raised at.
+ * @param $context
+ *   An array that points to the active symbol table at the point the error
+ *   occurred.
+ */
+function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) {
+  if ($error_level & error_reporting()) {
+    $types = drupal_error_levels();
+    list($severity_msg, $severity_level) = $types[$error_level];
+    $caller = _drupal_get_last_caller(debug_backtrace());
+
+    if (!function_exists('filter_xss_admin')) {
+      require_once DRUPAL_ROOT . '/includes/common.inc';
+    }
+
+    // We treat recoverable errors as fatal.
+    _drupal_log_error(array(
+      '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
+      // The standard PHP error handler considers that the error messages
+      // are HTML. We mimick this behavior here.
+      '!message' => filter_xss_admin($message),
+      '%function' => $caller['function'],
+      '%file' => $caller['file'],
+      '%line' => $caller['line'],
+      'severity_level' => $severity_level,
+    ), $error_level == E_RECOVERABLE_ERROR);
+  }
+}
+
+/**
+ * Decodes an exception and retrieves the correct caller.
+ *
+ * @param $exception
+ *   The exception object that was thrown.
+ *
+ * @return
+ *   An error in the format expected by _drupal_log_error().
+ */
+function _drupal_decode_exception($exception) {
+  $message = $exception->getMessage();
+
+  $backtrace = $exception->getTrace();
+  // Add the line throwing the exception to the backtrace.
+  array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile()));
+
+  // For PDOException errors, we try to return the initial caller,
+  // skipping internal functions of the database layer.
+  if ($exception instanceof PDOException) {
+    // The first element in the stack is the call, the second element gives us the caller.
+    // We skip calls that occurred in one of the classes of the database layer
+    // or in one of its global functions.
+    $db_functions = array('db_query',  'db_query_range');
+    while (!empty($backtrace[1]) && ($caller = $backtrace[1]) &&
+        ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) ||
+        in_array($caller['function'], $db_functions))) {
+      // We remove that call.
+      array_shift($backtrace);
+    }
+    if (isset($exception->query_string, $exception->args)) {
+      $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
+    }
+  }
+  $caller = _drupal_get_last_caller($backtrace);
+
+  return array(
+    '%type' => get_class($exception),
+    // The standard PHP exception handler considers that the exception message
+    // is plain-text. We mimick this behavior here.
+    '!message' => check_plain($message),
+    '%function' => $caller['function'],
+    '%file' => $caller['file'],
+    '%line' => $caller['line'],
+    'severity_level' => WATCHDOG_ERROR,
+  );
+}
+
+/**
+ * Renders an exception error message without further exceptions.
+ *
+ * @param $exception
+ *   The exception object that was thrown.
+ * @return
+ *   An error message.
+ */
+function _drupal_render_exception_safe($exception) {
+  return check_plain(strtr('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception)));
+}
+
+/**
+ * Determines whether an error should be displayed.
+ *
+ * When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL,
+ * all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error
+ * will be examined to determine if it should be displayed.
+ *
+ * @param $error
+ *   Optional error to examine for ERROR_REPORTING_DISPLAY_SOME.
+ *
+ * @return
+ *   TRUE if an error should be displayed.
+ */
+function error_displayable($error = NULL) {
+  $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL);
+  $updating = (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update');
+  $all_errors_displayed = ($error_level == ERROR_REPORTING_DISPLAY_ALL);
+  $error_needs_display = ($error_level == ERROR_REPORTING_DISPLAY_SOME &&
+    isset($error) && $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning');
+
+  return ($updating || $all_errors_displayed || $error_needs_display);
+}
+
+/**
+ * Logs a PHP error or exception and displays an error page in fatal cases.
+ *
+ * @param $error
+ *   An array with the following keys: %type, !message, %function, %file, %line
+ *   and severity_level. All the parameters are plain-text, with the exception
+ *   of !message, which needs to be a safe HTML string.
+ * @param $fatal
+ *   TRUE if the error is fatal.
+ */
+function _drupal_log_error($error, $fatal = FALSE) {
+  // Initialize a maintenance theme if the boostrap was not complete.
+  // Do it early because drupal_set_message() triggers a drupal_theme_initialize().
+  if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) {
+    unset($GLOBALS['theme']);
+    if (!defined('MAINTENANCE_MODE')) {
+      define('MAINTENANCE_MODE', 'error');
+    }
+    drupal_maintenance_theme();
+  }
+
+  // When running inside the testing framework, we relay the errors
+  // to the tested site by the way of HTTP headers.
+  $test_info = &$GLOBALS['drupal_test_info'];
+  if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+    // $number does not use drupal_static as it should not be reset
+    // as it uniquely identifies each PHP error.
+    static $number = 0;
+    $assertion = array(
+      $error['!message'],
+      $error['%type'],
+      array(
+        'function' => $error['%function'],
+        'file' => $error['%file'],
+        'line' => $error['%line'],
+      ),
+    );
+    header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
+    $number++;
+  }
+
+  watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
+
+  if ($fatal) {
+    drupal_add_http_header('Status', '500 Service unavailable (with message)');
+  }
+
+  if (drupal_is_cli()) {
+    if ($fatal) {
+      // When called from CLI, simply output a plain text message.
+      print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n";
+      exit;
+    }
+  }
+
+  if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
+    if ($fatal) {
+      if (error_displayable($error)) {
+        // When called from JavaScript, simply output the error message.
+        print t('%type: !message in %function (line %line of %file).', $error);
+      }
+      exit;
+    }
+  }
+  else {
+    // Display the message if the current error reporting level allows this type
+    // of message to be displayed, and unconditionnaly in update.php.
+    if (error_displayable($error)) {
+      $class = 'error';
+
+      // If error type is 'User notice' then treat it as debug information
+      // instead of an error message, see dd().
+      if ($error['%type'] == 'User notice') {
+        $error['%type'] = 'Debug';
+        $class = 'status';
+      }
+
+      drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class);
+    }
+
+    if ($fatal) {
+      drupal_set_title(t('Error'));
+      // We fallback to a maintenance page at this point, because the page generation
+      // itself can generate errors.
+      print theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.')));
+      exit;
+    }
+  }
+}
+
+/**
+ * Gets the last caller from a backtrace.
+ *
+ * @param $backtrace
+ *   A standard PHP backtrace.
+ *
+ * @return
+ *   An associative array with keys 'file', 'line' and 'function'.
+ */
+function _drupal_get_last_caller($backtrace) {
+  // Errors that occur inside PHP internal functions do not generate
+  // information about file and line. Ignore black listed functions.
+  $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
+  while (($backtrace && !isset($backtrace[0]['line'])) ||
+         (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) {
+    array_shift($backtrace);
+  }
+
+  // The first trace is the call itself.
+  // It gives us the line and the file of the last call.
+  $call = $backtrace[0];
+
+  // The second call give us the function where the call originated.
+  if (isset($backtrace[1])) {
+    if (isset($backtrace[1]['class'])) {
+      $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
+    }
+    else {
+      $call['function'] = $backtrace[1]['function'] . '()';
+    }
+  }
+  else {
+    $call['function'] = 'main()';
+  }
+  return $call;
+}

+ 2489 - 0
includes/file.inc

@@ -0,0 +1,2489 @@
+<?php
+
+/**
+ * @file
+ * API for handling file uploads and server file management.
+ */
+
+/**
+ * Manually include stream wrapper code.
+ *
+ * Stream wrapper code is included here because there are cases where
+ * File API is needed before a bootstrap, or in an alternate order (e.g.
+ * maintenance theme).
+ */
+require_once DRUPAL_ROOT . '/includes/stream_wrappers.inc';
+
+/**
+ * @defgroup file File interface
+ * @{
+ * Common file handling functions.
+ *
+ * Fields on the file object:
+ * - fid: File ID
+ * - uid: The {users}.uid of the user who is associated with the file.
+ * - filename: Name of the file with no path components. This may differ from
+ *   the basename of the filepath if the file is renamed to avoid overwriting
+ *   an existing file.
+ * - uri: URI of the file.
+ * - filemime: The file's MIME type.
+ * - filesize: The size of the file in bytes.
+ * - status: A bitmapped field indicating the status of the file. The first 8
+ *   bits are reserved for Drupal core. The least significant bit indicates
+ *   temporary (0) or permanent (1). Temporary files older than
+ *   DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
+ * - timestamp: UNIX timestamp for the date the file was added to the database.
+ */
+
+/**
+ * Flag used by file_prepare_directory() -- create directory if not present.
+ */
+define('FILE_CREATE_DIRECTORY', 1);
+
+/**
+ * Flag used by file_prepare_directory() -- file permissions may be changed.
+ */
+define('FILE_MODIFY_PERMISSIONS', 2);
+
+/**
+ * Flag for dealing with existing files: Appends number until name is unique.
+ */
+define('FILE_EXISTS_RENAME', 0);
+
+/**
+ * Flag for dealing with existing files: Replace the existing file.
+ */
+define('FILE_EXISTS_REPLACE', 1);
+
+/**
+ * Flag for dealing with existing files: Do nothing and return FALSE.
+ */
+define('FILE_EXISTS_ERROR', 2);
+
+/**
+ * Indicates that the file is permanent and should not be deleted.
+ *
+ * Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed
+ * during cron runs, but permanent files will not be removed during the file
+ * garbage collection process.
+ */
+define('FILE_STATUS_PERMANENT', 1);
+
+/**
+ * Provides Drupal stream wrapper registry.
+ *
+ * A stream wrapper is an abstraction of a file system that allows Drupal to
+ * use the same set of methods to access both local files and remote resources.
+ *
+ * Provide a facility for managing and querying user-defined stream wrappers
+ * in PHP. PHP's internal stream_get_wrappers() doesn't return the class
+ * registered to handle a stream, which we need to be able to find the handler
+ * for class instantiation.
+ *
+ * If a module registers a scheme that is already registered with PHP, the
+ * existing scheme will be unregistered and replaced with the specified class.
+ *
+ * A stream is referenced as "scheme://target".
+ *
+ * The optional $filter parameter can be used to retrieve only the stream
+ * wrappers that are appropriate for particular usage. For example, this returns
+ * only stream wrappers that use local file storage:
+ * @code
+ *   $local_stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
+ * @endcode
+ *
+ * The $filter parameter can only filter to types containing a particular flag.
+ * In some cases, you may want to filter to types that do not contain a
+ * particular flag. For example, you may want to retrieve all stream wrappers
+ * that are not writable, or all stream wrappers that are not local. PHP's
+ * array_diff_key() function can be used to help with this. For example, this
+ * returns only stream wrappers that do not use local file storage:
+ * @code
+ *   $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
+ * @endcode
+ *
+ * @param $filter
+ *   (Optional) Filters out all types except those with an on bit for each on
+ *   bit in $filter. For example, if $filter is STREAM_WRAPPERS_WRITE_VISIBLE,
+ *   which is equal to (STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE |
+ *   STREAM_WRAPPERS_VISIBLE), then only stream wrappers with all three of these
+ *   bits set are returned. Defaults to STREAM_WRAPPERS_ALL, which returns all
+ *   registered stream wrappers.
+ *
+ * @return
+ *   An array keyed by scheme, with values containing an array of information
+ *   about the stream wrapper, as returned by hook_stream_wrappers(). If $filter
+ *   is omitted or set to STREAM_WRAPPERS_ALL, the entire Drupal stream wrapper
+ *   registry is returned. Otherwise only the stream wrappers whose 'type'
+ *   bitmask has an on bit for each bit specified in $filter are returned.
+ *
+ * @see hook_stream_wrappers()
+ * @see hook_stream_wrappers_alter()
+ */
+function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
+  $wrappers_storage = &drupal_static(__FUNCTION__);
+
+  if (!isset($wrappers_storage)) {
+    $wrappers = module_invoke_all('stream_wrappers');
+    foreach ($wrappers as $scheme => $info) {
+      // Add defaults.
+      $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
+    }
+    drupal_alter('stream_wrappers', $wrappers);
+    $existing = stream_get_wrappers();
+    foreach ($wrappers as $scheme => $info) {
+      // We only register classes that implement our interface.
+      if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) {
+        // Record whether we are overriding an existing scheme.
+        if (in_array($scheme, $existing, TRUE)) {
+          $wrappers[$scheme]['override'] = TRUE;
+          stream_wrapper_unregister($scheme);
+        }
+        else {
+          $wrappers[$scheme]['override'] = FALSE;
+        }
+        if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
+          stream_wrapper_register($scheme, $info['class']);
+        }
+        else {
+          stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
+        }
+      }
+      // Pre-populate the static cache with the filters most typically used.
+      $wrappers_storage[STREAM_WRAPPERS_ALL][$scheme] = $wrappers[$scheme];
+      if (($info['type'] & STREAM_WRAPPERS_WRITE_VISIBLE) == STREAM_WRAPPERS_WRITE_VISIBLE) {
+        $wrappers_storage[STREAM_WRAPPERS_WRITE_VISIBLE][$scheme] = $wrappers[$scheme];
+      }
+    }
+  }
+
+  if (!isset($wrappers_storage[$filter])) {
+    $wrappers_storage[$filter] = array();
+    foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
+      // Bit-wise filter.
+      if (($info['type'] & $filter) == $filter) {
+        $wrappers_storage[$filter][$scheme] = $info;
+      }
+    }
+  }
+
+  return $wrappers_storage[$filter];
+}
+
+/**
+ * Returns the stream wrapper class name for a given scheme.
+ *
+ * @param $scheme
+ *   Stream scheme.
+ *
+ * @return
+ *   Return string if a scheme has a registered handler, or FALSE.
+ */
+function file_stream_wrapper_get_class($scheme) {
+  $wrappers = file_get_stream_wrappers();
+  return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
+}
+
+/**
+ * Returns the scheme of a URI (e.g. a stream).
+ *
+ * @param $uri
+ *   A stream, referenced as "scheme://target".
+ *
+ * @return
+ *   A string containing the name of the scheme, or FALSE if none. For example,
+ *   the URI "public://example.txt" would return "public".
+ *
+ * @see file_uri_target()
+ */
+function file_uri_scheme($uri) {
+  $position = strpos($uri, '://');
+  return $position ? substr($uri, 0, $position) : FALSE;
+}
+
+/**
+ * Checks that the scheme of a stream URI is valid.
+ *
+ * Confirms that there is a registered stream handler for the provided scheme
+ * and that it is callable. This is useful if you want to confirm a valid
+ * scheme without creating a new instance of the registered handler.
+ *
+ * @param $scheme
+ *   A URI scheme, a stream is referenced as "scheme://target".
+ *
+ * @return
+ *   Returns TRUE if the string is the name of a validated stream,
+ *   or FALSE if the scheme does not have a registered handler.
+ */
+function file_stream_wrapper_valid_scheme($scheme) {
+  // Does the scheme have a registered handler that is callable?
+  $class = file_stream_wrapper_get_class($scheme);
+  if (class_exists($class)) {
+    return TRUE;
+  }
+  else {
+    return FALSE;
+  }
+}
+
+
+/**
+ * Returns the part of a URI after the schema.
+ *
+ * @param $uri
+ *   A stream, referenced as "scheme://target".
+ *
+ * @return
+ *   A string containing the target (path), or FALSE if none.
+ *   For example, the URI "public://sample/test.txt" would return
+ *   "sample/test.txt".
+ *
+ * @see file_uri_scheme()
+ */
+function file_uri_target($uri) {
+  $data = explode('://', $uri, 2);
+
+  // Remove erroneous leading or trailing, forward-slashes and backslashes.
+  return count($data) == 2 ? trim($data[1], '\/') : FALSE;
+}
+
+/**
+ * Gets the default file stream implementation.
+ *
+ * @return
+ *   'public', 'private' or any other file scheme defined as the default.
+ */
+function file_default_scheme() {
+  return variable_get('file_default_scheme', 'public');
+}
+
+/**
+ * Normalizes a URI by making it syntactically correct.
+ *
+ * A stream is referenced as "scheme://target".
+ *
+ * The following actions are taken:
+ * - Remove trailing slashes from target
+ * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
+ *
+ * @param $uri
+ *   String reference containing the URI to normalize.
+ *
+ * @return
+ *   The normalized URI.
+ */
+function file_stream_wrapper_uri_normalize($uri) {
+  $scheme = file_uri_scheme($uri);
+
+  if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
+    $target = file_uri_target($uri);
+
+    if ($target !== FALSE) {
+      $uri = $scheme . '://' . $target;
+    }
+  }
+  return $uri;
+}
+
+/**
+ * Returns a reference to the stream wrapper class responsible for a given URI.
+ *
+ * The scheme determines the stream wrapper class that should be
+ * used by consulting the stream wrapper registry.
+ *
+ * @param $uri
+ *   A stream, referenced as "scheme://target".
+ *
+ * @return
+ *   Returns a new stream wrapper object appropriate for the given URI or FALSE
+ *   if no registered handler could be found. For example, a URI of
+ *   "private://example.txt" would return a new private stream wrapper object
+ *   (DrupalPrivateStreamWrapper).
+ */
+function file_stream_wrapper_get_instance_by_uri($uri) {
+  $scheme = file_uri_scheme($uri);
+  $class = file_stream_wrapper_get_class($scheme);
+  if (class_exists($class)) {
+    $instance = new $class();
+    $instance->setUri($uri);
+    return $instance;
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Returns a reference to the stream wrapper class responsible for a scheme.
+ *
+ * This helper method returns a stream instance using a scheme. That is, the
+ * passed string does not contain a "://". For example, "public" is a scheme
+ * but "public://" is a URI (stream). This is because the later contains both
+ * a scheme and target despite target being empty.
+ *
+ * Note: the instance URI will be initialized to "scheme://" so that you can
+ * make the customary method calls as if you had retrieved an instance by URI.
+ *
+ * @param $scheme
+ *   If the stream was "public://target", "public" would be the scheme.
+ *
+ * @return
+ *   Returns a new stream wrapper object appropriate for the given $scheme.
+ *   For example, for the public scheme a stream wrapper object
+ *   (DrupalPublicStreamWrapper).
+ *   FALSE is returned if no registered handler could be found.
+ */
+function file_stream_wrapper_get_instance_by_scheme($scheme) {
+  $class = file_stream_wrapper_get_class($scheme);
+  if (class_exists($class)) {
+    $instance = new $class();
+    $instance->setUri($scheme . '://');
+    return $instance;
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Creates a web-accessible URL for a stream to an external or local file.
+ *
+ * Compatibility: normal paths and stream wrappers.
+ *
+ * There are two kinds of local files:
+ * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
+ *   These are files that have either been uploaded by users or were generated
+ *   automatically (for example through CSS aggregation).
+ * - "shipped files", i.e. those outside of the files directory, which ship as
+ *   part of Drupal core or contributed modules or themes.
+ *
+ * @param $uri
+ *   The URI to a file for which we need an external URL, or the path to a
+ *   shipped file.
+ *
+ * @return
+ *   A string containing a URL that may be used to access the file.
+ *   If the provided string already contains a preceding 'http', 'https', or
+ *   '/', nothing is done and the same string is returned. If a stream wrapper
+ *   could not be found to generate an external URL, then FALSE is returned.
+ *
+ * @see http://drupal.org/node/515192
+ */
+function file_create_url($uri) {
+  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
+  // file server.
+  drupal_alter('file_url', $uri);
+
+  $scheme = file_uri_scheme($uri);
+
+  if (!$scheme) {
+    // Allow for:
+    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
+    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
+    //   http://example.com/bar.jpg by the browser when viewing a page over
+    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
+    // Both types of relative URIs are characterized by a leading slash, hence
+    // we can use a single check.
+    if (drupal_substr($uri, 0, 1) == '/') {
+      return $uri;
+    }
+    else {
+      // If this is not a properly formatted stream, then it is a shipped file.
+      // Therefore, return the urlencoded URI with the base URL prepended.
+      return $GLOBALS['base_url'] . '/' . drupal_encode_path($uri);
+    }
+  }
+  elseif ($scheme == 'http' || $scheme == 'https') {
+    // Check for HTTP so that we don't have to implement getExternalUrl() for
+    // the HTTP wrapper.
+    return $uri;
+  }
+  else {
+    // Attempt to return an external URL using the appropriate wrapper.
+    if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
+      return $wrapper->getExternalUrl();
+    }
+    else {
+      return FALSE;
+    }
+  }
+}
+
+/**
+ * Checks that the directory exists and is writable.
+ *
+ * Directories need to have execute permissions to be considered a directory by
+ * FTP servers, etc.
+ *
+ * @param $directory
+ *   A string reference containing the name of a directory path or URI. A
+ *   trailing slash will be trimmed from a path.
+ * @param $options
+ *   A bitmask to indicate if the directory should be created if it does
+ *   not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
+ *   (FILE_MODIFY_PERMISSIONS).
+ *
+ * @return
+ *   TRUE if the directory exists (or was created) and is writable. FALSE
+ *   otherwise.
+ */
+function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
+  if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) {
+    // Only trim if we're not dealing with a stream.
+    $directory = rtrim($directory, '/\\');
+  }
+
+  // Check if directory exists.
+  if (!is_dir($directory)) {
+    // Let mkdir() recursively create directories and use the default directory
+    // permissions.
+    if (($options & FILE_CREATE_DIRECTORY) && @drupal_mkdir($directory, NULL, TRUE)) {
+      return drupal_chmod($directory);
+    }
+    return FALSE;
+  }
+  // The directory exists, so check to see if it is writable.
+  $writable = is_writable($directory);
+  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
+    return drupal_chmod($directory);
+  }
+
+  return $writable;
+}
+
+/**
+ * Creates a .htaccess file in each Drupal files directory if it is missing.
+ */
+function file_ensure_htaccess() {
+  file_create_htaccess('public://', FALSE);
+  if (variable_get('file_private_path', FALSE)) {
+    file_create_htaccess('private://', TRUE);
+  }
+  file_create_htaccess('temporary://', TRUE);
+}
+
+/**
+ * Creates a .htaccess file in the given directory.
+ *
+ * @param $directory
+ *   The directory.
+ * @param $private
+ *   FALSE indicates that $directory should be an open and public directory.
+ *   The default is TRUE which indicates a private and protected directory.
+ */
+function file_create_htaccess($directory, $private = TRUE) {
+  if (file_uri_scheme($directory)) {
+    $directory = file_stream_wrapper_uri_normalize($directory);
+  }
+  else {
+    $directory = rtrim($directory, '/\\');
+  }
+  $htaccess_path =  $directory . '/.htaccess';
+
+  if (file_exists($htaccess_path)) {
+    // Short circuit if the .htaccess file already exists.
+    return;
+  }
+
+  if ($private) {
+    // Private .htaccess file.
+    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks";
+  }
+  else {
+    // Public .htaccess file.
+    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
+  }
+
+  // Write the .htaccess file.
+  if (file_put_contents($htaccess_path, $htaccess_lines)) {
+    drupal_chmod($htaccess_path, 0444);
+  }
+  else {
+    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)));
+    watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables, WATCHDOG_ERROR);
+  }
+}
+
+/**
+ * Loads file objects from the database.
+ *
+ * @param $fids
+ *   An array of file IDs.
+ * @param $conditions
+ *   (deprecated) An associative array of conditions on the {file_managed}
+ *   table, where the keys are the database fields and the values are the
+ *   values those fields must have. Instead, it is preferable to use
+ *   EntityFieldQuery to retrieve a list of entity IDs loadable by
+ *   this function.
+ *
+ * @return
+ *   An array of file objects, indexed by fid.
+ *
+ * @todo Remove $conditions in Drupal 8.
+ *
+ * @see hook_file_load()
+ * @see file_load()
+ * @see entity_load()
+ * @see EntityFieldQuery
+ */
+function file_load_multiple($fids = array(), $conditions = array()) {
+  return entity_load('file', $fids, $conditions);
+}
+
+/**
+ * Loads a single file object from the database.
+ *
+ * @param $fid
+ *   A file ID.
+ *
+ * @return
+ *   An object representing the file, or FALSE if the file was not found.
+ *
+ * @see hook_file_load()
+ * @see file_load_multiple()
+ */
+function file_load($fid) {
+  $files = file_load_multiple(array($fid), array());
+  return reset($files);
+}
+
+/**
+ * Saves a file object to the database.
+ *
+ * If the $file->fid is not set a new record will be added.
+ *
+ * @param $file
+ *   A file object returned by file_load().
+ *
+ * @return
+ *   The updated file object.
+ *
+ * @see hook_file_insert()
+ * @see hook_file_update()
+ */
+function file_save(stdClass $file) {
+  $file->timestamp = REQUEST_TIME;
+  $file->filesize = filesize($file->uri);
+
+  // Load the stored entity, if any.
+  if (!empty($file->fid) && !isset($file->original)) {
+    $file->original = entity_load_unchanged('file', $file->fid);
+  }
+
+  module_invoke_all('file_presave', $file);
+  module_invoke_all('entity_presave', $file, 'file');
+
+  if (empty($file->fid)) {
+    drupal_write_record('file_managed', $file);
+    // Inform modules about the newly added file.
+    module_invoke_all('file_insert', $file);
+    module_invoke_all('entity_insert', $file, 'file');
+  }
+  else {
+    drupal_write_record('file_managed', $file, 'fid');
+    // Inform modules that the file has been updated.
+    module_invoke_all('file_update', $file);
+    module_invoke_all('entity_update', $file, 'file');
+  }
+
+  unset($file->original);
+  return $file;
+}
+
+/**
+ * Determines where a file is used.
+ *
+ * @param $file
+ *   A file object.
+ *
+ * @return
+ *   A nested array with usage data. The first level is keyed by module name,
+ *   the second by object type and the third by the object id. The value
+ *   of the third level contains the usage count.
+ *
+ * @see file_usage_add()
+ * @see file_usage_delete()
+ */
+function file_usage_list(stdClass $file) {
+  $result = db_select('file_usage', 'f')
+    ->fields('f', array('module', 'type', 'id', 'count'))
+    ->condition('fid', $file->fid)
+    ->condition('count', 0, '>')
+    ->execute();
+  $references = array();
+  foreach ($result as $usage) {
+    $references[$usage->module][$usage->type][$usage->id] = $usage->count;
+  }
+  return $references;
+}
+
+/**
+ * Records that a module is using a file.
+ *
+ * This usage information will be queried during file_delete() to ensure that
+ * a file is not in use before it is physically removed from disk.
+ *
+ * Examples:
+ * - A module that associates files with nodes, so $type would be
+ *   'node' and $id would be the node's nid. Files for all revisions are stored
+ *   within a single nid.
+ * - The User module associates an image with a user, so $type would be 'user'
+ *   and the $id would be the user's uid.
+ *
+ * @param $file
+ *   A file object.
+ * @param $module
+ *   The name of the module using the file.
+ * @param $type
+ *   The type of the object that contains the referenced file.
+ * @param $id
+ *   The unique, numeric ID of the object containing the referenced file.
+ * @param $count
+ *   (optional) The number of references to add to the object. Defaults to 1.
+ *
+ * @see file_usage_list()
+ * @see file_usage_delete()
+ */
+function file_usage_add(stdClass $file, $module, $type, $id, $count = 1) {
+  db_merge('file_usage')
+    ->key(array(
+      'fid' => $file->fid,
+      'module' => $module,
+      'type' => $type,
+      'id' => $id,
+    ))
+    ->fields(array('count' => $count))
+    ->expression('count', 'count + :count', array(':count' => $count))
+    ->execute();
+}
+
+/**
+ * Removes a record to indicate that a module is no longer using a file.
+ *
+ * The file_delete() function is typically called after removing a file usage
+ * to remove the record from the file_managed table and delete the file itself.
+ *
+ * @param $file
+ *   A file object.
+ * @param $module
+ *   The name of the module using the file.
+ * @param $type
+ *   (optional) The type of the object that contains the referenced file. May
+ *   be omitted if all module references to a file are being deleted.
+ * @param $id
+ *   (optional) The unique, numeric ID of the object containing the referenced
+ *   file. May be omitted if all module references to a file are being deleted.
+ * @param $count
+ *   (optional) The number of references to delete from the object. Defaults to
+ *   1. 0 may be specified to delete all references to the file within a
+ *   specific object.
+ *
+ * @see file_usage_add()
+ * @see file_usage_list()
+ * @see file_delete()
+ */
+function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $count = 1) {
+  // Delete rows that have a exact or less value to prevent empty rows.
+  $query = db_delete('file_usage')
+    ->condition('module', $module)
+    ->condition('fid', $file->fid);
+  if ($type && $id) {
+    $query
+      ->condition('type', $type)
+      ->condition('id', $id);
+  }
+  if ($count) {
+    $query->condition('count', $count, '<=');
+  }
+  $result = $query->execute();
+
+  // If the row has more than the specified count decrement it by that number.
+  if (!$result && $count > 0) {
+    $query = db_update('file_usage')
+      ->condition('module', $module)
+      ->condition('fid', $file->fid);
+    if ($type && $id) {
+      $query
+        ->condition('type', $type)
+        ->condition('id', $id);
+    }
+    $query->expression('count', 'count - :count', array(':count' => $count));
+    $query->execute();
+  }
+}
+
+/**
+ * Copies a file to a new location and adds a file record to the database.
+ *
+ * This function should be used when manipulating files that have records
+ * stored in the database. This is a powerful function that in many ways
+ * performs like an advanced version of copy().
+ * - Checks if $source and $destination are valid and readable/writable.
+ * - Checks that $source is not equal to $destination; if they are an error
+ *   is reported.
+ * - If file already exists in $destination either the call will error out,
+ *   replace the file or rename the file based on the $replace parameter.
+ * - Adds the new file to the files database. If the source file is a
+ *   temporary file, the resulting file will also be a temporary file. See
+ *   file_save_upload() for details on temporary files.
+ *
+ * @param $source
+ *   A file object.
+ * @param $destination
+ *   A string containing the destination that $source should be copied to.
+ *   This must be a stream wrapper URI.
+ * @param $replace
+ *   Replace behavior when the destination file already exists:
+ *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
+ *       the destination name exists then its database entry will be updated. If
+ *       no database entry is found then a new one will be created.
+ *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ *       unique.
+ *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ *   File object if the copy is successful, or FALSE in the event of an error.
+ *
+ * @see file_unmanaged_copy()
+ * @see hook_file_copy()
+ */
+function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
+  if (!file_valid_uri($destination)) {
+    if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
+      watchdog('file', 'File %file (%realpath) could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
+    }
+    else {
+      watchdog('file', 'File %file could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
+    }
+    drupal_set_message(t('The specified file %file could not be copied, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
+    return FALSE;
+  }
+
+  if ($uri = file_unmanaged_copy($source->uri, $destination, $replace)) {
+    $file = clone $source;
+    $file->fid = NULL;
+    $file->uri = $uri;
+    $file->filename = drupal_basename($uri);
+    // If we are replacing an existing file re-use its database record.
+    if ($replace == FILE_EXISTS_REPLACE) {
+      $existing_files = file_load_multiple(array(), array('uri' => $uri));
+      if (count($existing_files)) {
+        $existing = reset($existing_files);
+        $file->fid = $existing->fid;
+        $file->filename = $existing->filename;
+      }
+    }
+    // If we are renaming around an existing file (rather than a directory),
+    // use its basename for the filename.
+    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
+      $file->filename = drupal_basename($destination);
+    }
+
+    $file = file_save($file);
+
+    // Inform modules that the file has been copied.
+    module_invoke_all('file_copy', $file, $source);
+
+    return $file;
+  }
+  return FALSE;
+}
+
+/**
+ * Determines whether the URI has a valid scheme for file API operations.
+ *
+ * There must be a scheme and it must be a Drupal-provided scheme like
+ * 'public', 'private', 'temporary', or an extension provided with
+ * hook_stream_wrappers().
+ *
+ * @param $uri
+ *   The URI to be tested.
+ *
+ * @return
+ *   TRUE if the URI is allowed.
+ */
+function file_valid_uri($uri) {
+  // Assert that the URI has an allowed scheme. Barepaths are not allowed.
+  $uri_scheme = file_uri_scheme($uri);
+  if (empty($uri_scheme) || !file_stream_wrapper_valid_scheme($uri_scheme)) {
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Copies a file to a new location without invoking the file API.
+ *
+ * This is a powerful function that in many ways performs like an advanced
+ * version of copy().
+ * - Checks if $source and $destination are valid and readable/writable.
+ * - Checks that $source is not equal to $destination; if they are an error
+ *   is reported.
+ * - If file already exists in $destination either the call will error out,
+ *   replace the file or rename the file based on the $replace parameter.
+ * - Provides a fallback using realpaths if the move fails using stream
+ *   wrappers. This can occur because PHP's copy() function does not properly
+ *   support streams if safe_mode or open_basedir are enabled. See
+ *   https://bugs.php.net/bug.php?id=60456
+ *
+ * @param $source
+ *   A string specifying the filepath or URI of the source file.
+ * @param $destination
+ *   A URI containing the destination that $source should be copied to. The
+ *   URI may be a bare filepath (without a scheme). If this value is omitted,
+ *   Drupal's default files scheme will be used, usually "public://".
+ * @param $replace
+ *   Replace behavior when the destination file already exists:
+ *   - FILE_EXISTS_REPLACE - Replace the existing file.
+ *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ *       unique.
+ *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ *   The path to the new file, or FALSE in the event of an error.
+ *
+ * @see file_copy()
+ */
+function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
+  $original_source = $source;
+  $original_destination = $destination;
+
+  // Assert that the source file actually exists.
+  if (!file_exists($source)) {
+    // @todo Replace drupal_set_message() calls with exceptions instead.
+    drupal_set_message(t('The specified file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
+    if (($realpath = drupal_realpath($original_source)) !== FALSE) {
+      watchdog('file', 'File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
+    }
+    else {
+      watchdog('file', 'File %file could not be copied because it does not exist.', array('%file' => $original_source));
+    }
+    return FALSE;
+  }
+
+  // Build a destination URI if necessary.
+  if (!isset($destination)) {
+    $destination = file_build_uri(drupal_basename($source));
+  }
+
+
+  // Prepare the destination directory.
+  if (file_prepare_directory($destination)) {
+    // The destination is already a directory, so append the source basename.
+    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
+  }
+  else {
+    // Perhaps $destination is a dir/file?
+    $dirname = drupal_dirname($destination);
+    if (!file_prepare_directory($dirname)) {
+      // The destination is not valid.
+      watchdog('file', 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
+      drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
+      return FALSE;
+    }
+  }
+
+  // Determine whether we can perform this operation based on overwrite rules.
+  $destination = file_destination($destination, $replace);
+  if ($destination === FALSE) {
+    drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
+    watchdog('file', 'File %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $original_source, '%directory' => $destination));
+    return FALSE;
+  }
+
+  // Assert that the source and destination filenames are not the same.
+  $real_source = drupal_realpath($source);
+  $real_destination = drupal_realpath($destination);
+  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
+    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
+    watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
+    return FALSE;
+  }
+  // Make sure the .htaccess files are present.
+  file_ensure_htaccess();
+  // Perform the copy operation.
+  if (!@copy($source, $destination)) {
+    // If the copy failed and realpaths exist, retry the operation using them
+    // instead.
+    if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
+      watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination), WATCHDOG_ERROR);
+      return FALSE;
+    }
+  }
+
+  // Set the permissions on the new file.
+  drupal_chmod($destination);
+
+  return $destination;
+}
+
+/**
+ * Constructs a URI to Drupal's default files location given a relative path.
+ */
+function file_build_uri($path) {
+  $uri = file_default_scheme() . '://' . $path;
+  return file_stream_wrapper_uri_normalize($uri);
+}
+
+/**
+ * Determines the destination path for a file.
+ *
+ * @param $destination
+ *   A string specifying the desired final URI or filepath.
+ * @param $replace
+ *   Replace behavior when the destination file already exists.
+ *   - FILE_EXISTS_REPLACE - Replace the existing file.
+ *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ *       unique.
+ *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ *   The destination filepath, or FALSE if the file already exists
+ *   and FILE_EXISTS_ERROR is specified.
+ */
+function file_destination($destination, $replace) {
+  if (file_exists($destination)) {
+    switch ($replace) {
+      case FILE_EXISTS_REPLACE:
+        // Do nothing here, we want to overwrite the existing file.
+        break;
+
+      case FILE_EXISTS_RENAME:
+        $basename = drupal_basename($destination);
+        $directory = drupal_dirname($destination);
+        $destination = file_create_filename($basename, $directory);
+        break;
+
+      case FILE_EXISTS_ERROR:
+        // Error reporting handled by calling function.
+        return FALSE;
+    }
+  }
+  return $destination;
+}
+
+/**
+ * Moves a file to a new location and update the file's database entry.
+ *
+ * Moving a file is performed by copying the file to the new location and then
+ * deleting the original.
+ * - Checks if $source and $destination are valid and readable/writable.
+ * - Performs a file move if $source is not equal to $destination.
+ * - If file already exists in $destination either the call will error out,
+ *   replace the file or rename the file based on the $replace parameter.
+ * - Adds the new file to the files database.
+ *
+ * @param $source
+ *   A file object.
+ * @param $destination
+ *   A string containing the destination that $source should be moved to.
+ *   This must be a stream wrapper URI.
+ * @param $replace
+ *   Replace behavior when the destination file already exists:
+ *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
+ *       the destination name exists then its database entry will be updated and
+ *       file_delete() called on the source file after hook_file_move is called.
+ *       If no database entry is found then the source files record will be
+ *       updated.
+ *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ *       unique.
+ *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ *   Resulting file object for success, or FALSE in the event of an error.
+ *
+ * @see file_unmanaged_move()
+ * @see hook_file_move()
+ */
+function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
+  if (!file_valid_uri($destination)) {
+    if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
+      watchdog('file', 'File %file (%realpath) could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
+    }
+    else {
+      watchdog('file', 'File %file could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
+    }
+    drupal_set_message(t('The specified file %file could not be moved, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
+    return FALSE;
+  }
+
+  if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) {
+    $delete_source = FALSE;
+
+    $file = clone $source;
+    $file->uri = $uri;
+    // If we are replacing an existing file re-use its database record.
+    if ($replace == FILE_EXISTS_REPLACE) {
+      $existing_files = file_load_multiple(array(), array('uri' => $uri));
+      if (count($existing_files)) {
+        $existing = reset($existing_files);
+        $delete_source = TRUE;
+        $file->fid = $existing->fid;
+      }
+    }
+    // If we are renaming around an existing file (rather than a directory),
+    // use its basename for the filename.
+    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
+      $file->filename = drupal_basename($destination);
+    }
+
+    $file = file_save($file);
+
+    // Inform modules that the file has been moved.
+    module_invoke_all('file_move', $file, $source);
+
+    if ($delete_source) {
+      // Try a soft delete to remove original if it's not in use elsewhere.
+      file_delete($source);
+    }
+
+    return $file;
+  }
+  return FALSE;
+}
+
+/**
+ * Moves a file to a new location without database changes or hook invocation.
+ *
+ * @param $source
+ *   A string specifying the filepath or URI of the original file.
+ * @param $destination
+ *   A string containing the destination that $source should be moved to.
+ *   This must be a stream wrapper URI. If this value is omitted, Drupal's
+ *   default files scheme will be used, usually "public://".
+ * @param $replace
+ *   Replace behavior when the destination file already exists:
+ *   - FILE_EXISTS_REPLACE - Replace the existing file.
+ *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ *       unique.
+ *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ *   The URI of the moved file, or FALSE in the event of an error.
+ *
+ * @see file_move()
+ */
+function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
+  $filepath = file_unmanaged_copy($source, $destination, $replace);
+  if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
+    return FALSE;
+  }
+  return $filepath;
+}
+
+/**
+ * Modifies a filename as needed for security purposes.
+ *
+ * Munging a file name prevents unknown file extensions from masking exploit
+ * files. When web servers such as Apache decide how to process a URL request,
+ * they use the file extension. If the extension is not recognized, Apache
+ * skips that extension and uses the previous file extension. For example, if
+ * the file being requested is exploit.php.pps, and Apache does not recognize
+ * the '.pps' extension, it treats the file as PHP and executes it. To make
+ * this file name safe for Apache and prevent it from executing as PHP, the
+ * .php extension is "munged" into .php_, making the safe file name
+ * exploit.php_.pps.
+ *
+ * Specifically, this function adds an underscore to all extensions that are
+ * between 2 and 5 characters in length, internal to the file name, and not
+ * included in $extensions.
+ *
+ * Function behavior is also controlled by the Drupal variable
+ * 'allow_insecure_uploads'. If 'allow_insecure_uploads' evaluates to TRUE, no
+ * alterations will be made, if it evaluates to FALSE, the filename is 'munged'.
+ *
+ * @param $filename
+ *   File name to modify.
+ * @param $extensions
+ *   A space-separated list of extensions that should not be altered.
+ * @param $alerts
+ *   If TRUE, drupal_set_message() will be called to display a message if the
+ *   file name was changed.
+ *
+ * @return
+ *   The potentially modified $filename.
+ */
+function file_munge_filename($filename, $extensions, $alerts = TRUE) {
+  $original = $filename;
+
+  // Allow potentially insecure uploads for very savvy users and admin
+  if (!variable_get('allow_insecure_uploads', 0)) {
+    // Remove any null bytes. See http://php.net/manual/en/security.filesystem.nullbytes.php
+    $filename = str_replace(chr(0), '', $filename);
+
+    $whitelist = array_unique(explode(' ', trim($extensions)));
+
+    // Split the filename up by periods. The first part becomes the basename
+    // the last part the final extension.
+    $filename_parts = explode('.', $filename);
+    $new_filename = array_shift($filename_parts); // Remove file basename.
+    $final_extension = array_pop($filename_parts); // Remove final extension.
+
+    // Loop through the middle parts of the name and add an underscore to the
+    // end of each section that could be a file extension but isn't in the list
+    // of allowed extensions.
+    foreach ($filename_parts as $filename_part) {
+      $new_filename .= '.' . $filename_part;
+      if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
+        $new_filename .= '_';
+      }
+    }
+    $filename = $new_filename . '.' . $final_extension;
+
+    if ($alerts && $original != $filename) {
+      drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename)));
+    }
+  }
+
+  return $filename;
+}
+
+/**
+ * Undoes the effect of file_munge_filename().
+ *
+ * @param $filename
+ *   String with the filename to be unmunged.
+ *
+ * @return
+ *   An unmunged filename string.
+ */
+function file_unmunge_filename($filename) {
+  return str_replace('_.', '.', $filename);
+}
+
+/**
+ * Creates a full file path from a directory and filename.
+ *
+ * If a file with the specified name already exists, an alternative will be
+ * used.
+ *
+ * @param $basename
+ *   String filename
+ * @param $directory
+ *   String containing the directory or parent URI.
+ *
+ * @return
+ *   File path consisting of $directory and a unique filename based off
+ *   of $basename.
+ */
+function file_create_filename($basename, $directory) {
+  // Strip control characters (ASCII value < 32). Though these are allowed in
+  // some filesystems, not many applications handle them well.
+  $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
+  if (substr(PHP_OS, 0, 3) == 'WIN') {
+    // These characters are not allowed in Windows filenames
+    $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
+  }
+
+  // A URI or path may already have a trailing slash or look like "public://".
+  if (substr($directory, -1) == '/') {
+    $separator = '';
+  }
+  else {
+    $separator = '/';
+  }
+
+  $destination = $directory . $separator . $basename;
+
+  if (file_exists($destination)) {
+    // Destination file already exists, generate an alternative.
+    $pos = strrpos($basename, '.');
+    if ($pos !== FALSE) {
+      $name = substr($basename, 0, $pos);
+      $ext = substr($basename, $pos);
+    }
+    else {
+      $name = $basename;
+      $ext = '';
+    }
+
+    $counter = 0;
+    do {
+      $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
+    } while (file_exists($destination));
+  }
+
+  return $destination;
+}
+
+/**
+ * Deletes a file and its database record.
+ *
+ * If the $force parameter is not TRUE, file_usage_list() will be called to
+ * determine if the file is being used by any modules. If the file is being
+ * used the delete will be canceled.
+ *
+ * @param $file
+ *   A file object.
+ * @param $force
+ *   Boolean indicating that the file should be deleted even if the file is
+ *   reported as in use by the file_usage table.
+ *
+ * @return mixed
+ *   TRUE for success, FALSE in the event of an error, or an array if the file
+ *   is being used by any modules.
+ *
+ * @see file_unmanaged_delete()
+ * @see file_usage_list()
+ * @see file_usage_delete()
+ * @see hook_file_delete()
+ */
+function file_delete(stdClass $file, $force = FALSE) {
+  if (!file_valid_uri($file->uri)) {
+    if (($realpath = drupal_realpath($file->uri)) !== FALSE) {
+      watchdog('file', 'File %file (%realpath) could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri, '%realpath' => $realpath));
+    }
+    else {
+      watchdog('file', 'File %file could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri));
+    }
+    drupal_set_message(t('The specified file %file could not be deleted, because it is not a valid URI. More information is available in the system log.', array('%file' => $file->uri)), 'error');
+    return FALSE;
+  }
+
+  // If any module still has a usage entry in the file_usage table, the file
+  // will not be deleted, but file_delete() will return a populated array
+  // that tests as TRUE.
+  if (!$force && ($references = file_usage_list($file))) {
+    return $references;
+  }
+
+  // Let other modules clean up any references to the deleted file.
+  module_invoke_all('file_delete', $file);
+  module_invoke_all('entity_delete', $file, 'file');
+
+  // Make sure the file is deleted before removing its row from the
+  // database, so UIs can still find the file in the database.
+  if (file_unmanaged_delete($file->uri)) {
+    db_delete('file_managed')->condition('fid', $file->fid)->execute();
+    db_delete('file_usage')->condition('fid', $file->fid)->execute();
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * Deletes a file without database changes or hook invocations.
+ *
+ * This function should be used when the file to be deleted does not have an
+ * entry recorded in the files table.
+ *
+ * @param $path
+ *   A string containing a file path or (streamwrapper) URI.
+ *
+ * @return
+ *   TRUE for success or path does not exist, or FALSE in the event of an
+ *   error.
+ *
+ * @see file_delete()
+ * @see file_unmanaged_delete_recursive()
+ */
+function file_unmanaged_delete($path) {
+  if (is_dir($path)) {
+    watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  if (is_file($path)) {
+    return drupal_unlink($path);
+  }
+  // Return TRUE for non-existent file, but log that nothing was actually
+  // deleted, as the current state is the intended result.
+  if (!file_exists($path)) {
+    watchdog('file', 'The file %path was not deleted, because it does not exist.', array('%path' => $path), WATCHDOG_NOTICE);
+    return TRUE;
+  }
+  // We cannot handle anything other than files and directories. Log an error
+  // for everything else (sockets, symbolic links, etc).
+  watchdog('file', 'The file %path is not of a recognized type so it was not deleted.', array('%path' => $path), WATCHDOG_ERROR);
+  return FALSE;
+}
+
+/**
+ * Deletes all files and directories in the specified filepath recursively.
+ *
+ * If the specified path is a directory then the function will call itself
+ * recursively to process the contents. Once the contents have been removed the
+ * directory will also be removed.
+ *
+ * If the specified path is a file then it will be passed to
+ * file_unmanaged_delete().
+ *
+ * Note that this only deletes visible files with write permission.
+ *
+ * @param $path
+ *   A string containing either an URI or a file or directory path.
+ *
+ * @return
+ *   TRUE for success or if path does not exist, FALSE in the event of an
+ *   error.
+ *
+ * @see file_unmanaged_delete()
+ */
+function file_unmanaged_delete_recursive($path) {
+  if (is_dir($path)) {
+    $dir = dir($path);
+    while (($entry = $dir->read()) !== FALSE) {
+      if ($entry == '.' || $entry == '..') {
+        continue;
+      }
+      $entry_path = $path . '/' . $entry;
+      file_unmanaged_delete_recursive($entry_path);
+    }
+    $dir->close();
+
+    return drupal_rmdir($path);
+  }
+  return file_unmanaged_delete($path);
+}
+
+/**
+ * Determines total disk space used by a single user or the whole filesystem.
+ *
+ * @param $uid
+ *   Optional. A user id, specifying NULL returns the total space used by all
+ *   non-temporary files.
+ * @param $status
+ *   Optional. The file status to consider. The default is to only
+ *   consider files in status FILE_STATUS_PERMANENT.
+ *
+ * @return
+ *   An integer containing the number of bytes used.
+ */
+function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
+  $query = db_select('file_managed', 'f');
+  $query->condition('f.status', $status);
+  $query->addExpression('SUM(f.filesize)', 'filesize');
+  if (isset($uid)) {
+    $query->condition('f.uid', $uid);
+  }
+  return $query->execute()->fetchField();
+}
+
+/**
+ * Saves a file upload to a new location.
+ *
+ * The file will be added to the {file_managed} table as a temporary file.
+ * Temporary files are periodically cleaned. To make the file a permanent file,
+ * assign the status and use file_save() to save the changes.
+ *
+ * @param $source
+ *   A string specifying the filepath or URI of the uploaded file to save.
+ * @param $validators
+ *   An optional, associative array of callback functions used to validate the
+ *   file. See file_validate() for a full discussion of the array format.
+ *   If no extension validator is provided it will default to a limited safe
+ *   list of extensions which is as follows: "jpg jpeg gif png txt
+ *   doc xls pdf ppt pps odt ods odp". To allow all extensions you must
+ *   explicitly set the 'file_validate_extensions' validator to an empty array
+ *   (Beware: this is not safe and should only be allowed for trusted users, if
+ *   at all).
+ * @param $destination
+ *   A string containing the URI $source should be copied to.
+ *   This must be a stream wrapper URI. If this value is omitted, Drupal's
+ *   temporary files scheme will be used ("temporary://").
+ * @param $replace
+ *   Replace behavior when the destination file already exists:
+ *   - FILE_EXISTS_REPLACE: Replace the existing file.
+ *   - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is
+ *     unique.
+ *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
+ *
+ * @return
+ *   An object containing the file information if the upload succeeded, FALSE
+ *   in the event of an error, or NULL if no file was uploaded. The
+ *   documentation for the "File interface" group, which you can find under
+ *   Related topics, or the header at the top of this file, documents the
+ *   components of a file object. In addition to the standard components,
+ *   this function adds:
+ *   - source: Path to the file before it is moved.
+ *   - destination: Path to the file after it is moved (same as 'uri').
+ */
+function file_save_upload($source, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
+  global $user;
+  static $upload_cache;
+
+  // Return cached objects without processing since the file will have
+  // already been processed and the paths in _FILES will be invalid.
+  if (isset($upload_cache[$source])) {
+    return $upload_cache[$source];
+  }
+
+  // Make sure there's an upload to process.
+  if (empty($_FILES['files']['name'][$source])) {
+    return NULL;
+  }
+
+  // Check for file upload errors and return FALSE if a lower level system
+  // error occurred. For a complete list of errors:
+  // See http://php.net/manual/en/features.file-upload.errors.php.
+  switch ($_FILES['files']['error'][$source]) {
+    case UPLOAD_ERR_INI_SIZE:
+    case UPLOAD_ERR_FORM_SIZE:
+      drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $_FILES['files']['name'][$source], '%maxsize' => format_size(file_upload_max_size()))), 'error');
+      return FALSE;
+
+    case UPLOAD_ERR_PARTIAL:
+    case UPLOAD_ERR_NO_FILE:
+      drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $_FILES['files']['name'][$source])), 'error');
+      return FALSE;
+
+    case UPLOAD_ERR_OK:
+      // Final check that this is a valid upload, if it isn't, use the
+      // default error handler.
+      if (is_uploaded_file($_FILES['files']['tmp_name'][$source])) {
+        break;
+      }
+
+    // Unknown error
+    default:
+      drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source])), 'error');
+      return FALSE;
+  }
+
+  // Begin building file object.
+  $file = new stdClass();
+  $file->uid      = $user->uid;
+  $file->status   = 0;
+  $file->filename = trim(drupal_basename($_FILES['files']['name'][$source]), '.');
+  $file->uri      = $_FILES['files']['tmp_name'][$source];
+  $file->filemime = file_get_mimetype($file->filename);
+  $file->filesize = $_FILES['files']['size'][$source];
+
+  $extensions = '';
+  if (isset($validators['file_validate_extensions'])) {
+    if (isset($validators['file_validate_extensions'][0])) {
+      // Build the list of non-munged extensions if the caller provided them.
+      $extensions = $validators['file_validate_extensions'][0];
+    }
+    else {
+      // If 'file_validate_extensions' is set and the list is empty then the
+      // caller wants to allow any extension. In this case we have to remove the
+      // validator or else it will reject all extensions.
+      unset($validators['file_validate_extensions']);
+    }
+  }
+  else {
+    // No validator was provided, so add one using the default list.
+    // Build a default non-munged safe list for file_munge_filename().
+    $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
+    $validators['file_validate_extensions'] = array();
+    $validators['file_validate_extensions'][0] = $extensions;
+  }
+
+  if (!empty($extensions)) {
+    // Munge the filename to protect against possible malicious extension hiding
+    // within an unknown file type (ie: filename.html.foo).
+    $file->filename = file_munge_filename($file->filename, $extensions);
+  }
+
+  // Rename potentially executable files, to help prevent exploits (i.e. will
+  // rename filename.php.foo and filename.php to filename.php.foo.txt and
+  // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
+  // evaluates to TRUE.
+  if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
+    $file->filemime = 'text/plain';
+    $file->uri .= '.txt';
+    $file->filename .= '.txt';
+    // The .txt extension may not be in the allowed list of extensions. We have
+    // to add it here or else the file upload will fail.
+    if (!empty($extensions)) {
+      $validators['file_validate_extensions'][0] .= ' txt';
+      drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename)));
+    }
+  }
+
+  // If the destination is not provided, use the temporary directory.
+  if (empty($destination)) {
+    $destination = 'temporary://';
+  }
+
+  // Assert that the destination contains a valid stream.
+  $destination_scheme = file_uri_scheme($destination);
+  if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) {
+    drupal_set_message(t('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)), 'error');
+    return FALSE;
+  }
+
+  $file->source = $source;
+  // A URI may already have a trailing slash or look like "public://".
+  if (substr($destination, -1) != '/') {
+    $destination .= '/';
+  }
+  $file->destination = file_destination($destination . $file->filename, $replace);
+  // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
+  // there's an existing file so we need to bail.
+  if ($file->destination === FALSE) {
+    drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $source, '%directory' => $destination)), 'error');
+    return FALSE;
+  }
+
+  // Add in our check of the the file name length.
+  $validators['file_validate_name_length'] = array();
+
+  // Call the validation functions specified by this function's caller.
+  $errors = file_validate($file, $validators);
+
+  // Check for errors.
+  if (!empty($errors)) {
+    $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
+    if (count($errors) > 1) {
+      $message .= theme('item_list', array('items' => $errors));
+    }
+    else {
+      $message .= ' ' . array_pop($errors);
+    }
+    form_set_error($source, $message);
+    return FALSE;
+  }
+
+  // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
+  // directory. This overcomes open_basedir restrictions for future file
+  // operations.
+  $file->uri = $file->destination;
+  if (!drupal_move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->uri)) {
+    form_set_error($source, t('File upload error. Could not move uploaded file.'));
+    watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
+    return FALSE;
+  }
+
+  // Set the permissions on the new file.
+  drupal_chmod($file->uri);
+
+  // If we are replacing an existing file re-use its database record.
+  if ($replace == FILE_EXISTS_REPLACE) {
+    $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
+    if (count($existing_files)) {
+      $existing = reset($existing_files);
+      $file->fid = $existing->fid;
+    }
+  }
+
+  // If we made it this far it's safe to record this file in the database.
+  if ($file = file_save($file)) {
+    // Add file to the cache.
+    $upload_cache[$source] = $file;
+    return $file;
+  }
+  return FALSE;
+}
+
+/**
+ * Moves an uploaded file to a new location.
+ *
+ * PHP's move_uploaded_file() does not properly support streams if safe_mode
+ * or open_basedir are enabled, so this function fills that gap.
+ *
+ * Compatibility: normal paths and stream wrappers.
+ *
+ * @param $filename
+ *   The filename of the uploaded file.
+ * @param $uri
+ *   A string containing the destination URI of the file.
+ *
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ *
+ * @see move_uploaded_file()
+ * @see http://drupal.org/node/515192
+ * @ingroup php_wrappers
+ */
+function drupal_move_uploaded_file($filename, $uri) {
+  $result = @move_uploaded_file($filename, $uri);
+  // PHP's move_uploaded_file() does not properly support streams if safe_mode
+  // or open_basedir are enabled so if the move failed, try finding a real path
+  // and retry the move operation.
+  if (!$result) {
+    if ($realpath = drupal_realpath($uri)) {
+      $result = move_uploaded_file($filename, $realpath);
+    }
+    else {
+      $result = move_uploaded_file($filename, $uri);
+    }
+  }
+
+  return $result;
+}
+
+/**
+ * Checks that a file meets the criteria specified by the validators.
+ *
+ * After executing the validator callbacks specified hook_file_validate() will
+ * also be called to allow other modules to report errors about the file.
+ *
+ * @param $file
+ *   A Drupal file object.
+ * @param $validators
+ *   An optional, associative array of callback functions used to validate the
+ *   file. The keys are function names and the values arrays of callback
+ *   parameters which will be passed in after the file object. The
+ *   functions should return an array of error messages; an empty array
+ *   indicates that the file passed validation. The functions will be called in
+ *   the order specified.
+ *
+ * @return
+ *   An array containing validation error messages.
+ *
+ * @see hook_file_validate()
+ */
+function file_validate(stdClass &$file, $validators = array()) {
+  // Call the validation functions specified by this function's caller.
+  $errors = array();
+  foreach ($validators as $function => $args) {
+    if (function_exists($function)) {
+      array_unshift($args, $file);
+      $errors = array_merge($errors, call_user_func_array($function, $args));
+    }
+  }
+
+  // Let other modules perform validation on the new file.
+  return array_merge($errors, module_invoke_all('file_validate', $file));
+}
+
+/**
+ * Checks for files with names longer than we can store in the database.
+ *
+ * @param $file
+ *   A Drupal file object.
+ *
+ * @return
+ *   An array. If the file name is too long, it will contain an error message.
+ */
+function file_validate_name_length(stdClass $file) {
+  $errors = array();
+
+  if (empty($file->filename)) {
+    $errors[] = t("The file's name is empty. Please give a name to the file.");
+  }
+  if (strlen($file->filename) > 240) {
+    $errors[] = t("The file's name exceeds the 240 characters limit. Please rename the file and try again.");
+  }
+  return $errors;
+}
+
+/**
+ * Checks that the filename ends with an allowed extension.
+ *
+ * @param $file
+ *   A Drupal file object.
+ * @param $extensions
+ *   A string with a space separated list of allowed extensions.
+ *
+ * @return
+ *   An array. If the file extension is not allowed, it will contain an error
+ *   message.
+ *
+ * @see hook_file_validate()
+ */
+function file_validate_extensions(stdClass $file, $extensions) {
+  $errors = array();
+
+  $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
+  if (!preg_match($regex, $file->filename)) {
+    $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions));
+  }
+  return $errors;
+}
+
+/**
+ * Checks that the file's size is below certain limits.
+ *
+ * This check is not enforced for the user #1.
+ *
+ * @param $file
+ *   A Drupal file object.
+ * @param $file_limit
+ *   An integer specifying the maximum file size in bytes. Zero indicates that
+ *   no limit should be enforced.
+ * @param $user_limit
+ *   An integer specifying the maximum number of bytes the user is allowed.
+ *   Zero indicates that no limit should be enforced.
+ *
+ * @return
+ *   An array. If the file size exceeds limits, it will contain an error
+ *   message.
+ *
+ * @see hook_file_validate()
+ */
+function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) {
+  global $user;
+
+  $errors = array();
+
+  // Bypass validation for uid  = 1.
+  if ($user->uid != 1) {
+    if ($file_limit && $file->filesize > $file_limit) {
+      $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit)));
+    }
+
+    // Save a query by only calling file_space_used() when a limit is provided.
+    if ($user_limit && (file_space_used($user->uid) + $file->filesize) > $user_limit) {
+      $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', array('%filesize' => format_size($file->filesize), '%quota' => format_size($user_limit)));
+    }
+  }
+  return $errors;
+}
+
+/**
+ * Checks that the file is recognized by image_get_info() as an image.
+ *
+ * @param $file
+ *   A Drupal file object.
+ *
+ * @return
+ *   An array. If the file is not an image, it will contain an error message.
+ *
+ * @see hook_file_validate()
+ */
+function file_validate_is_image(stdClass $file) {
+  $errors = array();
+
+  $info = image_get_info($file->uri);
+  if (!$info || empty($info['extension'])) {
+    $errors[] = t('Only JPEG, PNG and GIF images are allowed.');
+  }
+
+  return $errors;
+}
+
+/**
+ * Verifies that image dimensions are within the specified maximum and minimum.
+ *
+ * Non-image files will be ignored. If a image toolkit is available the image
+ * will be scaled to fit within the desired maximum dimensions.
+ *
+ * @param $file
+ *   A Drupal file object. This function may resize the file affecting its
+ *   size.
+ * @param $maximum_dimensions
+ *   An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If
+ *   an image toolkit is installed the image will be resized down to these
+ *   dimensions. A value of 0 indicates no restriction on size, so resizing
+ *   will be attempted.
+ * @param $minimum_dimensions
+ *   An optional string in the form WIDTHxHEIGHT. This will check that the
+ *   image meets a minimum size. A value of 0 indicates no restriction.
+ *
+ * @return
+ *   An array. If the file is an image and did not meet the requirements, it
+ *   will contain an error message.
+ *
+ * @see hook_file_validate()
+ */
+function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
+  $errors = array();
+
+  // Check first that the file is an image.
+  if ($info = image_get_info($file->uri)) {
+    if ($maximum_dimensions) {
+      // Check that it is smaller than the given dimensions.
+      list($width, $height) = explode('x', $maximum_dimensions);
+      if ($info['width'] > $width || $info['height'] > $height) {
+        // Try to resize the image to fit the dimensions.
+        if ($image = image_load($file->uri)) {
+          image_scale($image, $width, $height);
+          image_save($image);
+          $file->filesize = $image->info['file_size'];
+          drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions)));
+        }
+        else {
+          $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions));
+        }
+      }
+    }
+
+    if ($minimum_dimensions) {
+      // Check that it is larger than the given dimensions.
+      list($width, $height) = explode('x', $minimum_dimensions);
+      if ($info['width'] < $width || $info['height'] < $height) {
+        $errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', array('%dimensions' => $minimum_dimensions));
+      }
+    }
+  }
+
+  return $errors;
+}
+
+/**
+ * Saves a file to the specified destination and creates a database entry.
+ *
+ * @param $data
+ *   A string containing the contents of the file.
+ * @param $destination
+ *   A string containing the destination URI. This must be a stream wrapper URI.
+ *   If no value is provided, a randomized name will be generated and the file
+ *   will be saved using Drupal's default files scheme, usually "public://".
+ * @param $replace
+ *   Replace behavior when the destination file already exists:
+ *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
+ *       the destination name exists then its database entry will be updated. If
+ *       no database entry is found then a new one will be created.
+ *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ *       unique.
+ *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ *   A file object, or FALSE on error.
+ *
+ * @see file_unmanaged_save_data()
+ */
+function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
+  global $user;
+
+  if (empty($destination)) {
+    $destination = file_default_scheme() . '://';
+  }
+  if (!file_valid_uri($destination)) {
+    watchdog('file', 'The data could not be saved because the destination %destination is invalid. This may be caused by improper use of file_save_data() or a missing stream wrapper.', array('%destination' => $destination));
+    drupal_set_message(t('The data could not be saved, because the destination is invalid. More information is available in the system log.'), 'error');
+    return FALSE;
+  }
+
+  if ($uri = file_unmanaged_save_data($data, $destination, $replace)) {
+    // Create a file object.
+    $file = new stdClass();
+    $file->fid = NULL;
+    $file->uri = $uri;
+    $file->filename = drupal_basename($uri);
+    $file->filemime = file_get_mimetype($file->uri);
+    $file->uid      = $user->uid;
+    $file->status   = FILE_STATUS_PERMANENT;
+    // If we are replacing an existing file re-use its database record.
+    if ($replace == FILE_EXISTS_REPLACE) {
+      $existing_files = file_load_multiple(array(), array('uri' => $uri));
+      if (count($existing_files)) {
+        $existing = reset($existing_files);
+        $file->fid = $existing->fid;
+        $file->filename = $existing->filename;
+      }
+    }
+    // If we are renaming around an existing file (rather than a directory),
+    // use its basename for the filename.
+    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
+      $file->filename = drupal_basename($destination);
+    }
+
+    return file_save($file);
+  }
+  return FALSE;
+}
+
+/**
+ * Saves a string to the specified destination without invoking file API.
+ *
+ * This function is identical to file_save_data() except the file will not be
+ * saved to the {file_managed} table and none of the file_* hooks will be
+ * called.
+ *
+ * @param $data
+ *   A string containing the contents of the file.
+ * @param $destination
+ *   A string containing the destination location. This must be a stream wrapper
+ *   URI. If no value is provided, a randomized name will be generated and the
+ *   file will be saved using Drupal's default files scheme, usually
+ *   "public://".
+ * @param $replace
+ *   Replace behavior when the destination file already exists:
+ *   - FILE_EXISTS_REPLACE - Replace the existing file.
+ *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ *                          unique.
+ *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ *   A string with the path of the resulting file, or FALSE on error.
+ *
+ * @see file_save_data()
+ */
+function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
+  // Write the data to a temporary file.
+  $temp_name = drupal_tempnam('temporary://', 'file');
+  if (file_put_contents($temp_name, $data) === FALSE) {
+    drupal_set_message(t('The file could not be created.'), 'error');
+    return FALSE;
+  }
+
+  // Move the file to its final destination.
+  return file_unmanaged_move($temp_name, $destination, $replace);
+}
+
+/**
+ * Transfers a file to the client using HTTP.
+ *
+ * Pipes a file through Drupal to the client.
+ *
+ * @param $uri
+ *   String specifying the file URI to transfer.
+ * @param $headers
+ *   An array of HTTP headers to send along with file.
+ */
+function file_transfer($uri, $headers) {
+  if (ob_get_level()) {
+    ob_end_clean();
+  }
+
+  foreach ($headers as $name => $value) {
+    drupal_add_http_header($name, $value);
+  }
+  drupal_send_headers();
+  $scheme = file_uri_scheme($uri);
+  // Transfer file in 1024 byte chunks to save memory usage.
+  if ($scheme && file_stream_wrapper_valid_scheme($scheme) && $fd = fopen($uri, 'rb')) {
+    while (!feof($fd)) {
+      print fread($fd, 1024);
+    }
+    fclose($fd);
+  }
+  else {
+    drupal_not_found();
+  }
+  drupal_exit();
+}
+
+/**
+ * Menu handler for private file transfers.
+ *
+ * Call modules that implement hook_file_download() to find out if a file is
+ * accessible and what headers it should be transferred with. If one or more
+ * modules returned headers the download will start with the returned headers.
+ * If a module returns -1 drupal_access_denied() will be returned. If the file
+ * exists but no modules responded drupal_access_denied() will be returned.
+ * If the file does not exist drupal_not_found() will be returned.
+ *
+ * @see system_menu()
+ */
+function file_download() {
+  // Merge remainder of arguments from GET['q'], into relative file path.
+  $args = func_get_args();
+  $scheme = array_shift($args);
+  $target = implode('/', $args);
+  $uri = $scheme . '://' . $target;
+  if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
+    // Let other modules provide headers and controls access to the file.
+    // module_invoke_all() uses array_merge_recursive() which merges header
+    // values into a new array. To avoid that and allow modules to override
+    // headers instead, use array_merge() to merge the returned arrays.
+    $headers = array();
+    foreach (module_implements('file_download') as $module) {
+      $function = $module . '_file_download';
+      $result = $function($uri);
+      if ($result == -1) {
+        // Throw away the headers received so far.
+        $headers = array();
+        break;
+      }
+      if (isset($result) && is_array($result)) {
+        $headers = array_merge($headers, $result);
+      }
+    }
+    if (count($headers)) {
+      file_transfer($uri, $headers);
+    }
+    drupal_access_denied();
+  }
+  else {
+    drupal_not_found();
+  }
+  drupal_exit();
+}
+
+
+/**
+ * Finds all files that match a given mask in a given directory.
+ *
+ * Directories and files beginning with a period are excluded; this
+ * prevents hidden files and directories (such as SVN working directories)
+ * from being scanned.
+ *
+ * @param $dir
+ *   The base directory or URI to scan, without trailing slash.
+ * @param $mask
+ *   The preg_match() regular expression of the files to find.
+ * @param $options
+ *   An associative array of additional options, with the following elements:
+ *   - 'nomask': The preg_match() regular expression of the files to ignore.
+ *     Defaults to '/(\.\.?|CVS)$/'.
+ *   - 'callback': The callback function to call for each match. There is no
+ *     default callback.
+ *   - 'recurse': When TRUE, the directory scan will recurse the entire tree
+ *     starting at the provided directory. Defaults to TRUE.
+ *   - 'key': The key to be used for the returned associative array of files.
+ *     Possible values are 'uri', for the file's URI; 'filename', for the
+ *     basename of the file; and 'name' for the name of the file without the
+ *     extension. Defaults to 'uri'.
+ *   - 'min_depth': Minimum depth of directories to return files from. Defaults
+ *     to 0.
+ * @param $depth
+ *   Current depth of recursion. This parameter is only used internally and
+ *   should not be passed in.
+ *
+ * @return
+ *   An associative array (keyed on the chosen key) of objects with 'uri',
+ *   'filename', and 'name' members corresponding to the matching files.
+ */
+function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
+  // Merge in defaults.
+  $options += array(
+    'nomask' => '/(\.\.?|CVS)$/',
+    'callback' => 0,
+    'recurse' => TRUE,
+    'key' => 'uri',
+    'min_depth' => 0,
+  );
+
+  $options['key'] = in_array($options['key'], array('uri', 'filename', 'name')) ? $options['key'] : 'uri';
+  $files = array();
+  if (is_dir($dir) && $handle = opendir($dir)) {
+    while (FALSE !== ($filename = readdir($handle))) {
+      if (!preg_match($options['nomask'], $filename) && $filename[0] != '.') {
+        $uri = "$dir/$filename";
+        $uri = file_stream_wrapper_uri_normalize($uri);
+        if (is_dir($uri) && $options['recurse']) {
+          // Give priority to files in this folder by merging them in after any subdirectory files.
+          $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
+        }
+        elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
+          // Always use this match over anything already set in $files with the
+          // same $$options['key'].
+          $file = new stdClass();
+          $file->uri = $uri;
+          $file->filename = $filename;
+          $file->name = pathinfo($filename, PATHINFO_FILENAME);
+          $key = $options['key'];
+          $files[$file->$key] = $file;
+          if ($options['callback']) {
+            $options['callback']($uri);
+          }
+        }
+      }
+    }
+
+    closedir($handle);
+  }
+
+  return $files;
+}
+
+/**
+ * Determines the maximum file upload size by querying the PHP settings.
+ *
+ * @return
+ *   A file size limit in bytes based on the PHP upload_max_filesize and
+ *   post_max_size
+ */
+function file_upload_max_size() {
+  static $max_size = -1;
+
+  if ($max_size < 0) {
+    // Start with post_max_size.
+    $max_size = parse_size(ini_get('post_max_size'));
+
+    // If upload_max_size is less, then reduce. Except if upload_max_size is
+    // zero, which indicates no limit.
+    $upload_max = parse_size(ini_get('upload_max_filesize'));
+    if ($upload_max > 0 && $upload_max < $max_size) {
+      $max_size = $upload_max;
+    }
+  }
+  return $max_size;
+}
+
+/**
+ * Determines an Internet Media Type or MIME type from a filename.
+ *
+ * @param $uri
+ *   A string containing the URI, path, or filename.
+ * @param $mapping
+ *   An optional map of extensions to their mimetypes, in the form:
+ *    - 'mimetypes': a list of mimetypes, keyed by an identifier,
+ *    - 'extensions': the mapping itself, an associative array in which
+ *      the key is the extension (lowercase) and the value is the mimetype
+ *      identifier. If $mapping is NULL file_mimetype_mapping() is called.
+ *
+ * @return
+ *   The internet media type registered for the extension or
+ *   application/octet-stream for unknown extensions.
+ *
+ * @see file_default_mimetype_mapping()
+ */
+function file_get_mimetype($uri, $mapping = NULL) {
+  if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
+    return $wrapper->getMimeType($uri, $mapping);
+  }
+  else {
+    // getMimeType() is not implementation specific, so we can directly
+    // call it without an instance.
+    return DrupalLocalStreamWrapper::getMimeType($uri, $mapping);
+  }
+}
+
+/**
+ * Sets the permissions on a file or directory.
+ *
+ * This function will use the 'file_chmod_directory' and 'file_chmod_file'
+ * variables for the default modes for directories and uploaded/generated
+ * files. By default these will give everyone read access so that users
+ * accessing the files with a user account without the webserver group (e.g.
+ * via FTP) can read these files, and give group write permissions so webserver
+ * group members (e.g. a vhost account) can alter files uploaded and owned by
+ * the webserver.
+ *
+ * PHP's chmod does not support stream wrappers so we use our wrapper
+ * implementation which interfaces with chmod() by default. Contrib wrappers
+ * may override this behavior in their implementations as needed.
+ *
+ * @param $uri
+ *   A string containing a URI file, or directory path.
+ * @param $mode
+ *   Integer value for the permissions. Consult PHP chmod() documentation for
+ *   more information.
+ *
+ * @return
+ *   TRUE for success, FALSE in the event of an error.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_chmod($uri, $mode = NULL) {
+  if (!isset($mode)) {
+    if (is_dir($uri)) {
+      $mode = variable_get('file_chmod_directory', 0775);
+    }
+    else {
+      $mode = variable_get('file_chmod_file', 0664);
+    }
+  }
+
+  // If this URI is a stream, pass it off to the appropriate stream wrapper.
+  // Otherwise, attempt PHP's chmod. This allows use of drupal_chmod even
+  // for unmanaged files outside of the stream wrapper interface.
+  if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
+    if ($wrapper->chmod($mode)) {
+      return TRUE;
+    }
+  }
+  else {
+    if (@chmod($uri, $mode)) {
+      return TRUE;
+    }
+  }
+
+  watchdog('file', 'The file permissions could not be set on %uri.', array('%uri' => $uri), WATCHDOG_ERROR);
+  return FALSE;
+}
+
+/**
+ * Deletes a file.
+ *
+ * PHP's unlink() is broken on Windows, as it can fail to remove a file
+ * when it has a read-only flag set.
+ *
+ * @param $uri
+ *   A URI or pathname.
+ * @param $context
+ *   Refer to http://php.net/manual/en/ref.stream.php
+ *
+ * @return
+ *   Boolean TRUE on success, or FALSE on failure.
+ *
+ * @see unlink()
+ * @ingroup php_wrappers
+ */
+function drupal_unlink($uri, $context = NULL) {
+  $scheme = file_uri_scheme($uri);
+  if ((!$scheme || !file_stream_wrapper_valid_scheme($scheme)) && (substr(PHP_OS, 0, 3) == 'WIN')) {
+    chmod($uri, 0600);
+  }
+  if ($context) {
+    return unlink($uri, $context);
+  }
+  else {
+    return unlink($uri);
+  }
+}
+
+/**
+ * Returns the absolute local filesystem path of a stream URI.
+ *
+ * This function was originally written to ease the conversion of 6.x code to
+ * use 7.x stream wrappers. However, it assumes that every URI may be resolved
+ * to an absolute local filesystem path, and this assumption fails when stream
+ * wrappers are used to support remote file storage. Remote stream wrappers
+ * may implement the realpath method by always returning FALSE. The use of
+ * drupal_realpath() is discouraged, and is slowly being removed from core
+ * functions where possible.
+ *
+ * Only use this function if you know that the stream wrapper in the URI uses
+ * the local file system, and you need to pass an absolute path to a function
+ * that is incompatible with stream URIs.
+ *
+ * @param $uri
+ *   A stream wrapper URI or a filesystem path, possibly including one or more
+ *   symbolic links.
+ *
+ * @return
+ *   The absolute local filesystem path (with no symbolic links), or FALSE on
+ *   failure.
+ *
+ * @todo This function is deprecated, and should be removed wherever possible.
+ *
+ * @see DrupalStreamWrapperInterface::realpath()
+ * @see http://php.net/manual/function.realpath.php
+ * @ingroup php_wrappers
+ */
+function drupal_realpath($uri) {
+  // If this URI is a stream, pass it off to the appropriate stream wrapper.
+  // Otherwise, attempt PHP's realpath. This allows use of drupal_realpath even
+  // for unmanaged files outside of the stream wrapper interface.
+  if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
+    return $wrapper->realpath();
+  }
+  // Check that the URI has a value. There is a bug in PHP 5.2 on *BSD systems
+  // that makes realpath not return FALSE as expected when passing an empty
+  // variable.
+  // @todo Remove when Drupal drops support for PHP 5.2.
+  elseif (!empty($uri)) {
+    return realpath($uri);
+  }
+  return FALSE;
+}
+
+/**
+ * Gets the name of the directory from a given path.
+ *
+ * PHP's dirname() does not properly pass streams, so this function fills
+ * that gap. It is backwards compatible with normal paths and will use
+ * PHP's dirname() as a fallback.
+ *
+ * Compatibility: normal paths and stream wrappers.
+ *
+ * @param $uri
+ *   A URI or path.
+ *
+ * @return
+ *   A string containing the directory name.
+ *
+ * @see dirname()
+ * @see http://drupal.org/node/515192
+ * @ingroup php_wrappers
+ */
+function drupal_dirname($uri) {
+  $scheme = file_uri_scheme($uri);
+
+  if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
+    return file_stream_wrapper_get_instance_by_scheme($scheme)->dirname($uri);
+  }
+  else {
+    return dirname($uri);
+  }
+}
+
+/**
+ * Gets the filename from a given path.
+ *
+ * PHP's basename() does not properly support streams or filenames beginning
+ * with a non-US-ASCII character.
+ *
+ * @see http://bugs.php.net/bug.php?id=37738
+ * @see basename()
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_basename($uri, $suffix = NULL) {
+  $separators = '/';
+  if (DIRECTORY_SEPARATOR != '/') {
+    // For Windows OS add special separator.
+    $separators .= DIRECTORY_SEPARATOR;
+  }
+  // Remove right-most slashes when $uri points to directory.
+  $uri = rtrim($uri, $separators);
+  // Returns the trailing part of the $uri starting after one of the directory
+  // separators.
+  $filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : '';
+  // Cuts off a suffix from the filename.
+  if ($suffix) {
+    $filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename);
+  }
+  return $filename;
+}
+
+/**
+ * Creates a directory using Drupal's default mode.
+ *
+ * PHP's mkdir() does not respect Drupal's default permissions mode. If a mode
+ * is not provided, this function will make sure that Drupal's is used.
+ *
+ * Compatibility: normal paths and stream wrappers.
+ *
+ * @param $uri
+ *   A URI or pathname.
+ * @param $mode
+ *   By default the Drupal mode is used.
+ * @param $recursive
+ *   Default to FALSE.
+ * @param $context
+ *   Refer to http://php.net/manual/en/ref.stream.php
+ *
+ * @return
+ *   Boolean TRUE on success, or FALSE on failure.
+ *
+ * @see mkdir()
+ * @see http://drupal.org/node/515192
+ * @ingroup php_wrappers
+ */
+function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
+  if (!isset($mode)) {
+    $mode = variable_get('file_chmod_directory', 0775);
+  }
+
+  if (!isset($context)) {
+    return mkdir($uri, $mode, $recursive);
+  }
+  else {
+    return mkdir($uri, $mode, $recursive, $context);
+  }
+}
+
+/**
+ * Removes a directory.
+ *
+ * PHP's rmdir() is broken on Windows, as it can fail to remove a directory
+ * when it has a read-only flag set.
+ *
+ * @param $uri
+ *   A URI or pathname.
+ * @param $context
+ *   Refer to http://php.net/manual/en/ref.stream.php
+ *
+ * @return
+ *   Boolean TRUE on success, or FALSE on failure.
+ *
+ * @see rmdir()
+ * @ingroup php_wrappers
+ */
+function drupal_rmdir($uri, $context = NULL) {
+  $scheme = file_uri_scheme($uri);
+  if ((!$scheme || !file_stream_wrapper_valid_scheme($scheme)) && (substr(PHP_OS, 0, 3) == 'WIN')) {
+    chmod($uri, 0700);
+  }
+  if ($context) {
+    return rmdir($uri, $context);
+  }
+  else {
+    return rmdir($uri);
+  }
+}
+
+/**
+ * Creates a file with a unique filename in the specified directory.
+ *
+ * PHP's tempnam() does not return a URI like we want. This function
+ * will return a URI if given a URI, or it will return a filepath if
+ * given a filepath.
+ *
+ * Compatibility: normal paths and stream wrappers.
+ *
+ * @param $directory
+ *   The directory where the temporary filename will be created.
+ * @param $prefix
+ *   The prefix of the generated temporary filename.
+ *   Note: Windows uses only the first three characters of prefix.
+ *
+ * @return
+ *   The new temporary filename, or FALSE on failure.
+ *
+ * @see tempnam()
+ * @see http://drupal.org/node/515192
+ * @ingroup php_wrappers
+ */
+function drupal_tempnam($directory, $prefix) {
+  $scheme = file_uri_scheme($directory);
+
+  if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
+    $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme);
+
+    if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) {
+      return $scheme . '://' . drupal_basename($filename);
+    }
+    else {
+      return FALSE;
+    }
+  }
+  else {
+    // Handle as a normal tempnam() call.
+    return tempnam($directory, $prefix);
+  }
+}
+
+/**
+ * Gets the path of system-appropriate temporary directory.
+ */
+function file_directory_temp() {
+  $temporary_directory = variable_get('file_temporary_path', NULL);
+
+  if (empty($temporary_directory)) {
+    $directories = array();
+
+    // Has PHP been set with an upload_tmp_dir?
+    if (ini_get('upload_tmp_dir')) {
+      $directories[] = ini_get('upload_tmp_dir');
+    }
+
+    // Operating system specific dirs.
+    if (substr(PHP_OS, 0, 3) == 'WIN') {
+      $directories[] = 'c:\\windows\\temp';
+      $directories[] = 'c:\\winnt\\temp';
+    }
+    else {
+      $directories[] = '/tmp';
+    }
+    // PHP may be able to find an alternative tmp directory.
+    // This function exists in PHP 5 >= 5.2.1, but Drupal
+    // requires PHP 5 >= 5.2.0, so we check for it.
+    if (function_exists('sys_get_temp_dir')) {
+      $directories[] = sys_get_temp_dir();
+    }
+
+    foreach ($directories as $directory) {
+      if (is_dir($directory) && is_writable($directory)) {
+        $temporary_directory = $directory;
+        break;
+      }
+    }
+
+    if (empty($temporary_directory)) {
+      // If no directory has been found default to 'files/tmp'.
+      $temporary_directory = variable_get('file_public_path', conf_path() . '/files') . '/tmp';
+
+      // Windows accepts paths with either slash (/) or backslash (\), but will
+      // not accept a path which contains both a slash and a backslash. Since
+      // the 'file_public_path' variable may have either format, we sanitize
+      // everything to use slash which is supported on all platforms.
+      $temporary_directory = str_replace('\\', '/', $temporary_directory);
+    }
+    // Save the path of the discovered directory.
+    variable_set('file_temporary_path', $temporary_directory);
+  }
+
+  return $temporary_directory;
+}
+
+/**
+ * Examines a file object and returns appropriate content headers for download.
+ *
+ * @param $file
+ *   A file object.
+ *
+ * @return
+ *   An associative array of headers, as expected by file_transfer().
+ */
+function file_get_content_headers($file) {
+  $name = mime_header_encode($file->filename);
+  $type = mime_header_encode($file->filemime);
+
+  return array(
+    'Content-Type' => $type,
+    'Content-Length' => $file->filesize,
+    'Cache-Control' => 'private',
+  );
+}
+
+/**
+ * @} End of "defgroup file".
+ */

+ 859 - 0
includes/file.mimetypes.inc

@@ -0,0 +1,859 @@
+<?php
+
+/**
+ * @file
+ * Provides mimetype mappings.
+ */
+
+/**
+ * Return an array of MIME extension mappings.
+ *
+ * Returns the mapping after modules have altered the default mapping.
+ *
+ * @return
+ *   Array of mimetypes correlated to the extensions that relate to them.
+ *
+ * @see file_get_mimetype()
+ */
+function file_mimetype_mapping() {
+  $mapping = &drupal_static(__FUNCTION__);
+  if (!isset($mapping)) {
+    $mapping = file_default_mimetype_mapping();
+    // Allow modules to alter the default mapping.
+    drupal_alter('file_mimetype_mapping', $mapping);
+  }
+  return $mapping;
+}
+
+/**
+ * Default MIME extension mapping.
+ *
+ * @return
+ *   Array of mimetypes correlated to the extensions that relate to them.
+ *
+ * @see file_get_mimetype()
+ */
+function file_default_mimetype_mapping() {
+  return array(
+    'mimetypes' => array(
+      0 => 'application/andrew-inset',
+      1 => 'application/atom',
+      2 => 'application/atomcat+xml',
+      3 => 'application/atomserv+xml',
+      4 => 'application/cap',
+      5 => 'application/cu-seeme',
+      6 => 'application/dsptype',
+      7 => 'application/hta',
+      8 => 'application/java-archive',
+      9 => 'application/java-serialized-object',
+      10 => 'application/java-vm',
+      11 => 'application/mac-binhex40',
+      12 => 'application/mathematica',
+      13 => 'application/msaccess',
+      14 => 'application/msword',
+      15 => 'application/octet-stream',
+      16 => 'application/oda',
+      17 => 'application/ogg',
+      18 => 'application/pdf',
+      19 => 'application/pgp-keys',
+      20 => 'application/pgp-signature',
+      21 => 'application/pics-rules',
+      22 => 'application/postscript',
+      23 => 'application/rar',
+      24 => 'application/rdf+xml',
+      25 => 'application/rss+xml',
+      26 => 'application/rtf',
+      27 => 'application/smil',
+      28 => 'application/vnd.cinderella',
+      29 => 'application/vnd.google-earth.kml+xml',
+      30 => 'application/vnd.google-earth.kmz',
+      31 => 'application/vnd.mozilla.xul+xml',
+      32 => 'application/vnd.ms-excel',
+      33 => 'application/vnd.ms-excel.addin.macroEnabled.12',
+      34 => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+      35 => 'application/vnd.ms-excel.sheet.macroEnabled.12',
+      36 => 'application/vnd.ms-excel.template.macroEnabled.12',
+      37 => 'application/vnd.ms-pki.seccat',
+      38 => 'application/vnd.ms-pki.stl',
+      39 => 'application/vnd.ms-powerpoint',
+      40 => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
+      41 => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
+      42 => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
+      43 => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
+      44 => 'application/vnd.ms-word.document.macroEnabled.12',
+      45 => 'application/vnd.ms-word.template.macroEnabled.12',
+      46 => 'application/vnd.ms-xpsdocument',
+      47 => 'application/vnd.oasis.opendocument.chart',
+      48 => 'application/vnd.oasis.opendocument.database',
+      49 => 'application/vnd.oasis.opendocument.formula',
+      50 => 'application/vnd.oasis.opendocument.graphics',
+      51 => 'application/vnd.oasis.opendocument.graphics-template',
+      52 => 'application/vnd.oasis.opendocument.image',
+      53 => 'application/vnd.oasis.opendocument.presentation',
+      54 => 'application/vnd.oasis.opendocument.presentation-template',
+      55 => 'application/vnd.oasis.opendocument.spreadsheet',
+      56 => 'application/vnd.oasis.opendocument.spreadsheet-template',
+      57 => 'application/vnd.oasis.opendocument.text',
+      58 => 'application/vnd.oasis.opendocument.text-master',
+      59 => 'application/vnd.oasis.opendocument.text-template',
+      60 => 'application/vnd.oasis.opendocument.text-web',
+      61 => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+      62 => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+      63 => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+      64 => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+      65 => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+      66 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      67 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+      68 => 'application/vnd.rim.cod',
+      69 => 'application/vnd.smaf',
+      70 => 'application/vnd.stardivision.calc',
+      71 => 'application/vnd.stardivision.chart',
+      72 => 'application/vnd.stardivision.draw',
+      73 => 'application/vnd.stardivision.impress',
+      74 => 'application/vnd.stardivision.math',
+      75 => 'application/vnd.stardivision.writer',
+      76 => 'application/vnd.stardivision.writer-global',
+      77 => 'application/vnd.sun.xml.calc',
+      78 => 'application/vnd.sun.xml.calc.template',
+      79 => 'application/vnd.sun.xml.draw',
+      80 => 'application/vnd.sun.xml.draw.template',
+      81 => 'application/vnd.sun.xml.impress',
+      82 => 'application/vnd.sun.xml.impress.template',
+      83 => 'application/vnd.sun.xml.math',
+      84 => 'application/vnd.sun.xml.writer',
+      85 => 'application/vnd.sun.xml.writer.global',
+      86 => 'application/vnd.sun.xml.writer.template',
+      87 => 'application/vnd.symbian.install',
+      88 => 'application/vnd.visio',
+      89 => 'application/vnd.wap.wbxml',
+      90 => 'application/vnd.wap.wmlc',
+      91 => 'application/vnd.wap.wmlscriptc',
+      92 => 'application/wordperfect',
+      93 => 'application/wordperfect5.1',
+      94 => 'application/x-123',
+      95 => 'application/x-7z-compressed',
+      96 => 'application/x-abiword',
+      97 => 'application/x-apple-diskimage',
+      98 => 'application/x-bcpio',
+      99 => 'application/x-bittorrent',
+      100 => 'application/x-cab',
+      101 => 'application/x-cbr',
+      102 => 'application/x-cbz',
+      103 => 'application/x-cdf',
+      104 => 'application/x-cdlink',
+      105 => 'application/x-chess-pgn',
+      106 => 'application/x-cpio',
+      107 => 'application/x-debian-package',
+      108 => 'application/x-director',
+      109 => 'application/x-dms',
+      110 => 'application/x-doom',
+      111 => 'application/x-dvi',
+      112 => 'application/x-flac',
+      113 => 'application/x-font',
+      114 => 'application/x-freemind',
+      115 => 'application/x-futuresplash',
+      116 => 'application/x-gnumeric',
+      117 => 'application/x-go-sgf',
+      118 => 'application/x-graphing-calculator',
+      119 => 'application/x-gtar',
+      120 => 'application/x-hdf',
+      121 => 'application/x-httpd-eruby',
+      122 => 'application/x-httpd-php',
+      123 => 'application/x-httpd-php-source',
+      124 => 'application/x-httpd-php3',
+      125 => 'application/x-httpd-php3-preprocessed',
+      126 => 'application/x-httpd-php4',
+      127 => 'application/x-ica',
+      128 => 'application/x-internet-signup',
+      129 => 'application/x-iphone',
+      130 => 'application/x-iso9660-image',
+      131 => 'application/x-java-jnlp-file',
+      132 => 'application/x-javascript',
+      133 => 'application/x-jmol',
+      134 => 'application/x-kchart',
+      135 => 'application/x-killustrator',
+      136 => 'application/x-koan',
+      137 => 'application/x-kpresenter',
+      138 => 'application/x-kspread',
+      139 => 'application/x-kword',
+      140 => 'application/x-latex',
+      141 => 'application/x-lha',
+      142 => 'application/x-lyx',
+      143 => 'application/x-lzh',
+      144 => 'application/x-lzx',
+      145 => 'application/x-maker',
+      146 => 'application/x-mif',
+      147 => 'application/x-ms-wmd',
+      148 => 'application/x-ms-wmz',
+      149 => 'application/x-msdos-program',
+      150 => 'application/x-msi',
+      151 => 'application/x-netcdf',
+      152 => 'application/x-ns-proxy-autoconfig',
+      153 => 'application/x-nwc',
+      154 => 'application/x-object',
+      155 => 'application/x-oz-application',
+      156 => 'application/x-pkcs7-certreqresp',
+      157 => 'application/x-pkcs7-crl',
+      158 => 'application/x-python-code',
+      159 => 'application/x-quicktimeplayer',
+      160 => 'application/x-redhat-package-manager',
+      161 => 'application/x-shar',
+      162 => 'application/x-shockwave-flash',
+      163 => 'application/x-stuffit',
+      164 => 'application/x-sv4cpio',
+      165 => 'application/x-sv4crc',
+      166 => 'application/x-tar',
+      167 => 'application/x-tcl',
+      168 => 'application/x-tex-gf',
+      169 => 'application/x-tex-pk',
+      170 => 'application/x-texinfo',
+      171 => 'application/x-trash',
+      172 => 'application/x-troff',
+      173 => 'application/x-troff-man',
+      174 => 'application/x-troff-me',
+      175 => 'application/x-troff-ms',
+      176 => 'application/x-ustar',
+      177 => 'application/x-wais-source',
+      178 => 'application/x-wingz',
+      179 => 'application/x-x509-ca-cert',
+      180 => 'application/x-xcf',
+      181 => 'application/x-xfig',
+      182 => 'application/x-xpinstall',
+      183 => 'application/xhtml+xml',
+      184 => 'application/xml',
+      185 => 'application/zip',
+      186 => 'audio/basic',
+      187 => 'audio/midi',
+      346 => 'audio/mp4',
+      188 => 'audio/mpeg',
+      189 => 'audio/ogg',
+      190 => 'audio/prs.sid',
+      191 => 'audio/x-aiff',
+      192 => 'audio/x-gsm',
+      193 => 'audio/x-mpegurl',
+      194 => 'audio/x-ms-wax',
+      195 => 'audio/x-ms-wma',
+      196 => 'audio/x-pn-realaudio',
+      197 => 'audio/x-realaudio',
+      198 => 'audio/x-scpls',
+      199 => 'audio/x-sd2',
+      200 => 'audio/x-wav',
+      201 => 'chemical/x-alchemy',
+      202 => 'chemical/x-cache',
+      203 => 'chemical/x-cache-csf',
+      204 => 'chemical/x-cactvs-binary',
+      205 => 'chemical/x-cdx',
+      206 => 'chemical/x-cerius',
+      207 => 'chemical/x-chem3d',
+      208 => 'chemical/x-chemdraw',
+      209 => 'chemical/x-cif',
+      210 => 'chemical/x-cmdf',
+      211 => 'chemical/x-cml',
+      212 => 'chemical/x-compass',
+      213 => 'chemical/x-crossfire',
+      214 => 'chemical/x-csml',
+      215 => 'chemical/x-ctx',
+      216 => 'chemical/x-cxf',
+      217 => 'chemical/x-embl-dl-nucleotide',
+      218 => 'chemical/x-galactic-spc',
+      219 => 'chemical/x-gamess-input',
+      220 => 'chemical/x-gaussian-checkpoint',
+      221 => 'chemical/x-gaussian-cube',
+      222 => 'chemical/x-gaussian-input',
+      223 => 'chemical/x-gaussian-log',
+      224 => 'chemical/x-gcg8-sequence',
+      225 => 'chemical/x-genbank',
+      226 => 'chemical/x-hin',
+      227 => 'chemical/x-isostar',
+      228 => 'chemical/x-jcamp-dx',
+      229 => 'chemical/x-kinemage',
+      230 => 'chemical/x-macmolecule',
+      231 => 'chemical/x-macromodel-input',
+      232 => 'chemical/x-mdl-molfile',
+      233 => 'chemical/x-mdl-rdfile',
+      234 => 'chemical/x-mdl-rxnfile',
+      235 => 'chemical/x-mdl-sdfile',
+      236 => 'chemical/x-mdl-tgf',
+      237 => 'chemical/x-mmcif',
+      238 => 'chemical/x-mol2',
+      239 => 'chemical/x-molconn-Z',
+      240 => 'chemical/x-mopac-graph',
+      241 => 'chemical/x-mopac-input',
+      242 => 'chemical/x-mopac-out',
+      243 => 'chemical/x-mopac-vib',
+      244 => 'chemical/x-ncbi-asn1-ascii',
+      245 => 'chemical/x-ncbi-asn1-binary',
+      246 => 'chemical/x-ncbi-asn1-spec',
+      247 => 'chemical/x-pdb',
+      248 => 'chemical/x-rosdal',
+      249 => 'chemical/x-swissprot',
+      250 => 'chemical/x-vamas-iso14976',
+      251 => 'chemical/x-vmd',
+      252 => 'chemical/x-xtel',
+      253 => 'chemical/x-xyz',
+      254 => 'image/gif',
+      255 => 'image/ief',
+      256 => 'image/jpeg',
+      257 => 'image/pcx',
+      258 => 'image/png',
+      259 => 'image/svg+xml',
+      260 => 'image/tiff',
+      261 => 'image/vnd.djvu',
+      262 => 'image/vnd.microsoft.icon',
+      263 => 'image/vnd.wap.wbmp',
+      264 => 'image/x-cmu-raster',
+      265 => 'image/x-coreldraw',
+      266 => 'image/x-coreldrawpattern',
+      267 => 'image/x-coreldrawtemplate',
+      268 => 'image/x-corelphotopaint',
+      269 => 'image/x-jg',
+      270 => 'image/x-jng',
+      271 => 'image/x-ms-bmp',
+      272 => 'image/x-photoshop',
+      273 => 'image/x-portable-anymap',
+      274 => 'image/x-portable-bitmap',
+      275 => 'image/x-portable-graymap',
+      276 => 'image/x-portable-pixmap',
+      277 => 'image/x-rgb',
+      278 => 'image/x-xbitmap',
+      279 => 'image/x-xpixmap',
+      280 => 'image/x-xwindowdump',
+      281 => 'message/rfc822',
+      282 => 'model/iges',
+      283 => 'model/mesh',
+      284 => 'model/vrml',
+      285 => 'text/calendar',
+      286 => 'text/css',
+      287 => 'text/csv',
+      288 => 'text/h323',
+      289 => 'text/html',
+      290 => 'text/iuls',
+      291 => 'text/mathml',
+      292 => 'text/plain',
+      293 => 'text/richtext',
+      294 => 'text/scriptlet',
+      295 => 'text/tab-separated-values',
+      296 => 'text/texmacs',
+      297 => 'text/vnd.sun.j2me.app-descriptor',
+      298 => 'text/vnd.wap.wml',
+      299 => 'text/vnd.wap.wmlscript',
+      300 => 'text/x-bibtex',
+      301 => 'text/x-boo',
+      302 => 'text/x-c++hdr',
+      303 => 'text/x-c++src',
+      304 => 'text/x-chdr',
+      305 => 'text/x-component',
+      306 => 'text/x-csh',
+      307 => 'text/x-csrc',
+      308 => 'text/x-diff',
+      309 => 'text/x-dsrc',
+      310 => 'text/x-haskell',
+      311 => 'text/x-java',
+      312 => 'text/x-literate-haskell',
+      313 => 'text/x-moc',
+      314 => 'text/x-pascal',
+      315 => 'text/x-pcs-gcd',
+      316 => 'text/x-perl',
+      317 => 'text/x-python',
+      318 => 'text/x-setext',
+      319 => 'text/x-sh',
+      320 => 'text/x-tcl',
+      321 => 'text/x-tex',
+      322 => 'text/x-vcalendar',
+      323 => 'text/x-vcard',
+      324 => 'video/3gpp',
+      325 => 'video/dl',
+      326 => 'video/dv',
+      327 => 'video/fli',
+      328 => 'video/gl',
+      329 => 'video/mp4',
+      330 => 'video/mpeg',
+      331 => 'video/ogg',
+      332 => 'video/quicktime',
+      333 => 'video/vnd.mpegurl',
+      347 => 'video/x-flv',
+      334 => 'video/x-la-asf',
+      348 => 'video/x-m4v',
+      335 => 'video/x-mng',
+      336 => 'video/x-ms-asf',
+      337 => 'video/x-ms-wm',
+      338 => 'video/x-ms-wmv',
+      339 => 'video/x-ms-wmx',
+      340 => 'video/x-ms-wvx',
+      341 => 'video/x-msvideo',
+      342 => 'video/x-sgi-movie',
+      343 => 'x-conference/x-cooltalk',
+      344 => 'x-epoc/x-sisx-app',
+      345 => 'x-world/x-vrml',
+    ),
+
+    // Extensions added to this list MUST be lower-case.
+    'extensions' => array(
+      'ez' => 0,
+      'atom' => 1,
+      'atomcat' => 2,
+      'atomsrv' => 3,
+      'cap' => 4,
+      'pcap' => 4,
+      'cu' => 5,
+      'tsp' => 6,
+      'hta' => 7,
+      'jar' => 8,
+      'ser' => 9,
+      'class' => 10,
+      'hqx' => 11,
+      'nb' => 12,
+      'mdb' => 13,
+      'dot' => 14,
+      'doc' => 14,
+      'bin' => 15,
+      'oda' => 16,
+      'ogx' => 17,
+      'pdf' => 18,
+      'key' => 19,
+      'pgp' => 20,
+      'prf' => 21,
+      'eps' => 22,
+      'ai' => 22,
+      'ps' => 22,
+      'rar' => 23,
+      'rdf' => 24,
+      'rss' => 25,
+      'rtf' => 26,
+      'smi' => 27,
+      'smil' => 27,
+      'cdy' => 28,
+      'kml' => 29,
+      'kmz' => 30,
+      'xul' => 31,
+      'xlb' => 32,
+      'xlt' => 32,
+      'xls' => 32,
+      'xlam' => 33,
+      'xlsb' => 34,
+      'xlsm' => 35,
+      'xltm' => 36,
+      'cat' => 37,
+      'stl' => 38,
+      'pps' => 39,
+      'ppt' => 39,
+      'ppam' => 40,
+      'pptm' => 41,
+      'ppsm' => 42,
+      'potm' => 43,
+      'docm' => 44,
+      'dotm' => 45,
+      'xps' => 46,
+      'odc' => 47,
+      'odb' => 48,
+      'odf' => 49,
+      'odg' => 50,
+      'otg' => 51,
+      'odi' => 52,
+      'odp' => 53,
+      'otp' => 54,
+      'ods' => 55,
+      'ots' => 56,
+      'odt' => 57,
+      'odm' => 58,
+      'ott' => 59,
+      'oth' => 60,
+      'pptx' => 61,
+      'ppsx' => 62,
+      'potx' => 63,
+      'xlsx' => 64,
+      'xltx' => 65,
+      'docx' => 66,
+      'dotx' => 67,
+      'cod' => 68,
+      'mmf' => 69,
+      'sdc' => 70,
+      'sds' => 71,
+      'sda' => 72,
+      'sdd' => 73,
+      'sdw' => 75,
+      'sgl' => 76,
+      'sxc' => 77,
+      'stc' => 78,
+      'sxd' => 79,
+      'std' => 80,
+      'sxi' => 81,
+      'sti' => 82,
+      'sxm' => 83,
+      'sxw' => 84,
+      'sxg' => 85,
+      'stw' => 86,
+      'sis' => 87,
+      'vsd' => 88,
+      'wbxml' => 89,
+      'wmlc' => 90,
+      'wmlsc' => 91,
+      'wpd' => 92,
+      'wp5' => 93,
+      'wk' => 94,
+      '7z' => 95,
+      'abw' => 96,
+      'dmg' => 97,
+      'bcpio' => 98,
+      'torrent' => 99,
+      'cab' => 100,
+      'cbr' => 101,
+      'cbz' => 102,
+      'cdf' => 103,
+      'vcd' => 104,
+      'pgn' => 105,
+      'cpio' => 106,
+      'udeb' => 107,
+      'deb' => 107,
+      'dir' => 108,
+      'dxr' => 108,
+      'dcr' => 108,
+      'dms' => 109,
+      'wad' => 110,
+      'dvi' => 111,
+      'flac' => 112,
+      'pfa' => 113,
+      'pfb' => 113,
+      'pcf' => 113,
+      'gsf' => 113,
+      'pcf.z' => 113,
+      'mm' => 114,
+      'spl' => 115,
+      'gnumeric' => 116,
+      'sgf' => 117,
+      'gcf' => 118,
+      'taz' => 119,
+      'gtar' => 119,
+      'tgz' => 119,
+      'hdf' => 120,
+      'rhtml' => 121,
+      'phtml' => 122,
+      'pht' => 122,
+      'php' => 122,
+      'phps' => 123,
+      'php3' => 124,
+      'php3p' => 125,
+      'php4' => 126,
+      'ica' => 127,
+      'ins' => 128,
+      'isp' => 128,
+      'iii' => 129,
+      'iso' => 130,
+      'jnlp' => 131,
+      'js' => 132,
+      'jmz' => 133,
+      'chrt' => 134,
+      'kil' => 135,
+      'skp' => 136,
+      'skd' => 136,
+      'skm' => 136,
+      'skt' => 136,
+      'kpr' => 137,
+      'kpt' => 137,
+      'ksp' => 138,
+      'kwd' => 139,
+      'kwt' => 139,
+      'latex' => 140,
+      'lha' => 141,
+      'lyx' => 142,
+      'lzh' => 143,
+      'lzx' => 144,
+      'maker' => 145,
+      'frm' => 145,
+      'frame' => 145,
+      'fm' => 145,
+      'book' => 145,
+      'fb' => 145,
+      'fbdoc' => 145,
+      'mif' => 146,
+      'wmd' => 147,
+      'wmz' => 148,
+      'dll' => 149,
+      'bat' => 149,
+      'exe' => 149,
+      'com' => 149,
+      'msi' => 150,
+      'nc' => 151,
+      'pac' => 152,
+      'nwc' => 153,
+      'o' => 154,
+      'oza' => 155,
+      'p7r' => 156,
+      'crl' => 157,
+      'pyo' => 158,
+      'pyc' => 158,
+      'qtl' => 159,
+      'rpm' => 160,
+      'shar' => 161,
+      'swf' => 162,
+      'swfl' => 162,
+      'sitx' => 163,
+      'sit' => 163,
+      'sv4cpio' => 164,
+      'sv4crc' => 165,
+      'tar' => 166,
+      'gf' => 168,
+      'pk' => 169,
+      'texi' => 170,
+      'texinfo' => 170,
+      'sik' => 171,
+      '~' => 171,
+      'bak' => 171,
+      '%' => 171,
+      'old' => 171,
+      't' => 172,
+      'roff' => 172,
+      'tr' => 172,
+      'man' => 173,
+      'me' => 174,
+      'ms' => 175,
+      'ustar' => 176,
+      'src' => 177,
+      'wz' => 178,
+      'crt' => 179,
+      'xcf' => 180,
+      'fig' => 181,
+      'xpi' => 182,
+      'xht' => 183,
+      'xhtml' => 183,
+      'xml' => 184,
+      'xsl' => 184,
+      'zip' => 185,
+      'au' => 186,
+      'snd' => 186,
+      'mid' => 187,
+      'midi' => 187,
+      'kar' => 187,
+      'mpega' => 188,
+      'mpga' => 188,
+      'm4a' => 188,
+      'mp3' => 188,
+      'mp2' => 188,
+      'ogg' => 189,
+      'oga' => 189,
+      'spx' => 189,
+      'sid' => 190,
+      'aif' => 191,
+      'aiff' => 191,
+      'aifc' => 191,
+      'gsm' => 192,
+      'm3u' => 193,
+      'wax' => 194,
+      'wma' => 195,
+      'rm' => 196,
+      'ram' => 196,
+      'ra' => 197,
+      'pls' => 198,
+      'sd2' => 199,
+      'wav' => 200,
+      'alc' => 201,
+      'cac' => 202,
+      'cache' => 202,
+      'csf' => 203,
+      'cascii' => 204,
+      'cbin' => 204,
+      'ctab' => 204,
+      'cdx' => 205,
+      'cer' => 206,
+      'c3d' => 207,
+      'chm' => 208,
+      'cif' => 209,
+      'cmdf' => 210,
+      'cml' => 211,
+      'cpa' => 212,
+      'bsd' => 213,
+      'csml' => 214,
+      'csm' => 214,
+      'ctx' => 215,
+      'cxf' => 216,
+      'cef' => 216,
+      'emb' => 217,
+      'embl' => 217,
+      'spc' => 218,
+      'gam' => 219,
+      'inp' => 219,
+      'gamin' => 219,
+      'fchk' => 220,
+      'fch' => 220,
+      'cub' => 221,
+      'gau' => 222,
+      'gjf' => 222,
+      'gjc' => 222,
+      'gal' => 223,
+      'gcg' => 224,
+      'gen' => 225,
+      'hin' => 226,
+      'istr' => 227,
+      'ist' => 227,
+      'dx' => 228,
+      'jdx' => 228,
+      'kin' => 229,
+      'mcm' => 230,
+      'mmd' => 231,
+      'mmod' => 231,
+      'mol' => 232,
+      'rd' => 233,
+      'rxn' => 234,
+      'sdf' => 235,
+      'sd' => 235,
+      'tgf' => 236,
+      'mcif' => 237,
+      'mol2' => 238,
+      'b' => 239,
+      'gpt' => 240,
+      'mopcrt' => 241,
+      'zmt' => 241,
+      'mpc' => 241,
+      'dat' => 241,
+      'mop' => 241,
+      'moo' => 242,
+      'mvb' => 243,
+      'prt' => 244,
+      'aso' => 245,
+      'val' => 245,
+      'asn' => 246,
+      'ent' => 247,
+      'pdb' => 247,
+      'ros' => 248,
+      'sw' => 249,
+      'vms' => 250,
+      'vmd' => 251,
+      'xtel' => 252,
+      'xyz' => 253,
+      'gif' => 254,
+      'ief' => 255,
+      'jpeg' => 256,
+      'jpe' => 256,
+      'jpg' => 256,
+      'pcx' => 257,
+      'png' => 258,
+      'svgz' => 259,
+      'svg' => 259,
+      'tif' => 260,
+      'tiff' => 260,
+      'djvu' => 261,
+      'djv' => 261,
+      'ico' => 262,
+      'wbmp' => 263,
+      'ras' => 264,
+      'cdr' => 265,
+      'pat' => 266,
+      'cdt' => 267,
+      'cpt' => 268,
+      'art' => 269,
+      'jng' => 270,
+      'bmp' => 271,
+      'psd' => 272,
+      'pnm' => 273,
+      'pbm' => 274,
+      'pgm' => 275,
+      'ppm' => 276,
+      'rgb' => 277,
+      'xbm' => 278,
+      'xpm' => 279,
+      'xwd' => 280,
+      'eml' => 281,
+      'igs' => 282,
+      'iges' => 282,
+      'silo' => 283,
+      'msh' => 283,
+      'mesh' => 283,
+      'icz' => 285,
+      'ics' => 285,
+      'css' => 286,
+      'csv' => 287,
+      '323' => 288,
+      'html' => 289,
+      'htm' => 289,
+      'shtml' => 289,
+      'uls' => 290,
+      'mml' => 291,
+      'txt' => 292,
+      'pot' => 292,
+      'text' => 292,
+      'asc' => 292,
+      'rtx' => 293,
+      'wsc' => 294,
+      'sct' => 294,
+      'tsv' => 295,
+      'ts' => 296,
+      'tm' => 296,
+      'jad' => 297,
+      'wml' => 298,
+      'wmls' => 299,
+      'bib' => 300,
+      'boo' => 301,
+      'hpp' => 302,
+      'hh' => 302,
+      'h++' => 302,
+      'hxx' => 302,
+      'cxx' => 303,
+      'cc' => 303,
+      'cpp' => 303,
+      'c++' => 303,
+      'h' => 304,
+      'htc' => 305,
+      'csh' => 306,
+      'c' => 307,
+      'patch' => 308,
+      'diff' => 308,
+      'd' => 309,
+      'hs' => 310,
+      'java' => 311,
+      'lhs' => 312,
+      'moc' => 313,
+      'pas' => 314,
+      'p' => 314,
+      'gcd' => 315,
+      'pm' => 316,
+      'pl' => 316,
+      'py' => 317,
+      'etx' => 318,
+      'sh' => 319,
+      'tk' => 320,
+      'tcl' => 320,
+      'cls' => 321,
+      'ltx' => 321,
+      'sty' => 321,
+      'tex' => 321,
+      'vcs' => 322,
+      'vcf' => 323,
+      '3gp' => 324,
+      'dl' => 325,
+      'dif' => 326,
+      'dv' => 326,
+      'fli' => 327,
+      'gl' => 328,
+      'mp4' => 329,
+      'f4v' => 329,
+      'f4p' => 329,
+      'mpe' => 330,
+      'mpeg' => 330,
+      'mpg' => 330,
+      'ogv' => 331,
+      'qt' => 332,
+      'mov' => 332,
+      'mxu' => 333,
+      'lsf' => 334,
+      'lsx' => 334,
+      'mng' => 335,
+      'asx' => 336,
+      'asf' => 336,
+      'wm' => 337,
+      'wmv' => 338,
+      'wmx' => 339,
+      'wvx' => 340,
+      'avi' => 341,
+      'movie' => 342,
+      'ice' => 343,
+      'sisx' => 344,
+      'wrl' => 345,
+      'vrm' => 345,
+      'vrml' => 345,
+      'f4a' => 346,
+      'f4b' => 346,
+      'flv' => 347,
+      'm4v' => 348,
+    ),
+  );
+}

+ 427 - 0
includes/filetransfer/filetransfer.inc

@@ -0,0 +1,427 @@
+<?php
+
+/*
+ * Base FileTransfer class.
+ *
+ * Classes extending this class perform file operations on directories not
+ * writable by the webserver. To achieve this, the class should connect back
+ * to the server using some backend (for example FTP or SSH). To keep security,
+ * the password should always be asked from the user and never stored. For
+ * safety, all methods operate only inside a "jail", by default the Drupal root.
+ */
+abstract class FileTransfer {
+  protected $username;
+  protected $password;
+  protected $hostname = 'localhost';
+  protected $port;
+
+  /**
+   * The constructor for the UpdateConnection class. This method is also called
+   * from the classes that extend this class and override this method.
+   */
+  function __construct($jail) {
+    $this->jail = $jail;
+  }
+
+  /**
+   * Classes that extend this class must override the factory() static method.
+   *
+   * @param string $jail
+   *   The full path where all file operations performed by this object will
+   *   be restricted to. This prevents the FileTransfer classes from being
+   *   able to touch other parts of the filesystem.
+   * @param array $settings
+   *   An array of connection settings for the FileTransfer subclass. If the
+   *   getSettingsForm() method uses any nested settings, the same structure
+   *   will be assumed here.
+   * @return object
+   *   New instance of the appropriate FileTransfer subclass.
+   */
+  static function factory($jail, $settings) {
+    throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
+  }
+
+  /**
+   * Implementation of the magic __get() method.
+   *
+   * If the connection isn't set to anything, this will call the connect() method
+   * and set it to and return the result; afterwards, the connection will be
+   * returned directly without using this method.
+   */
+  function __get($name) {
+    if ($name == 'connection') {
+      $this->connect();
+      return $this->connection;
+    }
+
+    if ($name == 'chroot') {
+      $this->setChroot();
+      return $this->chroot;
+    }
+  }
+
+  /**
+   * Connect to the server.
+   */
+  abstract protected function connect();
+
+  /**
+   * Copies a directory.
+   *
+   * @param $source
+   *   The source path.
+   * @param $destination
+   *   The destination path.
+   */
+  public final function copyDirectory($source, $destination) {
+    $source = $this->sanitizePath($source);
+    $destination = $this->fixRemotePath($destination);
+    $this->checkPath($destination);
+    $this->copyDirectoryJailed($source, $destination);
+  }
+
+  /**
+   * @see http://php.net/chmod
+   *
+   * @param string $path
+   * @param long $mode
+   * @param bool $recursive
+   */
+  public final function chmod($path, $mode, $recursive = FALSE) {
+    if (!in_array('FileTransferChmodInterface', class_implements(get_class($this)))) {
+      throw new FileTransferException('Unable to change file permissions');
+    }
+    $path = $this->sanitizePath($path);
+    $path = $this->fixRemotePath($path);
+    $this->checkPath($path);
+    $this->chmodJailed($path, $mode, $recursive);
+  }
+
+  /**
+   * Creates a directory.
+   *
+   * @param $directory
+   *   The directory to be created.
+   */
+  public final function createDirectory($directory) {
+    $directory = $this->fixRemotePath($directory);
+    $this->checkPath($directory);
+    $this->createDirectoryJailed($directory);
+  }
+
+  /**
+   * Removes a directory.
+   *
+   * @param $directory
+   *   The directory to be removed.
+   */
+  public final function removeDirectory($directory) {
+    $directory = $this->fixRemotePath($directory);
+    $this->checkPath($directory);
+    $this->removeDirectoryJailed($directory);
+  }
+
+  /**
+   * Copies a file.
+   *
+   * @param $source
+   *   The source file.
+   * @param $destination
+   *   The destination file.
+   */
+  public final function copyFile($source, $destination) {
+    $source = $this->sanitizePath($source);
+    $destination = $this->fixRemotePath($destination);
+    $this->checkPath($destination);
+    $this->copyFileJailed($source, $destination);
+  }
+
+  /**
+   * Removes a file.
+   *
+   * @param $destination
+   *   The destination file to be removed.
+   */
+  public final function removeFile($destination) {
+    $destination = $this->fixRemotePath($destination);
+    $this->checkPath($destination);
+    $this->removeFileJailed($destination);
+  }
+
+  /**
+   * Checks that the path is inside the jail and throws an exception if not.
+   *
+   * @param $path
+   *   A path to check against the jail.
+   */
+  protected final function checkPath($path) {
+    $full_jail = $this->chroot . $this->jail;
+    $full_path = drupal_realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
+    $full_path = $this->fixRemotePath($full_path, FALSE);
+    if ($full_jail !== $full_path) {
+      throw new FileTransferException('@directory is outside of the @jail', NULL, array('@directory' => $path, '@jail' => $this->jail));
+    }
+  }
+
+  /**
+   * Returns a modified path suitable for passing to the server.
+   * If a path is a windows path, makes it POSIX compliant by removing the drive letter.
+   * If $this->chroot has a value, it is stripped from the path to allow for
+   * chroot'd filetransfer systems.
+   *
+   * @param $path
+   * @param $strip_chroot
+   *
+   * @return string
+   */
+  protected final function fixRemotePath($path, $strip_chroot = TRUE) {
+    $path = $this->sanitizePath($path);
+    $path = preg_replace('|^([a-z]{1}):|i', '', $path); // Strip out windows driveletter if its there.
+    if ($strip_chroot) {
+      if ($this->chroot && strpos($path, $this->chroot) === 0) {
+        $path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
+      }
+    }
+    return $path;
+  }
+
+  /**
+  * Changes backslashes to slashes, also removes a trailing slash.
+  *
+  * @param string $path
+  * @return string
+  */
+  function sanitizePath($path) {
+    $path = str_replace('\\', '/', $path); // Windows path sanitization.
+    if (substr($path, -1) == '/') {
+      $path = substr($path, 0, -1);
+    }
+    return $path;
+  }
+
+  /**
+   * Copies a directory.
+   *
+   * We need a separate method to make the $destination is in the jail.
+   *
+   * @param $source
+   *   The source path.
+   * @param $destination
+   *   The destination path.
+   */
+  protected function copyDirectoryJailed($source, $destination) {
+    if ($this->isDirectory($destination)) {
+      $destination = $destination . '/' . drupal_basename($source);
+    }
+    $this->createDirectory($destination);
+    foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
+      $relative_path = substr($filename, strlen($source));
+      if ($file->isDir()) {
+        $this->createDirectory($destination . $relative_path);
+      }
+      else {
+        $this->copyFile($file->getPathName(), $destination . $relative_path);
+      }
+    }
+  }
+
+  /**
+   * Creates a directory.
+   *
+   * @param $directory
+   *   The directory to be created.
+   */
+  abstract protected function createDirectoryJailed($directory);
+
+  /**
+   * Removes a directory.
+   *
+   * @param $directory
+   *   The directory to be removed.
+   */
+  abstract protected function removeDirectoryJailed($directory);
+
+  /**
+   * Copies a file.
+   *
+   * @param $source
+   *   The source file.
+   * @param $destination
+   *   The destination file.
+   */
+  abstract protected function copyFileJailed($source, $destination);
+
+  /**
+   * Removes a file.
+   *
+   * @param $destination
+   *   The destination file to be removed.
+   */
+  abstract protected function removeFileJailed($destination);
+
+  /**
+   * Checks if a particular path is a directory
+   *
+   * @param $path
+   *   The path to check
+   *
+   * @return boolean
+   */
+  abstract public function isDirectory($path);
+
+  /**
+   * Checks if a particular path is a file (not a directory).
+   *
+   * @param $path
+   *   The path to check
+   *
+   * @return boolean
+   */
+  abstract public function isFile($path);
+
+  /**
+   * Return the chroot property for this connection.
+   *
+   * It does this by moving up the tree until it finds itself. If successful,
+   * it will return the chroot, otherwise FALSE.
+   *
+   * @return
+   *   The chroot path for this connection or FALSE.
+   */
+  function findChroot() {
+    // If the file exists as is, there is no chroot.
+    $path = __FILE__;
+    $path = $this->fixRemotePath($path, FALSE);
+    if ($this->isFile($path)) {
+      return FALSE;
+    }
+
+    $path = dirname(__FILE__);
+    $path = $this->fixRemotePath($path, FALSE);
+    $parts = explode('/', $path);
+    $chroot = '';
+    while (count($parts)) {
+      $check = implode($parts, '/');
+      if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
+        // Remove the trailing slash.
+        return substr($chroot, 0, -1);
+      }
+      $chroot .= array_shift($parts) . '/';
+    }
+    return FALSE;
+  }
+
+  /**
+   * Sets the chroot and changes the jail to match the correct path scheme
+   *
+   */
+  function setChroot() {
+    $this->chroot = $this->findChroot();
+    $this->jail = $this->fixRemotePath($this->jail);
+  }
+
+  /**
+   * Returns a form to collect connection settings credentials.
+   *
+   * Implementing classes can either extend this form with fields collecting the
+   * specific information they need, or override it entirely.
+   */
+  public function getSettingsForm() {
+    $form['username'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Username'),
+    );
+    $form['password'] = array(
+      '#type' => 'password',
+      '#title' => t('Password'),
+      '#description' => t('Your password is not saved in the database and is only used to establish a connection.'),
+    );
+    $form['advanced'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Advanced settings'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+    );
+    $form['advanced']['hostname'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Host'),
+      '#default_value' => 'localhost',
+      '#description' => t('The connection will be created between your web server and the machine hosting the web server files. In the vast majority of cases, this will be the same machine, and "localhost" is correct.'),
+    );
+    $form['advanced']['port'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Port'),
+      '#default_value' => NULL,
+    );
+    return $form;
+  }
+}
+
+/**
+ * FileTransferException class.
+ */
+class FileTransferException extends Exception {
+  public $arguments;
+
+  function __construct($message, $code = 0, $arguments = array()) {
+    parent::__construct($message, $code);
+    $this->arguments = $arguments;
+  }
+}
+
+
+/**
+ * A FileTransfer Class implementing this interface can be used to chmod files.
+ */
+interface FileTransferChmodInterface {
+
+  /**
+   * Changes the permissions of the file / directory specified in $path
+   *
+   * @param string $path
+   *   Path to change permissions of.
+   * @param long $mode
+   *   The new file permission mode to be passed to chmod().
+   * @param boolean $recursive
+   *   Pass TRUE to recursively chmod the entire directory specified in $path.
+   */
+  function chmodJailed($path, $mode, $recursive);
+}
+
+/**
+ * Provides an interface for iterating recursively over filesystem directories.
+ *
+ * Manually skips '.' and '..' directories, since no existing method is
+ * available in PHP 5.2.
+ *
+ * @todo Depreciate in favor of RecursiveDirectoryIterator::SKIP_DOTS once PHP
+ *   5.3 or later is required.
+ */
+class SkipDotsRecursiveDirectoryIterator extends RecursiveDirectoryIterator {
+  /**
+   * Constructs a SkipDotsRecursiveDirectoryIterator
+   *
+   * @param $path
+   *   The path of the directory to be iterated over.
+   */
+  function __construct($path) {
+    parent::__construct($path);
+    $this->skipdots();
+  }
+
+  function rewind() {
+    parent::rewind();
+    $this->skipdots();
+  }
+
+  function next() {
+    parent::next();
+    $this->skipdots();
+  }
+
+  protected function skipdots() {
+    while ($this->isDot()) {
+      parent::next();
+    }
+  }
+}

+ 144 - 0
includes/filetransfer/ftp.inc

@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * Base class for FTP implementations.
+ */
+abstract class FileTransferFTP extends FileTransfer {
+
+  public function __construct($jail, $username, $password, $hostname, $port) {
+    $this->username = $username;
+    $this->password = $password;
+    $this->hostname = $hostname;
+    $this->port = $port;
+    parent::__construct($jail);
+  }
+
+  /**
+   * Return an object which can implement the FTP protocol.
+   *
+   * @param string $jail
+   * @param array $settings
+   * @return FileTransferFTP
+   *   The appropriate FileTransferFTP subclass based on the available
+   *   options. If the FTP PHP extension is available, use it.
+   */
+  static function factory($jail, $settings) {
+    $username = empty($settings['username']) ? '' : $settings['username'];
+    $password = empty($settings['password']) ? '' : $settings['password'];
+    $hostname = empty($settings['advanced']['hostname']) ? 'localhost' : $settings['advanced']['hostname'];
+    $port = empty($settings['advanced']['port']) ? 21 : $settings['advanced']['port'];
+
+    if (function_exists('ftp_connect')) {
+      $class = 'FileTransferFTPExtension';
+    }
+    else {
+      throw new FileTransferException('No FTP backend available.');
+    }
+
+    return new $class($jail, $username, $password, $hostname, $port);
+  }
+
+  /**
+   * Returns the form to configure the FileTransfer class for FTP.
+   */
+  public function getSettingsForm() {
+    $form = parent::getSettingsForm();
+    $form['advanced']['port']['#default_value'] = 21;
+    return $form;
+  }
+}
+
+class FileTransferFTPExtension extends FileTransferFTP implements FileTransferChmodInterface {
+
+  public function connect() {
+    $this->connection = ftp_connect($this->hostname, $this->port);
+
+    if (!$this->connection) {
+      throw new FileTransferException("Cannot connect to FTP Server, check settings");
+    }
+    if (!ftp_login($this->connection, $this->username, $this->password)) {
+      throw new FileTransferException("Cannot log in to FTP server. Check username and password");
+    }
+  }
+
+  protected function copyFileJailed($source, $destination) {
+    if (!@ftp_put($this->connection,  $destination, $source, FTP_BINARY)) {
+      throw new FileTransferException("Cannot move @source to @destination", NULL, array("@source" => $source, "@destination" => $destination));
+    }
+  }
+
+  protected function createDirectoryJailed($directory) {
+    if (!ftp_mkdir($this->connection, $directory)) {
+      throw new FileTransferException("Cannot create directory @directory", NULL, array("@directory" => $directory));
+    }
+  }
+
+  protected function removeDirectoryJailed($directory) {
+    $pwd = ftp_pwd($this->connection);
+    if (!ftp_chdir($this->connection, $directory)) {
+      throw new FileTransferException("Unable to change to directory @directory", NULL, array('@directory' => $directory));
+    }
+    $list = @ftp_nlist($this->connection, '.');
+    if (!$list) {
+      $list = array();
+    }
+    foreach ($list as $item){
+      if ($item == '.' || $item == '..') {
+        continue;
+      }
+      if (@ftp_chdir($this->connection, $item)){
+        ftp_cdup($this->connection);
+        $this->removeDirectory(ftp_pwd($this->connection) . '/' . $item);
+      }
+      else {
+        $this->removeFile(ftp_pwd($this->connection) . '/' . $item);
+      }
+    }
+    ftp_chdir($this->connection, $pwd);
+    if (!ftp_rmdir($this->connection, $directory)) {
+      throw new FileTransferException("Unable to remove to directory @directory", NULL, array('@directory' => $directory));
+    }
+  }
+
+  protected function removeFileJailed($destination) {
+    if (!ftp_delete($this->connection, $destination)) {
+      throw new FileTransferException("Unable to remove to file @file", NULL, array('@file' => $destination));
+    }
+  }
+
+  public function isDirectory($path) {
+    $result = FALSE;
+    $curr = ftp_pwd($this->connection);
+    if (@ftp_chdir($this->connection, $path)) {
+      $result = TRUE;
+    }
+    ftp_chdir($this->connection, $curr);
+    return $result;
+  }
+
+  public function isFile($path) {
+    return ftp_size($this->connection, $path) != -1;
+  }
+
+  function chmodJailed($path, $mode, $recursive) {
+    if (!ftp_chmod($this->connection, $mode, $path)) {
+      throw new FileTransferException("Unable to set permissions on %file", NULL, array ('%file' => $path));
+    }
+    if ($this->isDirectory($path) && $recursive) {
+      $filelist = @ftp_nlist($this->connection, $path);
+      if (!$filelist) {
+        //empty directory - returns false
+        return;
+      }
+      foreach ($filelist as $file) {
+        $this->chmodJailed($file, $mode, $recursive);
+      }
+    }
+  }
+}
+
+if (!function_exists('ftp_chmod')) {
+  function ftp_chmod($ftp_stream, $mode, $filename) {
+    return ftp_site($ftp_stream, sprintf('CHMOD %o %s', $mode, $filename));
+  }
+}

+ 76 - 0
includes/filetransfer/local.inc

@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * The local connection class for copying files as the httpd user.
+ */
+class FileTransferLocal extends FileTransfer implements FileTransferChmodInterface {
+
+  function connect() {
+    // No-op
+  }
+
+  static function factory($jail, $settings) {
+    return new FileTransferLocal($jail);
+  }
+
+  protected function copyFileJailed($source, $destination) {
+    if (@!copy($source, $destination)) {
+      throw new FileTransferException('Cannot copy %source to %destination.', NULL, array('%source' => $source, '%destination' => $destination));
+    }
+  }
+
+  protected function createDirectoryJailed($directory) {
+    if (!is_dir($directory) && @!mkdir($directory, 0777, TRUE)) {
+      throw new FileTransferException('Cannot create directory %directory.', NULL, array('%directory' => $directory));
+    }
+  }
+
+  protected function removeDirectoryJailed($directory) {
+    if (!is_dir($directory)) {
+      // Programmer error assertion, not something we expect users to see.
+      throw new FileTransferException('removeDirectoryJailed() called with a path (%directory) that is not a directory.', NULL, array('%directory' => $directory));
+    }
+    foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($directory), RecursiveIteratorIterator::CHILD_FIRST) as $filename => $file) {
+      if ($file->isDir()) {
+        if (@!drupal_rmdir($filename)) {
+          throw new FileTransferException('Cannot remove directory %directory.', NULL, array('%directory' => $filename));
+        }
+      }
+      elseif ($file->isFile()) {
+        if (@!drupal_unlink($filename)) {
+          throw new FileTransferException('Cannot remove file %file.', NULL, array('%file' => $filename));
+        }
+      }
+    }
+    if (@!drupal_rmdir($directory)) {
+      throw new FileTransferException('Cannot remove directory %directory.', NULL, array('%directory' => $directory));
+    }
+  }
+
+  protected function removeFileJailed($file) {
+    if (@!drupal_unlink($file)) {
+      throw new FileTransferException('Cannot remove file %file.', NULL, array('%file' => $file));
+    }
+  }
+
+  public function isDirectory($path) {
+    return is_dir($path);
+  }
+
+  public function isFile($path) {
+    return is_file($path);
+  }
+
+  public function chmodJailed($path, $mode, $recursive) {
+    if ($recursive && is_dir($path)) {
+      foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
+        if (@!chmod($filename, $mode)) {
+          throw new FileTransferException('Cannot chmod %path.', NULL, array('%path' => $filename));
+        }
+      }
+    }
+    elseif (@!chmod($path, $mode)) {
+      throw new FileTransferException('Cannot chmod %path.', NULL, array('%path' => $path));
+    }
+  }
+}

+ 108 - 0
includes/filetransfer/ssh.inc

@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * The SSH connection class for the update module.
+ */
+class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface {
+
+  function __construct($jail, $username, $password, $hostname = "localhost", $port = 22) {
+    $this->username = $username;
+    $this->password = $password;
+    $this->hostname = $hostname;
+    $this->port = $port;
+    parent::__construct($jail);
+  }
+
+  function connect() {
+    $this->connection = @ssh2_connect($this->hostname, $this->port);
+    if (!$this->connection) {
+      throw new FileTransferException('SSH Connection failed to @host:@port', NULL, array('@host' => $this->hostname, '@port' => $this->port));
+    }
+    if (!@ssh2_auth_password($this->connection, $this->username, $this->password)) {
+      throw new FileTransferException('The supplied username/password combination was not accepted.');
+    }
+  }
+
+  static function factory($jail, $settings) {
+    $username = empty($settings['username']) ? '' : $settings['username'];
+    $password = empty($settings['password']) ? '' : $settings['password'];
+    $hostname = empty($settings['advanced']['hostname']) ? 'localhost' : $settings['advanced']['hostname'];
+    $port = empty($settings['advanced']['port']) ? 22 : $settings['advanced']['port'];
+    return new FileTransferSSH($jail, $username, $password, $hostname, $port);
+  }
+
+  protected function copyFileJailed($source, $destination) {
+    if (!@ssh2_scp_send($this->connection, $source, $destination)) {
+      throw new FileTransferException('Cannot copy @source_file to @destination_file.', NULL, array('@source' => $source, '@destination' => $destination));
+    }
+  }
+
+  protected function copyDirectoryJailed($source, $destination) {
+    if (@!ssh2_exec($this->connection, 'cp -Rp ' . escapeshellarg($source) . ' ' . escapeshellarg($destination))) {
+      throw new FileTransferException('Cannot copy directory @directory.', NULL, array('@directory' => $source));
+    }
+  }
+
+  protected function createDirectoryJailed($directory) {
+    if (@!ssh2_exec($this->connection, 'mkdir ' . escapeshellarg($directory))) {
+      throw new FileTransferException('Cannot create directory @directory.', NULL, array('@directory' => $directory));
+    }
+  }
+
+  protected function removeDirectoryJailed($directory) {
+    if (@!ssh2_exec($this->connection, 'rm -Rf ' . escapeshellarg($directory))) {
+      throw new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $directory));
+    }
+  }
+
+  protected function removeFileJailed($destination) {
+    if (!@ssh2_exec($this->connection, 'rm ' . escapeshellarg($destination))) {
+      throw new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $destination));
+    }
+  }
+
+  /**
+   * WARNING: This is untested.  It is not currently used, but should do the trick.
+   */
+  public function isDirectory($path) {
+    $directory = escapeshellarg($path);
+    $cmd = "[ -d {$directory} ] && echo 'yes'";
+    if ($output = @ssh2_exec($this->connection, $cmd)) {
+      if ($output == 'yes') {
+        return TRUE;
+      }
+      return FALSE;
+    } else {
+      throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
+    }
+  }
+
+  public function isFile($path) {
+    $file = escapeshellarg($path);
+    $cmd = "[ -f {$file} ] && echo 'yes'";
+    if ($output = @ssh2_exec($this->connection, $cmd)) {
+      if ($output == 'yes') {
+        return TRUE;
+      }
+      return FALSE;
+    } else {
+      throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
+    }
+  }
+
+  function chmodJailed($path, $mode, $recursive) {
+    $cmd = sprintf("chmod %s%o %s", $recursive ? '-R ' : '', $mode, escapeshellarg($path));
+    if (@!ssh2_exec($this->connection, $cmd)) {
+      throw new FileTransferException('Cannot change permissions of @path.', NULL, array('@path' => $path));
+    }
+  }
+
+  /**
+   * Returns the form to configure the FileTransfer class for SSH.
+   */
+  public function getSettingsForm() {
+    $form = parent::getSettingsForm();
+    $form['advanced']['port']['#default_value'] = 22;
+    return $form;
+  }
+}

+ 4603 - 0
includes/form.inc

@@ -0,0 +1,4603 @@
+<?php
+ /**
+ * @file
+ * Functions for form and batch generation and processing.
+ */
+
+/**
+ * @defgroup forms Form builder functions
+ * @{
+ * Functions that build an abstract representation of a HTML form.
+ *
+ * All modules should declare their form builder functions to be in this
+ * group and each builder function should reference its validate and submit
+ * functions using \@see. Conversely, validate and submit functions should
+ * reference the form builder function using \@see. For examples, of this see
+ * system_modules_uninstall() or user_pass(), the latter of which has the
+ * following in its doxygen documentation:
+ *
+ * \@ingroup forms
+ * \@see user_pass_validate().
+ * \@see user_pass_submit().
+ *
+ * @}
+ */
+
+/**
+ * @defgroup form_api Form generation
+ * @{
+ * Functions to enable the processing and display of HTML forms.
+ *
+ * Drupal uses these functions to achieve consistency in its form processing and
+ * presentation, while simplifying code and reducing the amount of HTML that
+ * must be explicitly generated by modules.
+ *
+ * The primary function used with forms is drupal_get_form(), which is
+ * used for forms presented interactively to a user. Forms can also be built and
+ * submitted programmatically without any user input using the
+ * drupal_form_submit() function.
+ *
+ * drupal_get_form() handles retrieving, processing, and displaying a rendered
+ * HTML form for modules automatically.
+ *
+ * Here is an example of how to use drupal_get_form() and a form builder
+ * function:
+ * @code
+ * $form = drupal_get_form('my_module_example_form');
+ * ...
+ * function my_module_example_form($form, &$form_state) {
+ *   $form['submit'] = array(
+ *     '#type' => 'submit',
+ *     '#value' => t('Submit'),
+ *   );
+ *   return $form;
+ * }
+ * function my_module_example_form_validate($form, &$form_state) {
+ *   // Validation logic.
+ * }
+ * function my_module_example_form_submit($form, &$form_state) {
+ *   // Submission logic.
+ * }
+ * @endcode
+ *
+ * Or with any number of additional arguments:
+ * @code
+ * $extra = "extra";
+ * $form = drupal_get_form('my_module_example_form', $extra);
+ * ...
+ * function my_module_example_form($form, &$form_state, $extra) {
+ *   $form['submit'] = array(
+ *     '#type' => 'submit',
+ *     '#value' => $extra,
+ *   );
+ *   return $form;
+ * }
+ * @endcode
+ *
+ * The $form argument to form-related functions is a structured array containing
+ * the elements and properties of the form. For information on the array
+ * components and format, and more detailed explanations of the Form API
+ * workflow, see the
+ * @link forms_api_reference.html Form API reference @endlink
+ * and the
+ * @link http://drupal.org/node/37775 Form API documentation section. @endlink
+ * In addition, there is a set of Form API tutorials in
+ * @link form_example_tutorial.inc the Form Example Tutorial @endlink which
+ * provide basics all the way up through multistep forms.
+ *
+ * In the form builder, validation, submission, and other form functions,
+ * $form_state is the primary influence on the processing of the form and is
+ * passed by reference to most functions, so they use it to communicate with
+ * the form system and each other.
+ *
+ * See drupal_build_form() for documentation of $form_state keys.
+ */
+
+/**
+ * Returns a renderable form array for a given form ID.
+ *
+ * This function should be used instead of drupal_build_form() when $form_state
+ * is not needed (i.e., when initially rendering the form) and is often
+ * used as a menu callback.
+ *
+ * @param $form_id
+ *   The unique string identifying the desired form. If a function with that
+ *   name exists, it is called to build the form array. Modules that need to
+ *   generate the same form (or very similar forms) using different $form_ids
+ *   can implement hook_forms(), which maps different $form_id values to the
+ *   proper form constructor function. Examples may be found in node_forms(),
+ *   and search_forms().
+ * @param ...
+ *   Any additional arguments are passed on to the functions called by
+ *   drupal_get_form(), including the unique form constructor function. For
+ *   example, the node_edit form requires that a node object is passed in here
+ *   when it is called. These are available to implementations of
+ *   hook_form_alter() and hook_form_FORM_ID_alter() as the array
+ *   $form_state['build_info']['args'].
+ *
+ * @return
+ *   The form array.
+ *
+ * @see drupal_build_form()
+ */
+function drupal_get_form($form_id) {
+  $form_state = array();
+
+  $args = func_get_args();
+  // Remove $form_id from the arguments.
+  array_shift($args);
+  $form_state['build_info']['args'] = $args;
+
+  return drupal_build_form($form_id, $form_state);
+}
+
+/**
+ * Builds and process a form based on a form id.
+ *
+ * The form may also be retrieved from the cache if the form was built in a
+ * previous page-load. The form is then passed on for processing, validation
+ * and submission if there is proper input.
+ *
+ * @param $form_id
+ *   The unique string identifying the desired form. If a function with that
+ *   name exists, it is called to build the form array. Modules that need to
+ *   generate the same form (or very similar forms) using different $form_ids
+ *   can implement hook_forms(), which maps different $form_id values to the
+ *   proper form constructor function. Examples may be found in node_forms(),
+ *   and search_forms().
+ * @param $form_state
+ *   An array which stores information about the form. This is passed as a
+ *   reference so that the caller can use it to examine what in the form changed
+ *   when the form submission process is complete. Furthermore, it may be used
+ *   to store information related to the processed data in the form, which will
+ *   persist across page requests when the 'cache' or 'rebuild' flag is set.
+ *   The following parameters may be set in $form_state to affect how the form
+ *   is rendered:
+ *   - build_info: Internal. An associative array of information stored by Form
+ *     API that is necessary to build and rebuild the form from cache when the
+ *     original context may no longer be available:
+ *     - args: A list of arguments to pass to the form constructor.
+ *     - files: An optional array defining include files that need to be loaded
+ *       for building the form. Each array entry may be the path to a file or
+ *       another array containing values for the parameters 'type', 'module' and
+ *       'name' as needed by module_load_include(). The files listed here are
+ *       automatically loaded by form_get_cache(). By default the current menu
+ *       router item's 'file' definition is added, if any. Use
+ *       form_load_include() to add include files from a form constructor.
+ *     - form_id: Identification of the primary form being constructed and
+ *       processed.
+ *     - base_form_id: Identification for a base form, as declared in a
+ *       hook_forms() implementation.
+ *   - rebuild_info: Internal. Similar to 'build_info', but pertaining to
+ *     drupal_rebuild_form().
+ *   - rebuild: Normally, after the entire form processing is completed and
+ *     submit handlers have run, a form is considered to be done and
+ *     drupal_redirect_form() will redirect the user to a new page using a GET
+ *     request (so a browser refresh does not re-submit the form). However, if
+ *     'rebuild' has been set to TRUE, then a new copy of the form is
+ *     immediately built and sent to the browser, instead of a redirect. This is
+ *     used for multi-step forms, such as wizards and confirmation forms.
+ *     Normally, $form_state['rebuild'] is set by a submit handler, since it is
+ *     usually logic within a submit handler that determines whether a form is
+ *     done or requires another step. However, a validation handler may already
+ *     set $form_state['rebuild'] to cause the form processing to bypass submit
+ *     handlers and rebuild the form instead, even if there are no validation
+ *     errors.
+ *   - redirect: Used to redirect the form on submission. It may either be a
+ *     string containing the destination URL, or an array of arguments
+ *     compatible with drupal_goto(). See drupal_redirect_form() for complete
+ *     information.
+ *   - no_redirect: If set to TRUE the form will NOT perform a drupal_goto(),
+ *     even if 'redirect' is set.
+ *   - method: The HTTP form method to use for finding the input for this form.
+ *     May be 'post' or 'get'. Defaults to 'post'. Note that 'get' method
+ *     forms do not use form ids so are always considered to be submitted, which
+ *     can have unexpected effects. The 'get' method should only be used on
+ *     forms that do not change data, as that is exclusively the domain of
+ *     'post.'
+ *   - cache: If set to TRUE the original, unprocessed form structure will be
+ *     cached, which allows the entire form to be rebuilt from cache. A typical
+ *     form workflow involves two page requests; first, a form is built and
+ *     rendered for the user to fill in. Then, the user fills the form in and
+ *     submits it, triggering a second page request in which the form must be
+ *     built and processed. By default, $form and $form_state are built from
+ *     scratch during each of these page requests. Often, it is necessary or
+ *     desired to persist the $form and $form_state variables from the initial
+ *     page request to the one that processes the submission. 'cache' can be set
+ *     to TRUE to do this. A prominent example is an Ajax-enabled form, in which
+ *     ajax_process_form() enables form caching for all forms that include an
+ *     element with the #ajax property. (The Ajax handler has no way to build
+ *     the form itself, so must rely on the cached version.) Note that the
+ *     persistence of $form and $form_state happens automatically for
+ *     (multi-step) forms having the 'rebuild' flag set, regardless of the value
+ *     for 'cache'.
+ *   - no_cache: If set to TRUE the form will NOT be cached, even if 'cache' is
+ *     set.
+ *   - values: An associative array of values submitted to the form. The
+ *     validation functions and submit functions use this array for nearly all
+ *     their decision making. (Note that #tree determines whether the values are
+ *     a flat array or an array whose structure parallels the $form array. See
+ *     @link forms_api_reference.html Form API reference @endlink for more
+ *     information.) These are raw and unvalidated, so should not be used
+ *     without a thorough understanding of security implications. In almost all
+ *     cases, code should use the data in the 'values' array exclusively. The
+ *     most common use of this key is for multi-step forms that need to clear
+ *     some of the user input when setting 'rebuild'. The values correspond to
+ *     $_POST or $_GET, depending on the 'method' chosen.
+ *   - always_process: If TRUE and the method is GET, a form_id is not
+ *     necessary. This should only be used on RESTful GET forms that do NOT
+ *     write data, as this could lead to security issues. It is useful so that
+ *     searches do not need to have a form_id in their query arguments to
+ *     trigger the search.
+ *   - must_validate: Ordinarily, a form is only validated once, but there are
+ *     times when a form is resubmitted internally and should be validated
+ *     again. Setting this to TRUE will force that to happen. This is most
+ *     likely to occur during Ajax operations.
+ *   - programmed: If TRUE, the form was submitted programmatically, usually
+ *     invoked via drupal_form_submit(). Defaults to FALSE.
+ *   - process_input: Boolean flag. TRUE signifies correct form submission.
+ *     This is always TRUE for programmed forms coming from drupal_form_submit()
+ *     (see 'programmed' key), or if the form_id coming from the $_POST data is
+ *     set and matches the current form_id.
+ *   - submitted: If TRUE, the form has been submitted. Defaults to FALSE.
+ *   - executed: If TRUE, the form was submitted and has been processed and
+ *     executed. Defaults to FALSE.
+ *   - triggering_element: (read-only) The form element that triggered
+ *     submission. This is the same as the deprecated
+ *     $form_state['clicked_button']. It is the element that caused submission,
+ *     which may or may not be a button (in the case of Ajax forms). This key is
+ *     often used to distinguish between various buttons in a submit handler,
+ *     and is also used in Ajax handlers.
+ *   - clicked_button: Deprecated. Use triggering_element instead.
+ *   - has_file_element: Internal. If TRUE, there is a file element and Form API
+ *     will set the appropriate 'enctype' HTML attribute on the form.
+ *   - groups: Internal. An array containing references to fieldsets to render
+ *     them within vertical tabs.
+ *   - storage: $form_state['storage'] is not a special key, and no specific
+ *     support is provided for it in the Form API. By tradition it was
+ *     the location where application-specific data was stored for communication
+ *     between the submit, validation, and form builder functions, especially
+ *     in a multi-step-style form. Form implementations may use any key(s)
+ *     within $form_state (other than the keys listed here and other reserved
+ *     ones used by Form API internals) for this kind of storage. The
+ *     recommended way to ensure that the chosen key doesn't conflict with ones
+ *     used by the Form API or other modules is to use the module name as the
+ *     key name or a prefix for the key name. For example, the Node module uses
+ *     $form_state['node'] in node editing forms to store information about the
+ *     node being edited, and this information stays available across successive
+ *     clicks of the "Preview" button as well as when the "Save" button is
+ *     finally clicked.
+ *   - buttons: A list containing copies of all submit and button elements in
+ *     the form.
+ *   - complete form: A reference to the $form variable containing the complete
+ *     form structure. #process, #after_build, #element_validate, and other
+ *     handlers being invoked on a form element may use this reference to access
+ *     other information in the form the element is contained in.
+ *   - temporary: An array holding temporary data accessible during the current
+ *     page request only. All $form_state properties that are not reserved keys
+ *     (see form_state_keys_no_cache()) persist throughout a multistep form
+ *     sequence. Form API provides this key for modules to communicate
+ *     information across form-related functions during a single page request.
+ *     It may be used to temporarily save data that does not need to or should
+ *     not be cached during the whole form workflow; e.g., data that needs to be
+ *     accessed during the current form build process only. There is no use-case
+ *     for this functionality in Drupal core.
+ *   - wrapper_callback: Modules that wish to pre-populate certain forms with
+ *     common elements, such as back/next/save buttons in multi-step form
+ *     wizards, may define a form builder function name that returns a form
+ *     structure, which is passed on to the actual form builder function.
+ *     Such implementations may either define the 'wrapper_callback' via
+ *     hook_forms() or have to invoke drupal_build_form() (instead of
+ *     drupal_get_form()) on their own in a custom menu callback to prepare
+ *     $form_state accordingly.
+ *   Information on how certain $form_state properties control redirection
+ *   behavior after form submission may be found in drupal_redirect_form().
+ *
+ * @return
+ *   The rendered form. This function may also perform a redirect and hence may
+ *   not return at all, depending upon the $form_state flags that were set.
+ *
+ * @see drupal_redirect_form()
+ */
+function drupal_build_form($form_id, &$form_state) {
+  // Ensure some defaults; if already set they will not be overridden.
+  $form_state += form_state_defaults();
+
+  if (!isset($form_state['input'])) {
+    $form_state['input'] = $form_state['method'] == 'get' ? $_GET : $_POST;
+  }
+
+  if (isset($_SESSION['batch_form_state'])) {
+    // We've been redirected here after a batch processing. The form has
+    // already been processed, but needs to be rebuilt. See _batch_finished().
+    $form_state = $_SESSION['batch_form_state'];
+    unset($_SESSION['batch_form_state']);
+    return drupal_rebuild_form($form_id, $form_state);
+  }
+
+  // If the incoming input contains a form_build_id, we'll check the cache for a
+  // copy of the form in question. If it's there, we don't have to rebuild the
+  // form to proceed. In addition, if there is stored form_state data from a
+  // previous step, we'll retrieve it so it can be passed on to the form
+  // processing code.
+  $check_cache = isset($form_state['input']['form_id']) && $form_state['input']['form_id'] == $form_id && !empty($form_state['input']['form_build_id']);
+  if ($check_cache) {
+    $form = form_get_cache($form_state['input']['form_build_id'], $form_state);
+  }
+
+  // If the previous bit of code didn't result in a populated $form object, we
+  // are hitting the form for the first time and we need to build it from
+  // scratch.
+  if (!isset($form)) {
+    // If we attempted to serve the form from cache, uncacheable $form_state
+    // keys need to be removed after retrieving and preparing the form, except
+    // any that were already set prior to retrieving the form.
+    if ($check_cache) {
+      $form_state_before_retrieval = $form_state;
+    }
+
+    $form = drupal_retrieve_form($form_id, $form_state);
+    drupal_prepare_form($form_id, $form, $form_state);
+
+    // form_set_cache() removes uncacheable $form_state keys defined in
+    // form_state_keys_no_cache() in order for multi-step forms to work
+    // properly. This means that form processing logic for single-step forms
+    // using $form_state['cache'] may depend on data stored in those keys
+    // during drupal_retrieve_form()/drupal_prepare_form(), but form
+    // processing should not depend on whether the form is cached or not, so
+    // $form_state is adjusted to match what it would be after a
+    // form_set_cache()/form_get_cache() sequence. These exceptions are
+    // allowed to survive here:
+    // - always_process: Does not make sense in conjunction with form caching
+    //   in the first place, since passing form_build_id as a GET parameter is
+    //   not desired.
+    // - temporary: Any assigned data is expected to survives within the same
+    //   page request.
+    if ($check_cache) {
+      $uncacheable_keys = array_flip(array_diff(form_state_keys_no_cache(), array('always_process', 'temporary')));
+      $form_state = array_diff_key($form_state, $uncacheable_keys);
+      $form_state += $form_state_before_retrieval;
+    }
+  }
+
+  // Now that we have a constructed form, process it. This is where:
+  // - Element #process functions get called to further refine $form.
+  // - User input, if any, gets incorporated in the #value property of the
+  //   corresponding elements and into $form_state['values'].
+  // - Validation and submission handlers are called.
+  // - If this submission is part of a multistep workflow, the form is rebuilt
+  //   to contain the information of the next step.
+  // - If necessary, the form and form state are cached or re-cached, so that
+  //   appropriate information persists to the next page request.
+  // All of the handlers in the pipeline receive $form_state by reference and
+  // can use it to know or update information about the state of the form.
+  drupal_process_form($form_id, $form, $form_state);
+
+  // If this was a successful submission of a single-step form or the last step
+  // of a multi-step form, then drupal_process_form() issued a redirect to
+  // another page, or back to this page, but as a new request. Therefore, if
+  // we're here, it means that this is either a form being viewed initially
+  // before any user input, or there was a validation error requiring the form
+  // to be re-displayed, or we're in a multi-step workflow and need to display
+  // the form's next step. In any case, we have what we need in $form, and can
+  // return it for rendering.
+  return $form;
+}
+
+/**
+ * Retrieves default values for the $form_state array.
+ */
+function form_state_defaults() {
+  return array(
+    'rebuild' => FALSE,
+    'rebuild_info' => array(),
+    'redirect' => NULL,
+    // @todo 'args' is usually set, so no other default 'build_info' keys are
+    //   appended via += form_state_defaults().
+    'build_info' => array(
+      'args' => array(),
+      'files' => array(),
+    ),
+    'temporary' => array(),
+    'submitted' => FALSE,
+    'executed' => FALSE,
+    'programmed' => FALSE,
+    'cache'=> FALSE,
+    'method' => 'post',
+    'groups' => array(),
+    'buttons' => array(),
+  );
+}
+
+/**
+ * Constructs a new $form from the information in $form_state.
+ *
+ * This is the key function for making multi-step forms advance from step to
+ * step. It is called by drupal_process_form() when all user input processing,
+ * including calling validation and submission handlers, for the request is
+ * finished. If a validate or submit handler set $form_state['rebuild'] to TRUE,
+ * and if other conditions don't preempt a rebuild from happening, then this
+ * function is called to generate a new $form, the next step in the form
+ * workflow, to be returned for rendering.
+ *
+ * Ajax form submissions are almost always multi-step workflows, so that is one
+ * common use-case during which form rebuilding occurs. See ajax_form_callback()
+ * for more information about creating Ajax-enabled forms.
+ *
+ * @param $form_id
+ *   The unique string identifying the desired form. If a function
+ *   with that name exists, it is called to build the form array.
+ *   Modules that need to generate the same form (or very similar forms)
+ *   using different $form_ids can implement hook_forms(), which maps
+ *   different $form_id values to the proper form constructor function. Examples
+ *   may be found in node_forms() and search_forms().
+ * @param $form_state
+ *   A keyed array containing the current state of the form.
+ * @param $old_form
+ *   (optional) A previously built $form. Used to retain the #build_id and
+ *   #action properties in Ajax callbacks and similar partial form rebuilds. The
+ *   only properties copied from $old_form are the ones which both exist in
+ *   $old_form and for which $form_state['rebuild_info']['copy'][PROPERTY] is
+ *   TRUE. If $old_form is not passed, the entire $form is rebuilt freshly.
+ *   'rebuild_info' needs to be a separate top-level property next to
+ *   'build_info', since the contained data must not be cached.
+ *
+ * @return
+ *   The newly built form.
+ *
+ * @see drupal_process_form()
+ * @see ajax_form_callback()
+ */
+function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
+  $form = drupal_retrieve_form($form_id, $form_state);
+
+  // If only parts of the form will be returned to the browser (e.g., Ajax or
+  // RIA clients), re-use the old #build_id to not require client-side code to
+  // manually update the hidden 'build_id' input element.
+  // Otherwise, a new #build_id is generated, to not clobber the previous
+  // build's data in the form cache; also allowing the user to go back to an
+  // earlier build, make changes, and re-submit.
+  // @see drupal_prepare_form()
+  if (isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id'])) {
+    $form['#build_id'] = $old_form['#build_id'];
+  }
+  else {
+    $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
+  }
+
+  // #action defaults to request_uri(), but in case of Ajax and other partial
+  // rebuilds, the form is submitted to an alternate URL, and the original
+  // #action needs to be retained.
+  if (isset($old_form['#action']) && !empty($form_state['rebuild_info']['copy']['#action'])) {
+    $form['#action'] = $old_form['#action'];
+  }
+
+  drupal_prepare_form($form_id, $form, $form_state);
+
+  // Caching is normally done in drupal_process_form(), but what needs to be
+  // cached is the $form structure before it passes through form_builder(),
+  // so we need to do it here.
+  // @todo For Drupal 8, find a way to avoid this code duplication.
+  if (empty($form_state['no_cache'])) {
+    form_set_cache($form['#build_id'], $form, $form_state);
+  }
+
+  // Clear out all group associations as these might be different when
+  // re-rendering the form.
+  $form_state['groups'] = array();
+
+  // Return a fully built form that is ready for rendering.
+  return form_builder($form_id, $form, $form_state);
+}
+
+/**
+ * Fetches a form from cache.
+ */
+function form_get_cache($form_build_id, &$form_state) {
+  if ($cached = cache_get('form_' . $form_build_id, 'cache_form')) {
+    $form = $cached->data;
+
+    global $user;
+    if ((isset($form['#cache_token']) && drupal_valid_token($form['#cache_token'])) || (!isset($form['#cache_token']) && !$user->uid)) {
+      if ($cached = cache_get('form_state_' . $form_build_id, 'cache_form')) {
+        // Re-populate $form_state for subsequent rebuilds.
+        $form_state = $cached->data + $form_state;
+
+        // If the original form is contained in include files, load the files.
+        // @see form_load_include()
+        $form_state['build_info'] += array('files' => array());
+        foreach ($form_state['build_info']['files'] as $file) {
+          if (is_array($file)) {
+            $file += array('type' => 'inc', 'name' => $file['module']);
+            module_load_include($file['type'], $file['module'], $file['name']);
+          }
+          elseif (file_exists($file)) {
+            require_once DRUPAL_ROOT . '/' . $file;
+          }
+        }
+      }
+      return $form;
+    }
+  }
+}
+
+/**
+ * Stores a form in the cache.
+ */
+function form_set_cache($form_build_id, $form, $form_state) {
+  // 6 hours cache life time for forms should be plenty.
+  $expire = 21600;
+
+  // Cache form structure.
+  if (isset($form)) {
+    if ($GLOBALS['user']->uid) {
+      $form['#cache_token'] = drupal_get_token();
+    }
+    cache_set('form_' . $form_build_id, $form, 'cache_form', REQUEST_TIME + $expire);
+  }
+
+  // Cache form state.
+  if ($data = array_diff_key($form_state, array_flip(form_state_keys_no_cache()))) {
+    cache_set('form_state_' . $form_build_id, $data, 'cache_form', REQUEST_TIME + $expire);
+  }
+}
+
+/**
+ * Returns an array of $form_state keys that shouldn't be cached.
+ */
+function form_state_keys_no_cache() {
+  return array(
+    // Public properties defined by form constructors and form handlers.
+    'always_process',
+    'must_validate',
+    'rebuild',
+    'rebuild_info',
+    'redirect',
+    'no_redirect',
+    'temporary',
+    // Internal properties defined by form processing.
+    'buttons',
+    'triggering_element',
+    'clicked_button',
+    'complete form',
+    'groups',
+    'input',
+    'method',
+    'submit_handlers',
+    'submitted',
+    'executed',
+    'validate_handlers',
+    'values',
+  );
+}
+
+/**
+ * Ensures an include file is loaded whenever the form is processed.
+ *
+ * Example:
+ * @code
+ *   // Load node.admin.inc from Node module.
+ *   form_load_include($form_state, 'inc', 'node', 'node.admin');
+ * @endcode
+ *
+ * Use this function instead of module_load_include() from inside a form
+ * constructor or any form processing logic as it ensures that the include file
+ * is loaded whenever the form is processed. In contrast to using
+ * module_load_include() directly, form_load_include() makes sure the include
+ * file is correctly loaded also if the form is cached.
+ *
+ * @param $form_state
+ *   The current state of the form.
+ * @param $type
+ *   The include file's type (file extension).
+ * @param $module
+ *   The module to which the include file belongs.
+ * @param $name
+ *   (optional) The base file name (without the $type extension). If omitted,
+ *   $module is used; i.e., resulting in "$module.$type" by default.
+ *
+ * @return
+ *   The filepath of the loaded include file, or FALSE if the include file was
+ *   not found or has been loaded already.
+ *
+ * @see module_load_include()
+ */
+function form_load_include(&$form_state, $type, $module, $name = NULL) {
+  if (!isset($name)) {
+    $name = $module;
+  }
+  if (!isset($form_state['build_info']['files']["$module:$name.$type"])) {
+    // Only add successfully included files to the form state.
+    if ($result = module_load_include($type, $module, $name)) {
+      $form_state['build_info']['files']["$module:$name.$type"] = array(
+        'type' => $type,
+        'module' => $module,
+        'name' => $name,
+      );
+      return $result;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Retrieves, populates, and processes a form.
+ *
+ * This function allows you to supply values for form elements and submit a
+ * form for processing. Compare to drupal_get_form(), which also builds and
+ * processes a form, but does not allow you to supply values.
+ *
+ * There is no return value, but you can check to see if there are errors
+ * by calling form_get_errors().
+ *
+ * @param $form_id
+ *   The unique string identifying the desired form. If a function
+ *   with that name exists, it is called to build the form array.
+ *   Modules that need to generate the same form (or very similar forms)
+ *   using different $form_ids can implement hook_forms(), which maps
+ *   different $form_id values to the proper form constructor function. Examples
+ *   may be found in node_forms() and search_forms().
+ * @param $form_state
+ *   A keyed array containing the current state of the form. Most important is
+ *   the $form_state['values'] collection, a tree of data used to simulate the
+ *   incoming $_POST information from a user's form submission. If a key is not
+ *   filled in $form_state['values'], then the default value of the respective
+ *   element is used. To submit an unchecked checkbox or other control that
+ *   browsers submit by not having a $_POST entry, include the key, but set the
+ *   value to NULL.
+ * @param ...
+ *   Any additional arguments are passed on to the functions called by
+ *   drupal_form_submit(), including the unique form constructor function.
+ *   For example, the node_edit form requires that a node object be passed
+ *   in here when it is called. Arguments that need to be passed by reference
+ *   should not be included here, but rather placed directly in the $form_state
+ *   build info array so that the reference can be preserved. For example, a
+ *   form builder function with the following signature:
+ *   @code
+ *   function mymodule_form($form, &$form_state, &$object) {
+ *   }
+ *   @endcode
+ *   would be called via drupal_form_submit() as follows:
+ *   @code
+ *   $form_state['values'] = $my_form_values;
+ *   $form_state['build_info']['args'] = array(&$object);
+ *   drupal_form_submit('mymodule_form', $form_state);
+ *   @endcode
+ * For example:
+ * @code
+ * // register a new user
+ * $form_state = array();
+ * $form_state['values']['name'] = 'robo-user';
+ * $form_state['values']['mail'] = 'robouser@example.com';
+ * $form_state['values']['pass']['pass1'] = 'password';
+ * $form_state['values']['pass']['pass2'] = 'password';
+ * $form_state['values']['op'] = t('Create new account');
+ * drupal_form_submit('user_register_form', $form_state);
+ * @endcode
+ */
+function drupal_form_submit($form_id, &$form_state) {
+  if (!isset($form_state['build_info']['args'])) {
+    $args = func_get_args();
+    array_shift($args);
+    array_shift($args);
+    $form_state['build_info']['args'] = $args;
+  }
+  // Merge in default values.
+  $form_state += form_state_defaults();
+
+  // Populate $form_state['input'] with the submitted values before retrieving
+  // the form, to be consistent with what drupal_build_form() does for
+  // non-programmatic submissions (form builder functions may expect it to be
+  // there).
+  $form_state['input'] = $form_state['values'];
+
+  $form_state['programmed'] = TRUE;
+  $form = drupal_retrieve_form($form_id, $form_state);
+  // Programmed forms are always submitted.
+  $form_state['submitted'] = TRUE;
+
+  // Reset form validation.
+  $form_state['must_validate'] = TRUE;
+  form_clear_error();
+
+  drupal_prepare_form($form_id, $form, $form_state);
+  drupal_process_form($form_id, $form, $form_state);
+}
+
+/**
+ * Retrieves the structured array that defines a given form.
+ *
+ * @param $form_id
+ *   The unique string identifying the desired form. If a function
+ *   with that name exists, it is called to build the form array.
+ *   Modules that need to generate the same form (or very similar forms)
+ *   using different $form_ids can implement hook_forms(), which maps
+ *   different $form_id values to the proper form constructor function.
+ * @param $form_state
+ *   A keyed array containing the current state of the form, including the
+ *   additional arguments to drupal_get_form() or drupal_form_submit() in the
+ *   'args' component of the array.
+ */
+function drupal_retrieve_form($form_id, &$form_state) {
+  $forms = &drupal_static(__FUNCTION__);
+
+  // Record the $form_id.
+  $form_state['build_info']['form_id'] = $form_id;
+
+  // Record the filepath of the include file containing the original form, so
+  // the form builder callbacks can be loaded when the form is being rebuilt
+  // from cache on a different path (such as 'system/ajax'). See
+  // form_get_cache().
+  // $menu_get_item() is not available during installation.
+  if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) {
+    $item = menu_get_item();
+    if (!empty($item['include_file'])) {
+      // Do not use form_load_include() here, as the file is already loaded.
+      // Anyway, form_get_cache() is able to handle filepaths too.
+      $form_state['build_info']['files']['menu'] = $item['include_file'];
+    }
+  }
+
+  // We save two copies of the incoming arguments: one for modules to use
+  // when mapping form ids to constructor functions, and another to pass to
+  // the constructor function itself.
+  $args = $form_state['build_info']['args'];
+
+  // We first check to see if there's a function named after the $form_id.
+  // If there is, we simply pass the arguments on to it to get the form.
+  if (!function_exists($form_id)) {
+    // In cases where many form_ids need to share a central constructor function,
+    // such as the node editing form, modules can implement hook_forms(). It
+    // maps one or more form_ids to the correct constructor functions.
+    //
+    // We cache the results of that hook to save time, but that only works
+    // for modules that know all their form_ids in advance. (A module that
+    // adds a small 'rate this comment' form to each comment in a list
+    // would need a unique form_id for each one, for example.)
+    //
+    // So, we call the hook if $forms isn't yet populated, OR if it doesn't
+    // yet have an entry for the requested form_id.
+    if (!isset($forms) || !isset($forms[$form_id])) {
+      $forms = module_invoke_all('forms', $form_id, $args);
+    }
+    $form_definition = $forms[$form_id];
+    if (isset($form_definition['callback arguments'])) {
+      $args = array_merge($form_definition['callback arguments'], $args);
+    }
+    if (isset($form_definition['callback'])) {
+      $callback = $form_definition['callback'];
+      $form_state['build_info']['base_form_id'] = $callback;
+    }
+    // In case $form_state['wrapper_callback'] is not defined already, we also
+    // allow hook_forms() to define one.
+    if (!isset($form_state['wrapper_callback']) && isset($form_definition['wrapper_callback'])) {
+      $form_state['wrapper_callback'] = $form_definition['wrapper_callback'];
+    }
+  }
+
+  $form = array();
+  // We need to pass $form_state by reference in order for forms to modify it,
+  // since call_user_func_array() requires that referenced variables are passed
+  // explicitly.
+  $args = array_merge(array($form, &$form_state), $args);
+
+  // When the passed $form_state (not using drupal_get_form()) defines a
+  // 'wrapper_callback', then it requests to invoke a separate (wrapping) form
+  // builder function to pre-populate the $form array with form elements, which
+  // the actual form builder function ($callback) expects. This allows for
+  // pre-populating a form with common elements for certain forms, such as
+  // back/next/save buttons in multi-step form wizards. See drupal_build_form().
+  if (isset($form_state['wrapper_callback']) && function_exists($form_state['wrapper_callback'])) {
+    $form = call_user_func_array($form_state['wrapper_callback'], $args);
+    // Put the prepopulated $form into $args.
+    $args[0] = $form;
+  }
+
+  // If $callback was returned by a hook_forms() implementation, call it.
+  // Otherwise, call the function named after the form id.
+  $form = call_user_func_array(isset($callback) ? $callback : $form_id, $args);
+  $form['#form_id'] = $form_id;
+
+  return $form;
+}
+
+/**
+ * Processes a form submission.
+ *
+ * This function is the heart of form API. The form gets built, validated and in
+ * appropriate cases, submitted and rebuilt.
+ *
+ * @param $form_id
+ *   The unique string identifying the current form.
+ * @param $form
+ *   An associative array containing the structure of the form.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. This
+ *   includes the current persistent storage data for the form, and
+ *   any data passed along by earlier steps when displaying a
+ *   multi-step form. Additional information, like the sanitized $_POST
+ *   data, is also accumulated here.
+ */
+function drupal_process_form($form_id, &$form, &$form_state) {
+  $form_state['values'] = array();
+
+  // With $_GET, these forms are always submitted if requested.
+  if ($form_state['method'] == 'get' && !empty($form_state['always_process'])) {
+    if (!isset($form_state['input']['form_build_id'])) {
+      $form_state['input']['form_build_id'] = $form['#build_id'];
+    }
+    if (!isset($form_state['input']['form_id'])) {
+      $form_state['input']['form_id'] = $form_id;
+    }
+    if (!isset($form_state['input']['form_token']) && isset($form['#token'])) {
+      $form_state['input']['form_token'] = drupal_get_token($form['#token']);
+    }
+  }
+
+  // form_builder() finishes building the form by calling element #process
+  // functions and mapping user input, if any, to #value properties, and also
+  // storing the values in $form_state['values']. We need to retain the
+  // unprocessed $form in case it needs to be cached.
+  $unprocessed_form = $form;
+  $form = form_builder($form_id, $form, $form_state);
+
+  // Only process the input if we have a correct form submission.
+  if ($form_state['process_input']) {
+    drupal_validate_form($form_id, $form, $form_state);
+
+    // drupal_html_id() maintains a cache of element IDs it has seen,
+    // so it can prevent duplicates. We want to be sure we reset that
+    // cache when a form is processed, so scenarios that result in
+    // the form being built behind the scenes and again for the
+    // browser don't increment all the element IDs needlessly.
+    if (!form_get_errors()) {
+      // In case of errors, do not break HTML IDs of other forms.
+      drupal_static_reset('drupal_html_id');
+    }
+
+    if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild']) {
+      // Execute form submit handlers.
+      form_execute_handlers('submit', $form, $form_state);
+
+      // We'll clear out the cached copies of the form and its stored data
+      // here, as we've finished with them. The in-memory copies are still
+      // here, though.
+      if (!variable_get('cache', 0) && !empty($form_state['values']['form_build_id'])) {
+        cache_clear_all('form_' . $form_state['values']['form_build_id'], 'cache_form');
+        cache_clear_all('form_state_' . $form_state['values']['form_build_id'], 'cache_form');
+      }
+
+      // If batches were set in the submit handlers, we process them now,
+      // possibly ending execution. We make sure we do not react to the batch
+      // that is already being processed (if a batch operation performs a
+      // drupal_form_submit).
+      if ($batch =& batch_get() && !isset($batch['current_set'])) {
+        // Store $form_state information in the batch definition.
+        // We need the full $form_state when either:
+        // - Some submit handlers were saved to be called during batch
+        //   processing. See form_execute_handlers().
+        // - The form is multistep.
+        // In other cases, we only need the information expected by
+        // drupal_redirect_form().
+        if ($batch['has_form_submits'] || !empty($form_state['rebuild'])) {
+          $batch['form_state'] = $form_state;
+        }
+        else {
+          $batch['form_state'] = array_intersect_key($form_state, array_flip(array('programmed', 'rebuild', 'storage', 'no_redirect', 'redirect')));
+        }
+
+        $batch['progressive'] = !$form_state['programmed'];
+        batch_process();
+
+        // Execution continues only for programmatic forms.
+        // For 'regular' forms, we get redirected to the batch processing
+        // page. Form redirection will be handled in _batch_finished(),
+        // after the batch is processed.
+      }
+
+      // Set a flag to indicate the the form has been processed and executed.
+      $form_state['executed'] = TRUE;
+
+      // Redirect the form based on values in $form_state.
+      drupal_redirect_form($form_state);
+    }
+
+    // Don't rebuild or cache form submissions invoked via drupal_form_submit().
+    if (!empty($form_state['programmed'])) {
+      return;
+    }
+
+    // If $form_state['rebuild'] has been set and input has been processed
+    // without validation errors, we are in a multi-step workflow that is not
+    // yet complete. A new $form needs to be constructed based on the changes
+    // made to $form_state during this request. Normally, a submit handler sets
+    // $form_state['rebuild'] if a fully executed form requires another step.
+    // However, for forms that have not been fully executed (e.g., Ajax
+    // submissions triggered by non-buttons), there is no submit handler to set
+    // $form_state['rebuild']. It would not make sense to redisplay the
+    // identical form without an error for the user to correct, so we also
+    // rebuild error-free non-executed forms, regardless of
+    // $form_state['rebuild'].
+    // @todo D8: Simplify this logic; considering Ajax and non-HTML front-ends,
+    //   along with element-level #submit properties, it makes no sense to have
+    //   divergent form execution based on whether the triggering element has
+    //   #executes_submit_callback set to TRUE.
+    if (($form_state['rebuild'] || !$form_state['executed']) && !form_get_errors()) {
+      // Form building functions (e.g., _form_builder_handle_input_element())
+      // may use $form_state['rebuild'] to determine if they are running in the
+      // context of a rebuild, so ensure it is set.
+      $form_state['rebuild'] = TRUE;
+      $form = drupal_rebuild_form($form_id, $form_state, $form);
+    }
+  }
+
+  // After processing the form, the form builder or a #process callback may
+  // have set $form_state['cache'] to indicate that the form and form state
+  // shall be cached. But the form may only be cached if the 'no_cache' property
+  // is not set to TRUE. Only cache $form as it was prior to form_builder(),
+  // because form_builder() must run for each request to accommodate new user
+  // input. Rebuilt forms are not cached here, because drupal_rebuild_form()
+  // already takes care of that.
+  if (!$form_state['rebuild'] && $form_state['cache'] && empty($form_state['no_cache'])) {
+    form_set_cache($form['#build_id'], $unprocessed_form, $form_state);
+  }
+}
+
+/**
+ * Prepares a structured form array.
+ *
+ * Adds required elements, executes any hook_form_alter functions, and
+ * optionally inserts a validation token to prevent tampering.
+ *
+ * @param $form_id
+ *   A unique string identifying the form for validation, submission,
+ *   theming, and hook_form_alter functions.
+ * @param $form
+ *   An associative array containing the structure of the form.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. Passed
+ *   in here so that hook_form_alter() calls can use it, as well.
+ */
+function drupal_prepare_form($form_id, &$form, &$form_state) {
+  global $user;
+
+  $form['#type'] = 'form';
+  $form_state['programmed'] = isset($form_state['programmed']) ? $form_state['programmed'] : FALSE;
+
+  // Fix the form method, if it is 'get' in $form_state, but not in $form.
+  if ($form_state['method'] == 'get' && !isset($form['#method'])) {
+    $form['#method'] = 'get';
+  }
+
+  // Generate a new #build_id for this form, if none has been set already. The
+  // form_build_id is used as key to cache a particular build of the form. For
+  // multi-step forms, this allows the user to go back to an earlier build, make
+  // changes, and re-submit.
+  // @see drupal_build_form()
+  // @see drupal_rebuild_form()
+  if (!isset($form['#build_id'])) {
+    $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
+  }
+  $form['form_build_id'] = array(
+    '#type' => 'hidden',
+    '#value' => $form['#build_id'],
+    '#id' => $form['#build_id'],
+    '#name' => 'form_build_id',
+    // Form processing and validation requires this value, so ensure the
+    // submitted form value appears literally, regardless of custom #tree
+    // and #parents being set elsewhere.
+    '#parents' => array('form_build_id'),
+  );
+
+  // Add a token, based on either #token or form_id, to any form displayed to
+  // authenticated users. This ensures that any submitted form was actually
+  // requested previously by the user and protects against cross site request
+  // forgeries.
+  // This does not apply to programmatically submitted forms. Furthermore, since
+  // tokens are session-bound and forms displayed to anonymous users are very
+  // likely cached, we cannot assign a token for them.
+  // During installation, there is no $user yet.
+  if (!empty($user->uid) && !$form_state['programmed']) {
+    // Form constructors may explicitly set #token to FALSE when cross site
+    // request forgery is irrelevant to the form, such as search forms.
+    if (isset($form['#token']) && $form['#token'] === FALSE) {
+      unset($form['#token']);
+    }
+    // Otherwise, generate a public token based on the form id.
+    else {
+      $form['#token'] = $form_id;
+      $form['form_token'] = array(
+        '#id' => drupal_html_id('edit-' . $form_id . '-form-token'),
+        '#type' => 'token',
+        '#default_value' => drupal_get_token($form['#token']),
+        // Form processing and validation requires this value, so ensure the
+        // submitted form value appears literally, regardless of custom #tree
+        // and #parents being set elsewhere.
+        '#parents' => array('form_token'),
+      );
+    }
+  }
+
+  if (isset($form_id)) {
+    $form['form_id'] = array(
+      '#type' => 'hidden',
+      '#value' => $form_id,
+      '#id' => drupal_html_id("edit-$form_id"),
+      // Form processing and validation requires this value, so ensure the
+      // submitted form value appears literally, regardless of custom #tree
+      // and #parents being set elsewhere.
+      '#parents' => array('form_id'),
+    );
+  }
+  if (!isset($form['#id'])) {
+    $form['#id'] = drupal_html_id($form_id);
+  }
+
+  $form += element_info('form');
+  $form += array('#tree' => FALSE, '#parents' => array());
+
+  if (!isset($form['#validate'])) {
+    // Ensure that modules can rely on #validate being set.
+    $form['#validate'] = array();
+    // Check for a handler specific to $form_id.
+    if (function_exists($form_id . '_validate')) {
+      $form['#validate'][] = $form_id . '_validate';
+    }
+    // Otherwise check whether this is a shared form and whether there is a
+    // handler for the shared $form_id.
+    elseif (isset($form_state['build_info']['base_form_id']) && function_exists($form_state['build_info']['base_form_id'] . '_validate')) {
+      $form['#validate'][] = $form_state['build_info']['base_form_id'] . '_validate';
+    }
+  }
+
+  if (!isset($form['#submit'])) {
+    // Ensure that modules can rely on #submit being set.
+    $form['#submit'] = array();
+    // Check for a handler specific to $form_id.
+    if (function_exists($form_id . '_submit')) {
+      $form['#submit'][] = $form_id . '_submit';
+    }
+    // Otherwise check whether this is a shared form and whether there is a
+    // handler for the shared $form_id.
+    elseif (isset($form_state['build_info']['base_form_id']) && function_exists($form_state['build_info']['base_form_id'] . '_submit')) {
+      $form['#submit'][] = $form_state['build_info']['base_form_id'] . '_submit';
+    }
+  }
+
+  // If no #theme has been set, automatically apply theme suggestions.
+  // theme_form() itself is in #theme_wrappers and not #theme. Therefore, the
+  // #theme function only has to care for rendering the inner form elements,
+  // not the form itself.
+  if (!isset($form['#theme'])) {
+    $form['#theme'] = array($form_id);
+    if (isset($form_state['build_info']['base_form_id'])) {
+      $form['#theme'][] = $form_state['build_info']['base_form_id'];
+    }
+  }
+
+  // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
+  // hook_form_FORM_ID_alter() implementations.
+  $hooks = array('form');
+  if (isset($form_state['build_info']['base_form_id'])) {
+    $hooks[] = 'form_' . $form_state['build_info']['base_form_id'];
+  }
+  $hooks[] = 'form_' . $form_id;
+  drupal_alter($hooks, $form, $form_state, $form_id);
+}
+
+
+/**
+ * Validates user-submitted form data in the $form_state array.
+ *
+ * @param $form_id
+ *   A unique string identifying the form for validation, submission,
+ *   theming, and hook_form_alter functions.
+ * @param $form
+ *   An associative array containing the structure of the form, which is passed
+ *   by reference. Form validation handlers are able to alter the form structure
+ *   (like #process and #after_build callbacks during form building) in case of
+ *   a validation error. If a validation handler alters the form structure, it
+ *   is responsible for validating the values of changed form elements in
+ *   $form_state['values'] to prevent form submit handlers from receiving
+ *   unvalidated values.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. The current
+ *   user-submitted data is stored in $form_state['values'], though
+ *   form validation functions are passed an explicit copy of the
+ *   values for the sake of simplicity. Validation handlers can also use
+ *   $form_state to pass information on to submit handlers. For example:
+ *     $form_state['data_for_submission'] = $data;
+ *   This technique is useful when validation requires file parsing,
+ *   web service requests, or other expensive requests that should
+ *   not be repeated in the submission step.
+ */
+function drupal_validate_form($form_id, &$form, &$form_state) {
+  $validated_forms = &drupal_static(__FUNCTION__, array());
+
+  if (isset($validated_forms[$form_id]) && empty($form_state['must_validate'])) {
+    return;
+  }
+
+  // If the session token was set by drupal_prepare_form(), ensure that it
+  // matches the current user's session.
+  if (isset($form['#token'])) {
+    if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) {
+      $path = current_path();
+      $query = drupal_get_query_parameters();
+      $url = url($path, array('query' => $query));
+
+      // Setting this error will cause the form to fail validation.
+      form_set_error('form_token', t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url)));
+    }
+  }
+
+  _form_validate($form, $form_state, $form_id);
+  $validated_forms[$form_id] = TRUE;
+
+  // If validation errors are limited then remove any non validated form values,
+  // so that only values that passed validation are left for submit callbacks.
+  if (isset($form_state['triggering_element']['#limit_validation_errors']) && $form_state['triggering_element']['#limit_validation_errors'] !== FALSE) {
+    $values = array();
+    foreach ($form_state['triggering_element']['#limit_validation_errors'] as $section) {
+      // If the section exists within $form_state['values'], even if the value
+      // is NULL, copy it to $values.
+      $section_exists = NULL;
+      $value = drupal_array_get_nested_value($form_state['values'], $section, $section_exists);
+      if ($section_exists) {
+        drupal_array_set_nested_value($values, $section, $value);
+      }
+    }
+    // A button's #value does not require validation, so for convenience we
+    // allow the value of the clicked button to be retained in its normal
+    // $form_state['values'] locations, even if these locations are not included
+    // in #limit_validation_errors.
+    if (isset($form_state['triggering_element']['#button_type'])) {
+      $button_value = $form_state['triggering_element']['#value'];
+
+      // Like all input controls, the button value may be in the location
+      // dictated by #parents. If it is, copy it to $values, but do not override
+      // what may already be in $values.
+      $parents = $form_state['triggering_element']['#parents'];
+      if (!drupal_array_nested_key_exists($values, $parents) && drupal_array_get_nested_value($form_state['values'], $parents) === $button_value) {
+        drupal_array_set_nested_value($values, $parents, $button_value);
+      }
+
+      // Additionally, form_builder() places the button value in
+      // $form_state['values'][BUTTON_NAME]. If it's still there, after
+      // validation handlers have run, copy it to $values, but do not override
+      // what may already be in $values.
+      $name = $form_state['triggering_element']['#name'];
+      if (!isset($values[$name]) && isset($form_state['values'][$name]) && $form_state['values'][$name] === $button_value) {
+        $values[$name] = $button_value;
+      }
+    }
+    $form_state['values'] = $values;
+  }
+}
+
+/**
+ * Redirects the user to a URL after a form has been processed.
+ *
+ * After a form is submitted and processed, normally the user should be
+ * redirected to a new destination page. This function figures out what that
+ * destination should be, based on the $form_state array and the 'destination'
+ * query string in the request URL, and redirects the user there.
+ *
+ * Usually (for exceptions, see below) $form_state['redirect'] determines where
+ * to redirect the user. This can be set either to a string (the path to
+ * redirect to), or an array of arguments for drupal_goto(). If
+ * $form_state['redirect'] is missing, the user is usually (again, see below for
+ * exceptions) redirected back to the page they came from, where they should see
+ * a fresh, unpopulated copy of the form.
+ *
+ * Here is an example of how to set up a form to redirect to the path 'node':
+ * @code
+ * $form_state['redirect'] = 'node';
+ * @endcode
+ * And here is an example of how to redirect to 'node/123?foo=bar#baz':
+ * @code
+ * $form_state['redirect'] = array(
+ *   'node/123',
+ *   array(
+ *     'query' => array(
+ *       'foo' => 'bar',
+ *     ),
+ *     'fragment' => 'baz',
+ *   ),
+ * );
+ * @endcode
+ *
+ * There are several exceptions to the "usual" behavior described above:
+ * - If $form_state['programmed'] is TRUE, the form submission was usually
+ *   invoked via drupal_form_submit(), so any redirection would break the script
+ *   that invoked drupal_form_submit() and no redirection is done.
+ * - If $form_state['rebuild'] is TRUE, the form is being rebuilt, and no
+ *   redirection is done.
+ * - If $form_state['no_redirect'] is TRUE, redirection is disabled. This is
+ *   set, for instance, by ajax_get_form() to prevent redirection in Ajax
+ *   callbacks. $form_state['no_redirect'] should never be set or altered by
+ *   form builder functions or form validation/submit handlers.
+ * - If $form_state['redirect'] is set to FALSE, redirection is disabled.
+ * - If none of the above conditions has prevented redirection, then the
+ *   redirect is accomplished by calling drupal_goto(), passing in the value of
+ *   $form_state['redirect'] if it is set, or the current path if it is
+ *   not. drupal_goto() preferentially uses the value of $_GET['destination']
+ *   (the 'destination' URL query string) if it is present, so this will
+ *   override any values set by $form_state['redirect']. Note that during
+ *   installation, install_goto() is called in place of drupal_goto().
+ *
+ * @param $form_state
+ *   An associative array containing the current state of the form.
+ *
+ * @see drupal_process_form()
+ * @see drupal_build_form()
+ */
+function drupal_redirect_form($form_state) {
+  // Skip redirection for form submissions invoked via drupal_form_submit().
+  if (!empty($form_state['programmed'])) {
+    return;
+  }
+  // Skip redirection if rebuild is activated.
+  if (!empty($form_state['rebuild'])) {
+    return;
+  }
+  // Skip redirection if it was explicitly disallowed.
+  if (!empty($form_state['no_redirect'])) {
+    return;
+  }
+  // Only invoke drupal_goto() if redirect value was not set to FALSE.
+  if (!isset($form_state['redirect']) || $form_state['redirect'] !== FALSE) {
+    if (isset($form_state['redirect'])) {
+      if (is_array($form_state['redirect'])) {
+        call_user_func_array('drupal_goto', $form_state['redirect']);
+      }
+      else {
+        // This function can be called from the installer, which guarantees
+        // that $redirect will always be a string, so catch that case here
+        // and use the appropriate redirect function.
+        $function = drupal_installation_attempted() ? 'install_goto' : 'drupal_goto';
+        $function($form_state['redirect']);
+      }
+    }
+    drupal_goto(current_path(), array('query' => drupal_get_query_parameters()));
+  }
+}
+
+/**
+ * Performs validation on form elements.
+ *
+ * First ensures required fields are completed, #maxlength is not exceeded, and
+ * selected options were in the list of options given to the user. Then calls
+ * user-defined validators.
+ *
+ * @param $elements
+ *   An associative array containing the structure of the form.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. The current
+ *   user-submitted data is stored in $form_state['values'], though
+ *   form validation functions are passed an explicit copy of the
+ *   values for the sake of simplicity. Validation handlers can also
+ *   $form_state to pass information on to submit handlers. For example:
+ *     $form_state['data_for_submission'] = $data;
+ *   This technique is useful when validation requires file parsing,
+ *   web service requests, or other expensive requests that should
+ *   not be repeated in the submission step.
+ * @param $form_id
+ *   A unique string identifying the form for validation, submission,
+ *   theming, and hook_form_alter functions.
+ */
+function _form_validate(&$elements, &$form_state, $form_id = NULL) {
+  // Also used in the installer, pre-database setup.
+  $t = get_t();
+
+  // Recurse through all children.
+  foreach (element_children($elements) as $key) {
+    if (isset($elements[$key]) && $elements[$key]) {
+      _form_validate($elements[$key], $form_state);
+    }
+  }
+
+  // Validate the current input.
+  if (!isset($elements['#validated']) || !$elements['#validated']) {
+    // The following errors are always shown.
+    if (isset($elements['#needs_validation'])) {
+      // Verify that the value is not longer than #maxlength.
+      if (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#maxlength']) {
+        form_error($elements, $t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => drupal_strlen($elements['#value']))));
+      }
+
+      if (isset($elements['#options']) && isset($elements['#value'])) {
+        if ($elements['#type'] == 'select') {
+          $options = form_options_flatten($elements['#options']);
+        }
+        else {
+          $options = $elements['#options'];
+        }
+        if (is_array($elements['#value'])) {
+          $value = in_array($elements['#type'], array('checkboxes', 'tableselect')) ? array_keys($elements['#value']) : $elements['#value'];
+          foreach ($value as $v) {
+            if (!isset($options[$v])) {
+              form_error($elements, $t('An illegal choice has been detected. Please contact the site administrator.'));
+              watchdog('form', 'Illegal choice %choice in !name element.', array('%choice' => $v, '!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), WATCHDOG_ERROR);
+            }
+          }
+        }
+        // Non-multiple select fields always have a value in HTML. If the user
+        // does not change the form, it will be the value of the first option.
+        // Because of this, form validation for the field will almost always
+        // pass, even if the user did not select anything. To work around this
+        // browser behavior, required select fields without a #default_value get
+        // an additional, first empty option. In case the submitted value is
+        // identical to the empty option's value, we reset the element's value
+        // to NULL to trigger the regular #required handling below.
+        // @see form_process_select()
+        elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) {
+          $elements['#value'] = NULL;
+          form_set_value($elements, NULL, $form_state);
+        }
+        elseif (!isset($options[$elements['#value']])) {
+          form_error($elements, $t('An illegal choice has been detected. Please contact the site administrator.'));
+          watchdog('form', 'Illegal choice %choice in %name element.', array('%choice' => $elements['#value'], '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), WATCHDOG_ERROR);
+        }
+      }
+    }
+
+    // While this element is being validated, it may be desired that some calls
+    // to form_set_error() be suppressed and not result in a form error, so
+    // that a button that implements low-risk functionality (such as "Previous"
+    // or "Add more") that doesn't require all user input to be valid can still
+    // have its submit handlers triggered. The triggering element's
+    // #limit_validation_errors property contains the information for which
+    // errors are needed, and all other errors are to be suppressed. The
+    // #limit_validation_errors property is ignored if submit handlers will run,
+    // but the element doesn't have a #submit property, because it's too large a
+    // security risk to have any invalid user input when executing form-level
+    // submit handlers.
+    if (isset($form_state['triggering_element']['#limit_validation_errors']) && ($form_state['triggering_element']['#limit_validation_errors'] !== FALSE) && !($form_state['submitted'] && !isset($form_state['triggering_element']['#submit']))) {
+      form_set_error(NULL, '', $form_state['triggering_element']['#limit_validation_errors']);
+    }
+    // If submit handlers won't run (due to the submission having been triggered
+    // by an element whose #executes_submit_callback property isn't TRUE), then
+    // it's safe to suppress all validation errors, and we do so by default,
+    // which is particularly useful during an Ajax submission triggered by a
+    // non-button. An element can override this default by setting the
+    // #limit_validation_errors property. For button element types,
+    // #limit_validation_errors defaults to FALSE (via system_element_info()),
+    // so that full validation is their default behavior.
+    elseif (isset($form_state['triggering_element']) && !isset($form_state['triggering_element']['#limit_validation_errors']) && !$form_state['submitted']) {
+      form_set_error(NULL, '', array());
+    }
+    // As an extra security measure, explicitly turn off error suppression if
+    // one of the above conditions wasn't met. Since this is also done at the
+    // end of this function, doing it here is only to handle the rare edge case
+    // where a validate handler invokes form processing of another form.
+    else {
+      drupal_static_reset('form_set_error:limit_validation_errors');
+    }
+
+    // Make sure a value is passed when the field is required.
+    if (isset($elements['#needs_validation']) && $elements['#required']) {
+      // A simple call to empty() will not cut it here as some fields, like
+      // checkboxes, can return a valid value of '0'. Instead, check the
+      // length if it's a string, and the item count if it's an array.
+      // An unchecked checkbox has a #value of integer 0, different than string
+      // '0', which could be a valid value.
+      $is_empty_multiple = (!count($elements['#value']));
+      $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0);
+      $is_empty_value = ($elements['#value'] === 0);
+      if ($is_empty_multiple || $is_empty_string || $is_empty_value) {
+        // Although discouraged, a #title is not mandatory for form elements. In
+        // case there is no #title, we cannot set a form error message.
+        // Instead of setting no #title, form constructors are encouraged to set
+        // #title_display to 'invisible' to improve accessibility.
+        if (isset($elements['#title'])) {
+          form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
+        }
+        else {
+          form_error($elements);
+        }
+      }
+    }
+
+    // Call user-defined form level validators.
+    if (isset($form_id)) {
+      form_execute_handlers('validate', $elements, $form_state);
+    }
+    // Call any element-specific validators. These must act on the element
+    // #value data.
+    elseif (isset($elements['#element_validate'])) {
+      foreach ($elements['#element_validate'] as $function) {
+        $function($elements, $form_state, $form_state['complete form']);
+      }
+    }
+    $elements['#validated'] = TRUE;
+  }
+
+  // Done validating this element, so turn off error suppression.
+  // _form_validate() turns it on again when starting on the next element, if
+  // it's still appropriate to do so.
+  drupal_static_reset('form_set_error:limit_validation_errors');
+}
+
+/**
+ * Executes custom validation and submission handlers for a given form.
+ *
+ * Button-specific handlers are checked first. If none exist, the function
+ * falls back to form-level handlers.
+ *
+ * @param $type
+ *   The type of handler to execute. 'validate' or 'submit' are the
+ *   defaults used by Form API.
+ * @param $form
+ *   An associative array containing the structure of the form.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. If the user
+ *   submitted the form by clicking a button with custom handler functions
+ *   defined, those handlers will be stored here.
+ */
+function form_execute_handlers($type, &$form, &$form_state) {
+  $return = FALSE;
+  // If there was a button pressed, use its handlers.
+  if (isset($form_state[$type . '_handlers'])) {
+    $handlers = $form_state[$type . '_handlers'];
+  }
+  // Otherwise, check for a form-level handler.
+  elseif (isset($form['#' . $type])) {
+    $handlers = $form['#' . $type];
+  }
+  else {
+    $handlers = array();
+  }
+
+  foreach ($handlers as $function) {
+    // Check if a previous _submit handler has set a batch, but make sure we
+    // do not react to a batch that is already being processed (for instance
+    // if a batch operation performs a drupal_form_submit()).
+    if ($type == 'submit' && ($batch =& batch_get()) && !isset($batch['id'])) {
+      // Some previous submit handler has set a batch. To ensure correct
+      // execution order, store the call in a special 'control' batch set.
+      // See _batch_next_set().
+      $batch['sets'][] = array('form_submit' => $function);
+      $batch['has_form_submits'] = TRUE;
+    }
+    else {
+      $function($form, $form_state);
+    }
+    $return = TRUE;
+  }
+  return $return;
+}
+
+/**
+ * Files an error against a form element.
+ *
+ * When a validation error is detected, the validator calls form_set_error() to
+ * indicate which element needs to be changed and provide an error message. This
+ * causes the Form API to not execute the form submit handlers, and instead to
+ * re-display the form to the user with the corresponding elements rendered with
+ * an 'error' CSS class (shown as red by default).
+ *
+ * The standard form_set_error() behavior can be changed if a button provides
+ * the #limit_validation_errors property. Multistep forms not wanting to
+ * validate the whole form can set #limit_validation_errors on buttons to
+ * limit validation errors to only certain elements. For example, pressing the
+ * "Previous" button in a multistep form should not fire validation errors just
+ * because the current step has invalid values. If #limit_validation_errors is
+ * set on a clicked button, the button must also define a #submit property
+ * (may be set to an empty array). Any #submit handlers will be executed even if
+ * there is invalid input, so extreme care should be taken with respect to any
+ * actions taken by them. This is typically not a problem with buttons like
+ * "Previous" or "Add more" that do not invoke persistent storage of the
+ * submitted form values. Do not use the #limit_validation_errors property on
+ * buttons that trigger saving of form values to the database.
+ *
+ * The #limit_validation_errors property is a list of "sections" within
+ * $form_state['values'] that must contain valid values. Each "section" is an
+ * array with the ordered set of keys needed to reach that part of
+ * $form_state['values'] (i.e., the #parents property of the element).
+ *
+ * Example 1: Allow the "Previous" button to function, regardless of whether any
+ * user input is valid.
+ *
+ * @code
+ *   $form['actions']['previous'] = array(
+ *     '#type' => 'submit',
+ *     '#value' => t('Previous'),
+ *     '#limit_validation_errors' => array(),       // No validation.
+ *     '#submit' => array('some_submit_function'),  // #submit required.
+ *   );
+ * @endcode
+ *
+ * Example 2: Require some, but not all, user input to be valid to process the
+ * submission of a "Previous" button.
+ *
+ * @code
+ *   $form['actions']['previous'] = array(
+ *     '#type' => 'submit',
+ *     '#value' => t('Previous'),
+ *     '#limit_validation_errors' => array(
+ *       array('step1'),       // Validate $form_state['values']['step1'].
+ *       array('foo', 'bar'),  // Validate $form_state['values']['foo']['bar'].
+ *     ),
+ *     '#submit' => array('some_submit_function'), // #submit required.
+ *   );
+ * @endcode
+ *
+ * This will require $form_state['values']['step1'] and everything within it
+ * (for example, $form_state['values']['step1']['choice']) to be valid, so
+ * calls to form_set_error('step1', $message) or
+ * form_set_error('step1][choice', $message) will prevent the submit handlers
+ * from running, and result in the error message being displayed to the user.
+ * However, calls to form_set_error('step2', $message) and
+ * form_set_error('step2][groupX][choiceY', $message) will be suppressed,
+ * resulting in the message not being displayed to the user, and the submit
+ * handlers will run despite $form_state['values']['step2'] and
+ * $form_state['values']['step2']['groupX']['choiceY'] containing invalid
+ * values. Errors for an invalid $form_state['values']['foo'] will be
+ * suppressed, but errors flagging invalid values for
+ * $form_state['values']['foo']['bar'] and everything within it will be
+ * flagged and submission prevented.
+ *
+ * Partial form validation is implemented by suppressing errors rather than by
+ * skipping the input processing and validation steps entirely, because some
+ * forms have button-level submit handlers that call Drupal API functions that
+ * assume that certain data exists within $form_state['values'], and while not
+ * doing anything with that data that requires it to be valid, PHP errors
+ * would be triggered if the input processing and validation steps were fully
+ * skipped.
+ *
+ * @param $name
+ *   The name of the form element. If the #parents property of your form
+ *   element is array('foo', 'bar', 'baz') then you may set an error on 'foo'
+ *   or 'foo][bar][baz'. Setting an error on 'foo' sets an error for every
+ *   element where the #parents array starts with 'foo'.
+ * @param $message
+ *   The error message to present to the user.
+ * @param $limit_validation_errors
+ *   Internal use only. The #limit_validation_errors property of the clicked
+ *   button, if it exists.
+ *
+ * @return
+ *   Return value is for internal use only. To get a list of errors, use
+ *   form_get_errors() or form_get_error().
+ *
+ * @see http://drupal.org/node/370537
+ * @see http://drupal.org/node/763376
+ */
+function form_set_error($name = NULL, $message = '', $limit_validation_errors = NULL) {
+  $form = &drupal_static(__FUNCTION__, array());
+  $sections = &drupal_static(__FUNCTION__ . ':limit_validation_errors');
+  if (isset($limit_validation_errors)) {
+    $sections = $limit_validation_errors;
+  }
+
+  if (isset($name) && !isset($form[$name])) {
+    $record = TRUE;
+    if (isset($sections)) {
+      // #limit_validation_errors is an array of "sections" within which user
+      // input must be valid. If the element is within one of these sections,
+      // the error must be recorded. Otherwise, it can be suppressed.
+      // #limit_validation_errors can be an empty array, in which case all
+      // errors are suppressed. For example, a "Previous" button might want its
+      // submit action to be triggered even if none of the submitted values are
+      // valid.
+      $record = FALSE;
+      foreach ($sections as $section) {
+        // Exploding by '][' reconstructs the element's #parents. If the
+        // reconstructed #parents begin with the same keys as the specified
+        // section, then the element's values are within the part of
+        // $form_state['values'] that the clicked button requires to be valid,
+        // so errors for this element must be recorded. As the exploded array
+        // will all be strings, we need to cast every value of the section
+        // array to string.
+        if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) {
+          $record = TRUE;
+          break;
+        }
+      }
+    }
+    if ($record) {
+      $form[$name] = $message;
+      if ($message) {
+        drupal_set_message($message, 'error');
+      }
+    }
+  }
+
+  return $form;
+}
+
+/**
+ * Clears all errors against all form elements made by form_set_error().
+ */
+function form_clear_error() {
+  drupal_static_reset('form_set_error');
+}
+
+/**
+ * Returns an associative array of all errors.
+ */
+function form_get_errors() {
+  $form = form_set_error();
+  if (!empty($form)) {
+    return $form;
+  }
+}
+
+/**
+ * Returns the error message filed against the given form element.
+ *
+ * Form errors higher up in the form structure override deeper errors as well as
+ * errors on the element itself.
+ */
+function form_get_error($element) {
+  $form = form_set_error();
+  $parents = array();
+  foreach ($element['#parents'] as $parent) {
+    $parents[] = $parent;
+    $key = implode('][', $parents);
+    if (isset($form[$key])) {
+      return $form[$key];
+    }
+  }
+}
+
+/**
+ * Flags an element as having an error.
+ */
+function form_error(&$element, $message = '') {
+  form_set_error(implode('][', $element['#parents']), $message);
+}
+
+/**
+ * Builds and processes all elements in the structured form array.
+ *
+ * Adds any required properties to each element, maps the incoming input data
+ * to the proper elements, and executes any #process handlers attached to a
+ * specific element.
+ *
+ * This is one of the three primary functions that recursively iterates a form
+ * array. This one does it for completing the form building process. The other
+ * two are _form_validate() (invoked via drupal_validate_form() and used to
+ * invoke validation logic for each element) and drupal_render() (for rendering
+ * each element). Each of these three pipelines provides ample opportunity for
+ * modules to customize what happens. For example, during this function's life
+ * cycle, the following functions get called for each element:
+ * - $element['#value_callback']: A function that implements how user input is
+ *   mapped to an element's #value property. This defaults to a function named
+ *   'form_type_TYPE_value' where TYPE is $element['#type'].
+ * - $element['#process']: An array of functions called after user input has
+ *   been mapped to the element's #value property. These functions can be used
+ *   to dynamically add child elements: for example, for the 'date' element
+ *   type, one of the functions in this array is form_process_date(), which adds
+ *   the individual 'year', 'month', 'day', etc. child elements. These functions
+ *   can also be used to set additional properties or implement special logic
+ *   other than adding child elements: for example, for the 'fieldset' element
+ *   type, one of the functions in this array is form_process_fieldset(), which
+ *   adds the attributes and JavaScript needed to make the fieldset collapsible
+ *   if the #collapsible property is set. The #process functions are called in
+ *   preorder traversal, meaning they are called for the parent element first,
+ *   then for the child elements.
+ * - $element['#after_build']: An array of functions called after form_builder()
+ *   is done with its processing of the element. These are called in postorder
+ *   traversal, meaning they are called for the child elements first, then for
+ *   the parent element.
+ * There are similar properties containing callback functions invoked by
+ * _form_validate() and drupal_render(), appropriate for those operations.
+ *
+ * Developers are strongly encouraged to integrate the functionality needed by
+ * their form or module within one of these three pipelines, using the
+ * appropriate callback property, rather than implementing their own recursive
+ * traversal of a form array. This facilitates proper integration between
+ * multiple modules. For example, module developers are familiar with the
+ * relative order in which hook_form_alter() implementations and #process
+ * functions run. A custom traversal function that affects the building of a
+ * form is likely to not integrate with hook_form_alter() and #process in the
+ * expected way. Also, deep recursion within PHP is both slow and memory
+ * intensive, so it is best to minimize how often it's done.
+ *
+ * As stated above, each element's #process functions are executed after its
+ * #value has been set. This enables those functions to execute conditional
+ * logic based on the current value. However, all of form_builder() runs before
+ * drupal_validate_form() is called, so during #process function execution, the
+ * element's #value has not yet been validated, so any code that requires
+ * validated values must reside within a submit handler.
+ *
+ * As a security measure, user input is used for an element's #value only if the
+ * element exists within $form, is not disabled (as per the #disabled property),
+ * and can be accessed (as per the #access property, except that forms submitted
+ * using drupal_form_submit() bypass #access restrictions). When user input is
+ * ignored due to #disabled and #access restrictions, the element's default
+ * value is used.
+ *
+ * Because of the preorder traversal, where #process functions of an element run
+ * before user input for its child elements is processed, and because of the
+ * Form API security of user input processing with respect to #access and
+ * #disabled described above, this generally means that #process functions
+ * should not use an element's (unvalidated) #value to affect the #disabled or
+ * #access of child elements. Use-cases where a developer may be tempted to
+ * implement such conditional logic usually fall into one of two categories:
+ * - Where user input from the current submission must affect the structure of a
+ *   form, including properties like #access and #disabled that affect how the
+ *   next submission needs to be processed, a multi-step workflow is needed.
+ *   This is most commonly implemented with a submit handler setting persistent
+ *   data within $form_state based on *validated* values in
+ *   $form_state['values'] and setting $form_state['rebuild']. The form building
+ *   functions must then be implemented to use the $form_state data to rebuild
+ *   the form with the structure appropriate for the new state.
+ * - Where user input must affect the rendering of the form without affecting
+ *   its structure, the necessary conditional rendering logic should reside
+ *   within functions that run during the rendering phase (#pre_render, #theme,
+ *   #theme_wrappers, and #post_render).
+ *
+ * @param $form_id
+ *   A unique string identifying the form for validation, submission,
+ *   theming, and hook_form_alter functions.
+ * @param $element
+ *   An associative array containing the structure of the current element.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. In this
+ *   context, it is used to accumulate information about which button
+ *   was clicked when the form was submitted, as well as the sanitized
+ *   $_POST data.
+ */
+function form_builder($form_id, &$element, &$form_state) {
+  // Initialize as unprocessed.
+  $element['#processed'] = FALSE;
+
+  // Use element defaults.
+  if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = element_info($element['#type']))) {
+    // Overlay $info onto $element, retaining preexisting keys in $element.
+    $element += $info;
+    $element['#defaults_loaded'] = TRUE;
+  }
+  // Assign basic defaults common for all form elements.
+  $element += array(
+    '#required' => FALSE,
+    '#attributes' => array(),
+    '#title_display' => 'before',
+  );
+
+  // Special handling if we're on the top level form element.
+  if (isset($element['#type']) && $element['#type'] == 'form') {
+    if (!empty($element['#https']) && variable_get('https', FALSE) &&
+        !url_is_external($element['#action'])) {
+      global $base_root;
+
+      // Not an external URL so ensure that it is secure.
+      $element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action'];
+    }
+
+    // Store a reference to the complete form in $form_state prior to building
+    // the form. This allows advanced #process and #after_build callbacks to
+    // perform changes elsewhere in the form.
+    $form_state['complete form'] = &$element;
+
+    // Set a flag if we have a correct form submission. This is always TRUE for
+    // programmed forms coming from drupal_form_submit(), or if the form_id coming
+    // from the POST data is set and matches the current form_id.
+    if ($form_state['programmed'] || (!empty($form_state['input']) && (isset($form_state['input']['form_id']) && ($form_state['input']['form_id'] == $form_id)))) {
+      $form_state['process_input'] = TRUE;
+    }
+    else {
+      $form_state['process_input'] = FALSE;
+    }
+
+    // All form elements should have an #array_parents property.
+    $element['#array_parents'] = array();
+  }
+
+  if (!isset($element['#id'])) {
+    $element['#id'] = drupal_html_id('edit-' . implode('-', $element['#parents']));
+  }
+  // Handle input elements.
+  if (!empty($element['#input'])) {
+    _form_builder_handle_input_element($form_id, $element, $form_state);
+  }
+  // Allow for elements to expand to multiple elements, e.g., radios,
+  // checkboxes and files.
+  if (isset($element['#process']) && !$element['#processed']) {
+    foreach ($element['#process'] as $process) {
+      $element = $process($element, $form_state, $form_state['complete form']);
+    }
+    $element['#processed'] = TRUE;
+  }
+
+  // We start off assuming all form elements are in the correct order.
+  $element['#sorted'] = TRUE;
+
+  // Recurse through all child elements.
+  $count = 0;
+  foreach (element_children($element) as $key) {
+    // Prior to checking properties of child elements, their default properties
+    // need to be loaded.
+    if (isset($element[$key]['#type']) && empty($element[$key]['#defaults_loaded']) && ($info = element_info($element[$key]['#type']))) {
+      $element[$key] += $info;
+      $element[$key]['#defaults_loaded'] = TRUE;
+    }
+
+    // Don't squash an existing tree value.
+    if (!isset($element[$key]['#tree'])) {
+      $element[$key]['#tree'] = $element['#tree'];
+    }
+
+    // Deny access to child elements if parent is denied.
+    if (isset($element['#access']) && !$element['#access']) {
+      $element[$key]['#access'] = FALSE;
+    }
+
+    // Make child elements inherit their parent's #disabled and #allow_focus
+    // values unless they specify their own.
+    foreach (array('#disabled', '#allow_focus') as $property) {
+      if (isset($element[$property]) && !isset($element[$key][$property])) {
+        $element[$key][$property] = $element[$property];
+      }
+    }
+
+    // Don't squash existing parents value.
+    if (!isset($element[$key]['#parents'])) {
+      // Check to see if a tree of child elements is present. If so,
+      // continue down the tree if required.
+      $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key);
+    }
+    // Ensure #array_parents follows the actual form structure.
+    $array_parents = $element['#array_parents'];
+    $array_parents[] = $key;
+    $element[$key]['#array_parents'] = $array_parents;
+
+    // Assign a decimal placeholder weight to preserve original array order.
+    if (!isset($element[$key]['#weight'])) {
+      $element[$key]['#weight'] = $count/1000;
+    }
+    else {
+      // If one of the child elements has a weight then we will need to sort
+      // later.
+      unset($element['#sorted']);
+    }
+    $element[$key] = form_builder($form_id, $element[$key], $form_state);
+    $count++;
+  }
+
+  // The #after_build flag allows any piece of a form to be altered
+  // after normal input parsing has been completed.
+  if (isset($element['#after_build']) && !isset($element['#after_build_done'])) {
+    foreach ($element['#after_build'] as $function) {
+      $element = $function($element, $form_state);
+    }
+    $element['#after_build_done'] = TRUE;
+  }
+
+  // If there is a file element, we need to flip a flag so later the
+  // form encoding can be set.
+  if (isset($element['#type']) && $element['#type'] == 'file') {
+    $form_state['has_file_element'] = TRUE;
+  }
+
+  // Final tasks for the form element after form_builder() has run for all other
+  // elements.
+  if (isset($element['#type']) && $element['#type'] == 'form') {
+    // If there is a file element, we set the form encoding.
+    if (isset($form_state['has_file_element'])) {
+      $element['#attributes']['enctype'] = 'multipart/form-data';
+    }
+
+    // If a form contains a single textfield, and the ENTER key is pressed
+    // within it, Internet Explorer submits the form with no POST data
+    // identifying any submit button. Other browsers submit POST data as though
+    // the user clicked the first button. Therefore, to be as consistent as we
+    // can be across browsers, if no 'triggering_element' has been identified
+    // yet, default it to the first button.
+    if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) {
+      $form_state['triggering_element'] = $form_state['buttons'][0];
+    }
+
+    // If the triggering element specifies "button-level" validation and submit
+    // handlers to run instead of the default form-level ones, then add those to
+    // the form state.
+    foreach (array('validate', 'submit') as $type) {
+      if (isset($form_state['triggering_element']['#' . $type])) {
+        $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type];
+      }
+    }
+
+    // If the triggering element executes submit handlers, then set the form
+    // state key that's needed for those handlers to run.
+    if (!empty($form_state['triggering_element']['#executes_submit_callback'])) {
+      $form_state['submitted'] = TRUE;
+    }
+
+    // Special processing if the triggering element is a button.
+    if (isset($form_state['triggering_element']['#button_type'])) {
+      // Because there are several ways in which the triggering element could
+      // have been determined (including from input variables set by JavaScript
+      // or fallback behavior implemented for IE), and because buttons often
+      // have their #name property not derived from their #parents property, we
+      // can't assume that input processing that's happened up until here has
+      // resulted in $form_state['values'][BUTTON_NAME] being set. But it's
+      // common for forms to have several buttons named 'op' and switch on
+      // $form_state['values']['op'] during submit handler execution.
+      $form_state['values'][$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value'];
+
+      // @todo Legacy support. Remove in Drupal 8.
+      $form_state['clicked_button'] = $form_state['triggering_element'];
+    }
+  }
+  return $element;
+}
+
+/**
+ * Adds the #name and #value properties of an input element before rendering.
+ */
+function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
+  if (!isset($element['#name'])) {
+    $name = array_shift($element['#parents']);
+    $element['#name'] = $name;
+    if ($element['#type'] == 'file') {
+      // To make it easier to handle $_FILES in file.inc, we place all
+      // file fields in the 'files' array. Also, we do not support
+      // nested file names.
+      $element['#name'] = 'files[' . $element['#name'] . ']';
+    }
+    elseif (count($element['#parents'])) {
+      $element['#name'] .= '[' . implode('][', $element['#parents']) . ']';
+    }
+    array_unshift($element['#parents'], $name);
+  }
+
+  // Setting #disabled to TRUE results in user input being ignored, regardless
+  // of how the element is themed or whether JavaScript is used to change the
+  // control's attributes. However, it's good UI to let the user know that input
+  // is not wanted for the control. HTML supports two attributes for this:
+  // http://www.w3.org/TR/html401/interact/forms.html#h-17.12. If a form wants
+  // to start a control off with one of these attributes for UI purposes only,
+  // but still allow input to be processed if it's sumitted, it can set the
+  // desired attribute in #attributes directly rather than using #disabled.
+  // However, developers should think carefully about the accessibility
+  // implications of doing so: if the form expects input to be enterable under
+  // some condition triggered by JavaScript, how would someone who has
+  // JavaScript disabled trigger that condition? Instead, developers should
+  // consider whether a multi-step form would be more appropriate (#disabled can
+  // be changed from step to step). If one still decides to use JavaScript to
+  // affect when a control is enabled, then it is best for accessibility for the
+  // control to be enabled in the HTML, and disabled by JavaScript on document
+  // ready.
+  if (!empty($element['#disabled'])) {
+    if (!empty($element['#allow_focus'])) {
+      $element['#attributes']['readonly'] = 'readonly';
+    }
+    else {
+      $element['#attributes']['disabled'] = 'disabled';
+    }
+  }
+
+  // With JavaScript or other easy hacking, input can be submitted even for
+  // elements with #access=FALSE or #disabled=TRUE. For security, these must
+  // not be processed. Forms that set #disabled=TRUE on an element do not
+  // expect input for the element, and even forms submitted with
+  // drupal_form_submit() must not be able to get around this. Forms that set
+  // #access=FALSE on an element usually allow access for some users, so forms
+  // submitted with drupal_form_submit() may bypass access restriction and be
+  // treated as high-privilege users instead.
+  $process_input = empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])));
+
+  // Set the element's #value property.
+  if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
+    $value_callback = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value';
+    if ($process_input) {
+      // Get the input for the current element. NULL values in the input need to
+      // be explicitly distinguished from missing input. (see below)
+      $input_exists = NULL;
+      $input = drupal_array_get_nested_value($form_state['input'], $element['#parents'], $input_exists);
+      // For browser-submitted forms, the submitted values do not contain values
+      // for certain elements (empty multiple select, unchecked checkbox).
+      // During initial form processing, we add explicit NULL values for such
+      // elements in $form_state['input']. When rebuilding the form, we can
+      // distinguish elements having NULL input from elements that were not part
+      // of the initially submitted form and can therefore use default values
+      // for the latter, if required. Programmatically submitted forms can
+      // submit explicit NULL values when calling drupal_form_submit(), so we do
+      // not modify $form_state['input'] for them.
+      if (!$input_exists && !$form_state['rebuild'] && !$form_state['programmed']) {
+        // Add the necessary parent keys to $form_state['input'] and sets the
+        // element's input value to NULL.
+        drupal_array_set_nested_value($form_state['input'], $element['#parents'], NULL);
+        $input_exists = TRUE;
+      }
+      // If we have input for the current element, assign it to the #value
+      // property, optionally filtered through $value_callback.
+      if ($input_exists) {
+        if (function_exists($value_callback)) {
+          $element['#value'] = $value_callback($element, $input, $form_state);
+        }
+        if (!isset($element['#value']) && isset($input)) {
+          $element['#value'] = $input;
+        }
+      }
+      // Mark all posted values for validation.
+      if (isset($element['#value']) || (!empty($element['#required']))) {
+        $element['#needs_validation'] = TRUE;
+      }
+    }
+    // Load defaults.
+    if (!isset($element['#value'])) {
+      // Call #type_value without a second argument to request default_value handling.
+      if (function_exists($value_callback)) {
+        $element['#value'] = $value_callback($element, FALSE, $form_state);
+      }
+      // Final catch. If we haven't set a value yet, use the explicit default value.
+      // Avoid image buttons (which come with garbage value), so we only get value
+      // for the button actually clicked.
+      if (!isset($element['#value']) && empty($element['#has_garbage_value'])) {
+        $element['#value'] = isset($element['#default_value']) ? $element['#default_value'] : '';
+      }
+    }
+  }
+
+  // Determine which element (if any) triggered the submission of the form and
+  // keep track of all the clickable buttons in the form for
+  // form_state_values_clean(). Enforce the same input processing restrictions
+  // as above.
+  if ($process_input) {
+    // Detect if the element triggered the submission via Ajax.
+    if (_form_element_triggered_scripted_submission($element, $form_state)) {
+      $form_state['triggering_element'] = $element;
+    }
+
+    // If the form was submitted by the browser rather than via Ajax, then it
+    // can only have been triggered by a button, and we need to determine which
+    // button within the constraints of how browsers provide this information.
+    if (isset($element['#button_type'])) {
+      // All buttons in the form need to be tracked for
+      // form_state_values_clean() and for the form_builder() code that handles
+      // a form submission containing no button information in $_POST.
+      $form_state['buttons'][] = $element;
+      if (_form_button_was_clicked($element, $form_state)) {
+        $form_state['triggering_element'] = $element;
+      }
+    }
+  }
+
+  // Set the element's value in $form_state['values'], but only, if its key
+  // does not exist yet (a #value_callback may have already populated it).
+  if (!drupal_array_nested_key_exists($form_state['values'], $element['#parents'])) {
+    form_set_value($element, $element['#value'], $form_state);
+  }
+}
+
+/**
+ * Detects if an element triggered the form submission via Ajax.
+ *
+ * This detects button or non-button controls that trigger a form submission via
+ * Ajax or some other scriptable environment. These environments can set the
+ * special input key '_triggering_element_name' to identify the triggering
+ * element. If the name alone doesn't identify the element uniquely, the input
+ * key '_triggering_element_value' may also be set to require a match on element
+ * value. An example where this is needed is if there are several buttons all
+ * named 'op', and only differing in their value.
+ */
+function _form_element_triggered_scripted_submission($element, &$form_state) {
+  if (!empty($form_state['input']['_triggering_element_name']) && $element['#name'] == $form_state['input']['_triggering_element_name']) {
+    if (empty($form_state['input']['_triggering_element_value']) || $form_state['input']['_triggering_element_value'] == $element['#value']) {
+      return TRUE;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Determines if a given button triggered the form submission.
+ *
+ * This detects button controls that trigger a form submission by being clicked
+ * and having the click processed by the browser rather than being captured by
+ * JavaScript. Essentially, it detects if the button's name and value are part
+ * of the POST data, but with extra code to deal with the convoluted way in
+ * which browsers submit data for image button clicks.
+ *
+ * This does not detect button clicks processed by Ajax (that is done in
+ * _form_element_triggered_scripted_submission()) and it does not detect form
+ * submissions from Internet Explorer in response to an ENTER key pressed in a
+ * textfield (form_builder() has extra code for that).
+ *
+ * Because this function contains only part of the logic needed to determine
+ * $form_state['triggering_element'], it should not be called from anywhere
+ * other than within the Form API. Form validation and submit handlers needing
+ * to know which button was clicked should get that information from
+ * $form_state['triggering_element'].
+ */
+function _form_button_was_clicked($element, &$form_state) {
+  // First detect normal 'vanilla' button clicks. Traditionally, all
+  // standard buttons on a form share the same name (usually 'op'),
+  // and the specific return value is used to determine which was
+  // clicked. This ONLY works as long as $form['#name'] puts the
+  // value at the top level of the tree of $_POST data.
+  if (isset($form_state['input'][$element['#name']]) && $form_state['input'][$element['#name']] == $element['#value']) {
+    return TRUE;
+  }
+  // When image buttons are clicked, browsers do NOT pass the form element
+  // value in $_POST. Instead they pass an integer representing the
+  // coordinates of the click on the button image. This means that image
+  // buttons MUST have unique $form['#name'] values, but the details of
+  // their $_POST data should be ignored.
+  elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') {
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * Removes internal Form API elements and buttons from submitted form values.
+ *
+ * This function can be used when a module wants to store all submitted form
+ * values, for example, by serializing them into a single database column. In
+ * such cases, all internal Form API values and all form button elements should
+ * not be contained, and this function allows to remove them before the module
+ * proceeds to storage. Next to button elements, the following internal values
+ * are removed:
+ * - form_id
+ * - form_token
+ * - form_build_id
+ * - op
+ *
+ * @param $form_state
+ *   A keyed array containing the current state of the form, including
+ *   submitted form values; altered by reference.
+ */
+function form_state_values_clean(&$form_state) {
+  // Remove internal Form API values.
+  unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['op']);
+
+  // Remove button values.
+  // form_builder() collects all button elements in a form. We remove the button
+  // value separately for each button element.
+  foreach ($form_state['buttons'] as $button) {
+    // Remove this button's value from the submitted form values by finding
+    // the value corresponding to this button.
+    // We iterate over the #parents of this button and move a reference to
+    // each parent in $form_state['values']. For example, if #parents is:
+    //   array('foo', 'bar', 'baz')
+    // then the corresponding $form_state['values'] part will look like this:
+    // array(
+    //   'foo' => array(
+    //     'bar' => array(
+    //       'baz' => 'button_value',
+    //     ),
+    //   ),
+    // )
+    // We start by (re)moving 'baz' to $last_parent, so we are able unset it
+    // at the end of the iteration. Initially, $values will contain a
+    // reference to $form_state['values'], but in the iteration we move the
+    // reference to $form_state['values']['foo'], and finally to
+    // $form_state['values']['foo']['bar'], which is the level where we can
+    // unset 'baz' (that is stored in $last_parent).
+    $parents = $button['#parents'];
+    $last_parent = array_pop($parents);
+    $key_exists = NULL;
+    $values = &drupal_array_get_nested_value($form_state['values'], $parents, $key_exists);
+    if ($key_exists && is_array($values)) {
+      unset($values[$last_parent]);
+    }
+  }
+}
+
+/**
+ * Determines the value for an image button form element.
+ *
+ * @param $form
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ * @param $form_state
+ *   A keyed array containing the current state of the form.
+ *
+ * @return
+ *   The data that will appear in the $form_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_image_button_value($form, $input, $form_state) {
+  if ($input !== FALSE) {
+    if (!empty($input)) {
+      // If we're dealing with Mozilla or Opera, we're lucky. It will
+      // return a proper value, and we can get on with things.
+      return $form['#return_value'];
+    }
+    else {
+      // Unfortunately, in IE we never get back a proper value for THIS
+      // form element. Instead, we get back two split values: one for the
+      // X and one for the Y coordinates on which the user clicked the
+      // button. We'll find this element in the #post data, and search
+      // in the same spot for its name, with '_x'.
+      $input = $form_state['input'];
+      foreach (explode('[', $form['#name']) as $element_name) {
+        // chop off the ] that may exist.
+        if (substr($element_name, -1) == ']') {
+          $element_name = substr($element_name, 0, -1);
+        }
+
+        if (!isset($input[$element_name])) {
+          if (isset($input[$element_name . '_x'])) {
+            return $form['#return_value'];
+          }
+          return NULL;
+        }
+        $input = $input[$element_name];
+      }
+      return $form['#return_value'];
+    }
+  }
+}
+
+/**
+ * Determines the value for a checkbox form element.
+ *
+ * @param $form
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_checkbox_value($element, $input = FALSE) {
+  if ($input === FALSE) {
+    // Use #default_value as the default value of a checkbox, except change
+    // NULL to 0, because _form_builder_handle_input_element() would otherwise
+    // replace NULL with empty string, but an empty string is a potentially
+    // valid value for a checked checkbox.
+    return isset($element['#default_value']) ? $element['#default_value'] : 0;
+  }
+  else {
+    // Checked checkboxes are submitted with a value (possibly '0' or ''):
+    // http://www.w3.org/TR/html401/interact/forms.html#successful-controls.
+    // For checked checkboxes, browsers submit the string version of
+    // #return_value, but we return the original #return_value. For unchecked
+    // checkboxes, browsers submit nothing at all, but
+    // _form_builder_handle_input_element() detects this, and calls this
+    // function with $input=NULL. Returning NULL from a value callback means to
+    // use the default value, which is not what is wanted when an unchecked
+    // checkbox is submitted, so we use integer 0 as the value indicating an
+    // unchecked checkbox. Therefore, modules must not use integer 0 as a
+    // #return_value, as doing so results in the checkbox always being treated
+    // as unchecked. The string '0' is allowed for #return_value. The most
+    // common use-case for setting #return_value to either 0 or '0' is for the
+    // first option within a 0-indexed array of checkboxes, and for this,
+    // form_process_checkboxes() uses the string rather than the integer.
+    return isset($input) ? $element['#return_value'] : 0;
+  }
+}
+
+/**
+ * Determines the value for a checkboxes form element.
+ *
+ * @param $element
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_checkboxes_value($element, $input = FALSE) {
+  if ($input === FALSE) {
+    $value = array();
+    $element += array('#default_value' => array());
+    foreach ($element['#default_value'] as $key) {
+      $value[$key] = $key;
+    }
+    return $value;
+  }
+  elseif (is_array($input)) {
+    // Programmatic form submissions use NULL to indicate that a checkbox
+    // should be unchecked; see drupal_form_submit(). We therefore remove all
+    // NULL elements from the array before constructing the return value, to
+    // simulate the behavior of web browsers (which do not send unchecked
+    // checkboxes to the server at all). This will not affect non-programmatic
+    // form submissions, since all values in $_POST are strings.
+    foreach ($input as $key => $value) {
+      if (!isset($value)) {
+        unset($input[$key]);
+      }
+    }
+    return drupal_map_assoc($input);
+  }
+  else {
+    return array();
+  }
+}
+
+/**
+ * Determines the value for a tableselect form element.
+ *
+ * @param $element
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_tableselect_value($element, $input = FALSE) {
+  // If $element['#multiple'] == FALSE, then radio buttons are displayed and
+  // the default value handling is used.
+  if (isset($element['#multiple']) && $element['#multiple']) {
+    // Checkboxes are being displayed with the default value coming from the
+    // keys of the #default_value property. This differs from the checkboxes
+    // element which uses the array values.
+    if ($input === FALSE) {
+      $value = array();
+      $element += array('#default_value' => array());
+      foreach ($element['#default_value'] as $key => $flag) {
+        if ($flag) {
+          $value[$key] = $key;
+        }
+      }
+      return $value;
+    }
+    else {
+      return is_array($input) ? drupal_map_assoc($input) : array();
+    }
+  }
+}
+
+/**
+ * Form value callback: Determines the value for a #type radios form element.
+ *
+ * @param $element
+ *   The form element whose value is being populated.
+ * @param $input
+ *   (optional) The incoming input to populate the form element. If FALSE, the
+ *   element's default value is returned. Defaults to FALSE.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection for
+ *   this element.
+ */
+function form_type_radios_value(&$element, $input = FALSE) {
+  if ($input !== FALSE) {
+    // When there's user input (including NULL), return it as the value.
+    // However, if NULL is submitted, _form_builder_handle_input_element() will
+    // apply the default value, and we want that validated against #options
+    // unless it's empty. (An empty #default_value, such as NULL or FALSE, can
+    // be used to indicate that no radio button is selected by default.)
+    if (!isset($input) && !empty($element['#default_value'])) {
+      $element['#needs_validation'] = TRUE;
+    }
+    return $input;
+  }
+  else {
+    // For default value handling, simply return #default_value. Additionally,
+    // for a NULL default value, set #has_garbage_value to prevent
+    // _form_builder_handle_input_element() converting the NULL to an empty
+    // string, so that code can distinguish between nothing selected and the
+    // selection of a radio button whose value is an empty string.
+    $value = isset($element['#default_value']) ? $element['#default_value'] : NULL;
+    if (!isset($value)) {
+      $element['#has_garbage_value'] = TRUE;
+    }
+    return $value;
+  }
+}
+
+/**
+ * Determines the value for a password_confirm form element.
+ *
+ * @param $element
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_password_confirm_value($element, $input = FALSE) {
+  if ($input === FALSE) {
+    $element += array('#default_value' => array());
+    return $element['#default_value'] + array('pass1' => '', 'pass2' => '');
+  }
+}
+
+/**
+ * Determines the value for a select form element.
+ *
+ * @param $element
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_select_value($element, $input = FALSE) {
+  if ($input !== FALSE) {
+    if (isset($element['#multiple']) && $element['#multiple']) {
+      // If an enabled multi-select submits NULL, it means all items are
+      // unselected. A disabled multi-select always submits NULL, and the
+      // default value should be used.
+      if (empty($element['#disabled'])) {
+        return (is_array($input)) ? drupal_map_assoc($input) : array();
+      }
+      else {
+        return (isset($element['#default_value']) && is_array($element['#default_value'])) ? $element['#default_value'] : array();
+      }
+    }
+    // Non-multiple select elements may have an empty option preprended to them
+    // (see form_process_select()). When this occurs, usually #empty_value is
+    // an empty string, but some forms set #empty_value to integer 0 or some
+    // other non-string constant. PHP receives all submitted form input as
+    // strings, but if the empty option is selected, set the value to match the
+    // empty value exactly.
+    elseif (isset($element['#empty_value']) && $input === (string) $element['#empty_value']) {
+      return $element['#empty_value'];
+    }
+    else {
+      return $input;
+    }
+  }
+}
+
+/**
+ * Determines the value for a textfield form element.
+ *
+ * @param $element
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_textfield_value($element, $input = FALSE) {
+  if ($input !== FALSE && $input !== NULL) {
+    // Equate $input to the form value to ensure it's marked for
+    // validation.
+    return str_replace(array("\r", "\n"), '', $input);
+  }
+}
+
+/**
+ * Determines the value for form's token value.
+ *
+ * @param $element
+ *   The form element whose value is being populated.
+ * @param $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_token_value($element, $input = FALSE) {
+  if ($input !== FALSE) {
+    return (string) $input;
+  }
+}
+
+/**
+ * Changes submitted form values during form validation.
+ *
+ * Use this function to change the submitted value of a form element in a form
+ * validation function, so that the changed value persists in $form_state
+ * through the remaining validation and submission handlers. It does not change
+ * the value in $element['#value'], only in $form_state['values'], which is
+ * where submitted values are always stored.
+ *
+ * Note that form validation functions are specified in the '#validate'
+ * component of the form array (the value of $form['#validate'] is an array of
+ * validation function names). If the form does not originate in your module,
+ * you can implement hook_form_FORM_ID_alter() to add a validation function
+ * to $form['#validate'].
+ *
+ * @param $element
+ *   The form element that should have its value updated; in most cases you can
+ *   just pass in the element from the $form array, although the only component
+ *   that is actually used is '#parents'. If constructing yourself, set
+ *   $element['#parents'] to be an array giving the path through the form
+ *   array's keys to the element whose value you want to update. For instance,
+ *   if you want to update the value of $form['elem1']['elem2'], which should be
+ *   stored in $form_state['values']['elem1']['elem2'], you would set
+ *   $element['#parents'] = array('elem1','elem2').
+ * @param $value
+ *   The new value for the form element.
+ * @param $form_state
+ *   Form state array where the value change should be recorded.
+ */
+function form_set_value($element, $value, &$form_state) {
+  drupal_array_set_nested_value($form_state['values'], $element['#parents'], $value, TRUE);
+}
+
+/**
+ * Allows PHP array processing of multiple select options with the same value.
+ *
+ * Used for form select elements which need to validate HTML option groups
+ * and multiple options which may return the same value. Associative PHP arrays
+ * cannot handle these structures, since they share a common key.
+ *
+ * @param $array
+ *   The form options array to process.
+ *
+ * @return
+ *   An array with all hierarchical elements flattened to a single array.
+ */
+function form_options_flatten($array) {
+  // Always reset static var when first entering the recursion.
+  drupal_static_reset('_form_options_flatten');
+  return _form_options_flatten($array);
+}
+
+/**
+ * Iterates over an array and returns a flat array with duplicate keys removed.
+ *
+ * This function also handles cases where objects are passed as array values.
+ */
+function _form_options_flatten($array) {
+  $return = &drupal_static(__FUNCTION__);
+
+  foreach ($array as $key => $value) {
+    if (is_object($value)) {
+      _form_options_flatten($value->option);
+    }
+    elseif (is_array($value)) {
+      _form_options_flatten($value);
+    }
+    else {
+      $return[$key] = 1;
+    }
+  }
+
+  return $return;
+}
+
+/**
+ * Processes a select list form element.
+ *
+ * This process callback is mandatory for select fields, since all user agents
+ * automatically preselect the first available option of single (non-multiple)
+ * select lists.
+ *
+ * @param $element
+ *   The form element to process. Properties used:
+ *   - #multiple: (optional) Indicates whether one or more options can be
+ *     selected. Defaults to FALSE.
+ *   - #default_value: Must be NULL or not set in case there is no value for the
+ *     element yet, in which case a first default option is inserted by default.
+ *     Whether this first option is a valid option depends on whether the field
+ *     is #required or not.
+ *   - #required: (optional) Whether the user needs to select an option (TRUE)
+ *     or not (FALSE). Defaults to FALSE.
+ *   - #empty_option: (optional) The label to show for the first default option.
+ *     By default, the label is automatically set to "- Please select -" for a
+ *     required field and "- None -" for an optional field.
+ *   - #empty_value: (optional) The value for the first default option, which is
+ *     used to determine whether the user submitted a value or not.
+ *     - If #required is TRUE, this defaults to '' (an empty string).
+ *     - If #required is not TRUE and this value isn't set, then no extra option
+ *       is added to the select control, leaving the control in a slightly
+ *       illogical state, because there's no way for the user to select nothing,
+ *       since all user agents automatically preselect the first available
+ *       option. But people are used to this being the behavior of select
+ *       controls.
+ *       @todo Address the above issue in Drupal 8.
+ *     - If #required is not TRUE and this value is set (most commonly to an
+ *       empty string), then an extra option (see #empty_option above)
+ *       representing a "non-selection" is added with this as its value.
+ *
+ * @see _form_validate()
+ */
+function form_process_select($element) {
+  // #multiple select fields need a special #name.
+  if ($element['#multiple']) {
+    $element['#attributes']['multiple'] = 'multiple';
+    $element['#attributes']['name'] = $element['#name'] . '[]';
+  }
+  // A non-#multiple select needs special handling to prevent user agents from
+  // preselecting the first option without intention. #multiple select lists do
+  // not get an empty option, as it would not make sense, user interface-wise.
+  else {
+    $required = $element['#required'];
+    // If the element is required and there is no #default_value, then add an
+    // empty option that will fail validation, so that the user is required to
+    // make a choice. Also, if there's a value for #empty_value or
+    // #empty_option, then add an option that represents emptiness.
+    if (($required && !isset($element['#default_value'])) || isset($element['#empty_value']) || isset($element['#empty_option'])) {
+      $element += array(
+        '#empty_value' => '',
+        '#empty_option' => $required ? t('- Select -') : t('- None -'),
+      );
+      // The empty option is prepended to #options and purposively not merged
+      // to prevent another option in #options mistakenly using the same value
+      // as #empty_value.
+      $empty_option = array($element['#empty_value'] => $element['#empty_option']);
+      $element['#options'] = $empty_option + $element['#options'];
+    }
+  }
+  return $element;
+}
+
+/**
+ * Returns HTML for a select form element.
+ *
+ * It is possible to group options together; to do this, change the format of
+ * $options to an associative array in which the keys are group labels, and the
+ * values are associative arrays in the normal $options format.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #options, #description, #extra,
+ *     #multiple, #required, #name, #attributes, #size.
+ *
+ * @ingroup themeable
+ */
+function theme_select($variables) {
+  $element = $variables['element'];
+  element_set_attributes($element, array('id', 'name', 'size'));
+  _form_set_class($element, array('form-select'));
+
+  return '<select' . drupal_attributes($element['#attributes']) . '>' . form_select_options($element) . '</select>';
+}
+
+/**
+ * Converts a select form element's options array into HTML.
+ *
+ * @param $element
+ *   An associative array containing the properties of the element.
+ * @param $choices
+ *   Mixed: Either an associative array of items to list as choices, or an
+ *   object with an 'option' member that is an associative array. This
+ *   parameter is only used internally and should not be passed.
+ *
+ * @return
+ *   An HTML string of options for the select form element.
+ */
+function form_select_options($element, $choices = NULL) {
+  if (!isset($choices)) {
+    $choices = $element['#options'];
+  }
+  // array_key_exists() accommodates the rare event where $element['#value'] is NULL.
+  // isset() fails in this situation.
+  $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
+  $value_is_array = $value_valid && is_array($element['#value']);
+  $options = '';
+  foreach ($choices as $key => $choice) {
+    if (is_array($choice)) {
+      $options .= '<optgroup label="' . $key . '">';
+      $options .= form_select_options($element, $choice);
+      $options .= '</optgroup>';
+    }
+    elseif (is_object($choice)) {
+      $options .= form_select_options($element, $choice->option);
+    }
+    else {
+      $key = (string) $key;
+      if ($value_valid && (!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value'])))) {
+        $selected = ' selected="selected"';
+      }
+      else {
+        $selected = '';
+      }
+      $options .= '<option value="' . check_plain($key) . '"' . $selected . '>' . check_plain($choice) . '</option>';
+    }
+  }
+  return $options;
+}
+
+/**
+ * Returns the indexes of a select element's options matching a given key.
+ *
+ * This function is useful if you need to modify the options that are
+ * already in a form element; for example, to remove choices which are
+ * not valid because of additional filters imposed by another module.
+ * One example might be altering the choices in a taxonomy selector.
+ * To correctly handle the case of a multiple hierarchy taxonomy,
+ * #options arrays can now hold an array of objects, instead of a
+ * direct mapping of keys to labels, so that multiple choices in the
+ * selector can have the same key (and label). This makes it difficult
+ * to manipulate directly, which is why this helper function exists.
+ *
+ * This function does not support optgroups (when the elements of the
+ * #options array are themselves arrays), and will return FALSE if
+ * arrays are found. The caller must either flatten/restore or
+ * manually do their manipulations in this case, since returning the
+ * index is not sufficient, and supporting this would make the
+ * "helper" too complicated and cumbersome to be of any help.
+ *
+ * As usual with functions that can return array() or FALSE, do not
+ * forget to use === and !== if needed.
+ *
+ * @param $element
+ *   The select element to search.
+ * @param $key
+ *   The key to look for.
+ *
+ * @return
+ *   An array of indexes that match the given $key. Array will be
+ *   empty if no elements were found. FALSE if optgroups were found.
+ */
+function form_get_options($element, $key) {
+  $keys = array();
+  foreach ($element['#options'] as $index => $choice) {
+    if (is_array($choice)) {
+      return FALSE;
+    }
+    elseif (is_object($choice)) {
+      if (isset($choice->option[$key])) {
+        $keys[] = $index;
+      }
+    }
+    elseif ($index == $key) {
+      $keys[] = $index;
+    }
+  }
+  return $keys;
+}
+
+/**
+ * Returns HTML for a fieldset form element and its children.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #attributes, #children, #collapsed, #collapsible,
+ *     #description, #id, #title, #value.
+ *
+ * @ingroup themeable
+ */
+function theme_fieldset($variables) {
+  $element = $variables['element'];
+  element_set_attributes($element, array('id'));
+  _form_set_class($element, array('form-wrapper'));
+
+  $output = '<fieldset' . drupal_attributes($element['#attributes']) . '>';
+  if (!empty($element['#title'])) {
+    // Always wrap fieldset legends in a SPAN for CSS positioning.
+    $output .= '<legend><span class="fieldset-legend">' . $element['#title'] . '</span></legend>';
+  }
+  $output .= '<div class="fieldset-wrapper">';
+  if (!empty($element['#description'])) {
+    $output .= '<div class="fieldset-description">' . $element['#description'] . '</div>';
+  }
+  $output .= $element['#children'];
+  if (isset($element['#value'])) {
+    $output .= $element['#value'];
+  }
+  $output .= '</div>';
+  $output .= "</fieldset>\n";
+  return $output;
+}
+
+/**
+ * Returns HTML for a radio button form element.
+ *
+ * Note: The input "name" attribute needs to be sanitized before output, which
+ *       is currently done by passing all attributes to drupal_attributes().
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #required, #return_value, #value, #attributes, #title,
+ *     #description
+ *
+ * @ingroup themeable
+ */
+function theme_radio($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'radio';
+  element_set_attributes($element, array('id', 'name', '#return_value' => 'value'));
+
+  if (isset($element['#return_value']) && $element['#value'] !== FALSE && $element['#value'] == $element['#return_value']) {
+    $element['#attributes']['checked'] = 'checked';
+  }
+  _form_set_class($element, array('form-radio'));
+
+  return '<input' . drupal_attributes($element['#attributes']) . ' />';
+}
+
+/**
+ * Returns HTML for a set of radio button form elements.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #options, #description, #required,
+ *     #attributes, #children.
+ *
+ * @ingroup themeable
+ */
+function theme_radios($variables) {
+  $element = $variables['element'];
+  $attributes = array();
+  if (isset($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  $attributes['class'] = 'form-radios';
+  if (!empty($element['#attributes']['class'])) {
+    $attributes['class'] .= ' ' . implode(' ', $element['#attributes']['class']);
+  }
+  if (isset($element['#attributes']['title'])) {
+    $attributes['title'] = $element['#attributes']['title'];
+  }
+  return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
+}
+
+/**
+ * Expand a password_confirm field into two text boxes.
+ */
+function form_process_password_confirm($element) {
+  $element['pass1'] =  array(
+    '#type' => 'password',
+    '#title' => t('Password'),
+    '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'],
+    '#required' => $element['#required'],
+    '#attributes' => array('class' => array('password-field')),
+  );
+  $element['pass2'] =  array(
+    '#type' => 'password',
+    '#title' => t('Confirm password'),
+    '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'],
+    '#required' => $element['#required'],
+    '#attributes' => array('class' => array('password-confirm')),
+  );
+  $element['#element_validate'] = array('password_confirm_validate');
+  $element['#tree'] = TRUE;
+
+  if (isset($element['#size'])) {
+    $element['pass1']['#size'] = $element['pass2']['#size'] = $element['#size'];
+  }
+
+  return $element;
+}
+
+/**
+ * Validates a password_confirm element.
+ */
+function password_confirm_validate($element, &$element_state) {
+  $pass1 = trim($element['pass1']['#value']);
+  $pass2 = trim($element['pass2']['#value']);
+  if (!empty($pass1) || !empty($pass2)) {
+    if (strcmp($pass1, $pass2)) {
+      form_error($element, t('The specified passwords do not match.'));
+    }
+  }
+  elseif ($element['#required'] && !empty($element_state['input'])) {
+    form_error($element, t('Password field is required.'));
+  }
+
+  // Password field must be converted from a two-element array into a single
+  // string regardless of validation results.
+  form_set_value($element['pass1'], NULL, $element_state);
+  form_set_value($element['pass2'], NULL, $element_state);
+  form_set_value($element, $pass1, $element_state);
+
+  return $element;
+
+}
+
+/**
+ * Returns HTML for a date selection form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #options, #description, #required,
+ *     #attributes.
+ *
+ * @ingroup themeable
+ */
+function theme_date($variables) {
+  $element = $variables['element'];
+
+  $attributes = array();
+  if (isset($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  if (!empty($element['#attributes']['class'])) {
+    $attributes['class'] = (array) $element['#attributes']['class'];
+  }
+  $attributes['class'][] = 'container-inline';
+
+  return '<div' . drupal_attributes($attributes) . '>' . drupal_render_children($element) . '</div>';
+}
+
+/**
+ * Expands a date element into year, month, and day select elements.
+ */
+function form_process_date($element) {
+  // Default to current date
+  if (empty($element['#value'])) {
+    $element['#value'] = array(
+      'day' => format_date(REQUEST_TIME, 'custom', 'j'),
+      'month' => format_date(REQUEST_TIME, 'custom', 'n'),
+      'year' => format_date(REQUEST_TIME, 'custom', 'Y'),
+    );
+  }
+
+  $element['#tree'] = TRUE;
+
+  // Determine the order of day, month, year in the site's chosen date format.
+  $format = variable_get('date_format_short', 'm/d/Y - H:i');
+  $sort = array();
+  $sort['day'] = max(strpos($format, 'd'), strpos($format, 'j'));
+  $sort['month'] = max(strpos($format, 'm'), strpos($format, 'M'));
+  $sort['year'] = strpos($format, 'Y');
+  asort($sort);
+  $order = array_keys($sort);
+
+  // Output multi-selector for date.
+  foreach ($order as $type) {
+    switch ($type) {
+      case 'day':
+        $options = drupal_map_assoc(range(1, 31));
+        $title = t('Day');
+        break;
+
+      case 'month':
+        $options = drupal_map_assoc(range(1, 12), 'map_month');
+        $title = t('Month');
+        break;
+
+      case 'year':
+        $options = drupal_map_assoc(range(1900, 2050));
+        $title = t('Year');
+        break;
+    }
+
+    $element[$type] = array(
+      '#type' => 'select',
+      '#title' => $title,
+      '#title_display' => 'invisible',
+      '#value' => $element['#value'][$type],
+      '#attributes' => $element['#attributes'],
+      '#options' => $options,
+    );
+  }
+
+  return $element;
+}
+
+/**
+ * Validates the date type to prevent invalid dates (e.g., February 30, 2006).
+ */
+function date_validate($element) {
+  if (!checkdate($element['#value']['month'], $element['#value']['day'], $element['#value']['year'])) {
+    form_error($element, t('The specified date is invalid.'));
+  }
+}
+
+/**
+ * Helper function for usage with drupal_map_assoc to display month names.
+ */
+function map_month($month) {
+  $months = &drupal_static(__FUNCTION__, array(
+    1 => 'Jan',
+    2 => 'Feb',
+    3 => 'Mar',
+    4 => 'Apr',
+    5 => 'May',
+    6 => 'Jun',
+    7 => 'Jul',
+    8 => 'Aug',
+    9 => 'Sep',
+    10 => 'Oct',
+    11 => 'Nov',
+    12 => 'Dec',
+  ));
+  return t($months[$month]);
+}
+
+/**
+ * Sets the value for a weight element, with zero as a default.
+ */
+function weight_value(&$form) {
+  if (isset($form['#default_value'])) {
+    $form['#value'] = $form['#default_value'];
+  }
+  else {
+    $form['#value'] = 0;
+  }
+}
+
+/**
+ * Expands a radios element into individual radio elements.
+ */
+function form_process_radios($element) {
+  if (count($element['#options']) > 0) {
+    $weight = 0;
+    foreach ($element['#options'] as $key => $choice) {
+      // Maintain order of options as defined in #options, in case the element
+      // defines custom option sub-elements, but does not define all option
+      // sub-elements.
+      $weight += 0.001;
+
+      $element += array($key => array());
+      // Generate the parents as the autogenerator does, so we will have a
+      // unique id for each radio button.
+      $parents_for_id = array_merge($element['#parents'], array($key));
+      $element[$key] += array(
+        '#type' => 'radio',
+        '#title' => $choice,
+        // The key is sanitized in drupal_attributes() during output from the
+        // theme function.
+        '#return_value' => $key,
+        // Use default or FALSE. A value of FALSE means that the radio button is
+        // not 'checked'.
+        '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : FALSE,
+        '#attributes' => $element['#attributes'],
+        '#parents' => $element['#parents'],
+        '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
+        '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+        '#weight' => $weight,
+      );
+    }
+  }
+  return $element;
+}
+
+/**
+ * Returns HTML for a checkbox form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #return_value, #description, #required,
+ *     #attributes, #checked.
+ *
+ * @ingroup themeable
+ */
+function theme_checkbox($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'checkbox';
+  element_set_attributes($element, array('id', 'name', '#return_value' => 'value'));
+
+  // Unchecked checkbox has #value of integer 0.
+  if (!empty($element['#checked'])) {
+    $element['#attributes']['checked'] = 'checked';
+  }
+  _form_set_class($element, array('form-checkbox'));
+
+  return '<input' . drupal_attributes($element['#attributes']) . ' />';
+}
+
+/**
+ * Returns HTML for a set of checkbox form elements.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #children, #attributes.
+ *
+ * @ingroup themeable
+ */
+function theme_checkboxes($variables) {
+  $element = $variables['element'];
+  $attributes = array();
+  if (isset($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  $attributes['class'][] = 'form-checkboxes';
+  if (!empty($element['#attributes']['class'])) {
+    $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']);
+  }
+  if (isset($element['#attributes']['title'])) {
+    $attributes['title'] = $element['#attributes']['title'];
+  }
+  return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
+}
+
+/**
+ * Adds form element theming to an element if its title or description is set.
+ *
+ * This is used as a pre render function for checkboxes and radios.
+ */
+function form_pre_render_conditional_form_element($element) {
+  $t = get_t();
+  // Set the element's title attribute to show #title as a tooltip, if needed.
+  if (isset($element['#title']) && $element['#title_display'] == 'attribute') {
+    $element['#attributes']['title'] = $element['#title'];
+    if (!empty($element['#required'])) {
+      // Append an indication that this field is required.
+      $element['#attributes']['title'] .= ' (' . $t('Required') . ')';
+    }
+  }
+
+  if (isset($element['#title']) || isset($element['#description'])) {
+    $element['#theme_wrappers'][] = 'form_element';
+  }
+  return $element;
+}
+
+/**
+ * Sets the #checked property of a checkbox element.
+ */
+function form_process_checkbox($element, $form_state) {
+  $value = $element['#value'];
+  $return_value = $element['#return_value'];
+  // On form submission, the #value of an available and enabled checked
+  // checkbox is #return_value, and the #value of an available and enabled
+  // unchecked checkbox is integer 0. On not submitted forms, and for
+  // checkboxes with #access=FALSE or #disabled=TRUE, the #value is
+  // #default_value (integer 0 if #default_value is NULL). Most of the time,
+  // a string comparison of #value and #return_value is sufficient for
+  // determining the "checked" state, but a value of TRUE always means checked
+  // (even if #return_value is 'foo'), and a value of FALSE or integer 0 always
+  // means unchecked (even if #return_value is '' or '0').
+  if ($value === TRUE || $value === FALSE || $value === 0) {
+    $element['#checked'] = (bool) $value;
+  }
+  else {
+    // Compare as strings, so that 15 is not considered equal to '15foo', but 1
+    // is considered equal to '1'. This cast does not imply that either #value
+    // or #return_value is expected to be a string.
+    $element['#checked'] = ((string) $value === (string) $return_value);
+  }
+  return $element;
+}
+
+/**
+ * Processes a checkboxes form element.
+ */
+function form_process_checkboxes($element) {
+  $value = is_array($element['#value']) ? $element['#value'] : array();
+  $element['#tree'] = TRUE;
+  if (count($element['#options']) > 0) {
+    if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
+      $element['#default_value'] = array();
+    }
+    $weight = 0;
+    foreach ($element['#options'] as $key => $choice) {
+      // Integer 0 is not a valid #return_value, so use '0' instead.
+      // @see form_type_checkbox_value().
+      // @todo For Drupal 8, cast all integer keys to strings for consistency
+      //   with form_process_radios().
+      if ($key === 0) {
+        $key = '0';
+      }
+      // Maintain order of options as defined in #options, in case the element
+      // defines custom option sub-elements, but does not define all option
+      // sub-elements.
+      $weight += 0.001;
+
+      $element += array($key => array());
+      $element[$key] += array(
+        '#type' => 'checkbox',
+        '#title' => $choice,
+        '#return_value' => $key,
+        '#default_value' => isset($value[$key]) ? $key : NULL,
+        '#attributes' => $element['#attributes'],
+        '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+        '#weight' => $weight,
+      );
+    }
+  }
+  return $element;
+}
+
+/**
+ * Processes a form actions container element.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   form actions container.
+ * @param $form_state
+ *   The $form_state array for the form this element belongs to.
+ *
+ * @return
+ *   The processed element.
+ */
+function form_process_actions($element, &$form_state) {
+  $element['#attributes']['class'][] = 'form-actions';
+  return $element;
+}
+
+/**
+ * Processes a container element.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   container.
+ * @param $form_state
+ *   The $form_state array for the form this element belongs to.
+ *
+ * @return
+ *   The processed element.
+ */
+function form_process_container($element, &$form_state) {
+  // Generate the ID of the element if it's not explicitly given.
+  if (!isset($element['#id'])) {
+    $element['#id'] = drupal_html_id(implode('-', $element['#parents']) . '-wrapper');
+  }
+  return $element;
+}
+
+/**
+ * Returns HTML to wrap child elements in a container.
+ *
+ * Used for grouped form items. Can also be used as a #theme_wrapper for any
+ * renderable element, to surround it with a <div> and add attributes such as
+ * classes or an HTML id.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #id, #attributes, #children.
+ *
+ * @ingroup themeable
+ */
+function theme_container($variables) {
+  $element = $variables['element'];
+
+  // Special handling for form elements.
+  if (isset($element['#array_parents'])) {
+    // Assign an html ID.
+    if (!isset($element['#attributes']['id'])) {
+      $element['#attributes']['id'] = $element['#id'];
+    }
+    // Add the 'form-wrapper' class.
+    $element['#attributes']['class'][] = 'form-wrapper';
+  }
+
+  return '<div' . drupal_attributes($element['#attributes']) . '>' . $element['#children'] . '</div>';
+}
+
+/**
+ * Returns HTML for a table with radio buttons or checkboxes.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties and children of
+ *     the tableselect element. Properties used: #header, #options, #empty,
+ *     and #js_select. The #options property is an array of selection options;
+ *     each array element of #options is an array of properties. These
+ *     properties can include #attributes, which is added to the
+ *     table row's HTML attributes; see theme_table(). An example of per-row
+ *     options:
+ *     @code
+ *     $options = array(
+ *       array(
+ *         'title' => 'How to Learn Drupal',
+ *         'content_type' => 'Article',
+ *         'status' => 'published',
+ *         '#attributes' => array('class' => array('article-row')),
+ *       ),
+ *       array(
+ *         'title' => 'Privacy Policy',
+ *         'content_type' => 'Page',
+ *         'status' => 'published',
+ *         '#attributes' => array('class' => array('page-row')),
+ *       ),
+ *     );
+ *     $header = array(
+ *       'title' => t('Title'),
+ *       'content_type' => t('Content type'),
+ *       'status' => t('Status'),
+ *     );
+ *     $form['table'] = array(
+ *       '#type' => 'tableselect',
+ *       '#header' => $header,
+ *       '#options' => $options,
+ *       '#empty' => t('No content available.'),
+ *     );
+ *     @endcode
+ *
+ * @ingroup themeable
+ */
+function theme_tableselect($variables) {
+  $element = $variables['element'];
+  $rows = array();
+  $header = $element['#header'];
+  if (!empty($element['#options'])) {
+    // Generate a table row for each selectable item in #options.
+    foreach (element_children($element) as $key) {
+      $row = array();
+
+      $row['data'] = array();
+      if (isset($element['#options'][$key]['#attributes'])) {
+        $row += $element['#options'][$key]['#attributes'];
+      }
+      // Render the checkbox / radio element.
+      $row['data'][] = drupal_render($element[$key]);
+
+      // As theme_table only maps header and row columns by order, create the
+      // correct order by iterating over the header fields.
+      foreach ($element['#header'] as $fieldname => $title) {
+        $row['data'][] = $element['#options'][$key][$fieldname];
+      }
+      $rows[] = $row;
+    }
+    // Add an empty header or a "Select all" checkbox to provide room for the
+    // checkboxes/radios in the first table column.
+    if ($element['#js_select']) {
+      // Add a "Select all" checkbox.
+      drupal_add_js('misc/tableselect.js');
+      array_unshift($header, array('class' => array('select-all')));
+    }
+    else {
+      // Add an empty header when radio buttons are displayed or a "Select all"
+      // checkbox is not desired.
+      array_unshift($header, '');
+    }
+  }
+  return theme('table', array('header' => $header, 'rows' => $rows, 'empty' => $element['#empty'], 'attributes' => $element['#attributes']));
+}
+
+/**
+ * Creates checkbox or radio elements to populate a tableselect table.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   tableselect element.
+ *
+ * @return
+ *   The processed element.
+ */
+function form_process_tableselect($element) {
+
+  if ($element['#multiple']) {
+    $value = is_array($element['#value']) ? $element['#value'] : array();
+  }
+  else {
+    // Advanced selection behavior makes no sense for radios.
+    $element['#js_select'] = FALSE;
+  }
+
+  $element['#tree'] = TRUE;
+
+  if (count($element['#options']) > 0) {
+    if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
+      $element['#default_value'] = array();
+    }
+
+    // Create a checkbox or radio for each item in #options in such a way that
+    // the value of the tableselect element behaves as if it had been of type
+    // checkboxes or radios.
+    foreach ($element['#options'] as $key => $choice) {
+      // Do not overwrite manually created children.
+      if (!isset($element[$key])) {
+        if ($element['#multiple']) {
+          $title = '';
+          if (!empty($element['#options'][$key]['title']['data']['#title'])) {
+            $title = t('Update @title', array(
+              '@title' => $element['#options'][$key]['title']['data']['#title'],
+            ));
+          }
+          $element[$key] = array(
+            '#type' => 'checkbox',
+            '#title' => $title,
+            '#title_display' => 'invisible',
+            '#return_value' => $key,
+            '#default_value' => isset($value[$key]) ? $key : NULL,
+            '#attributes' => $element['#attributes'],
+          );
+        }
+        else {
+          // Generate the parents as the autogenerator does, so we will have a
+          // unique id for each radio button.
+          $parents_for_id = array_merge($element['#parents'], array($key));
+          $element[$key] = array(
+            '#type' => 'radio',
+            '#title' => '',
+            '#return_value' => $key,
+            '#default_value' => ($element['#default_value'] == $key) ? $key : NULL,
+            '#attributes' => $element['#attributes'],
+            '#parents' => $element['#parents'],
+            '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
+            '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+          );
+        }
+        if (isset($element['#options'][$key]['#weight'])) {
+          $element[$key]['#weight'] = $element['#options'][$key]['#weight'];
+        }
+      }
+    }
+  }
+  else {
+    $element['#value'] = array();
+  }
+  return $element;
+}
+
+/**
+ * Processes a machine-readable name form element.
+ *
+ * @param $element
+ *   The form element to process. Properties used:
+ *   - #machine_name: An associative array containing:
+ *     - exists: A function name to invoke for checking whether a submitted
+ *       machine name value already exists. The submitted value is passed as
+ *       argument. In most cases, an existing API or menu argument loader
+ *       function can be re-used. The callback is only invoked, if the submitted
+ *       value differs from the element's #default_value.
+ *     - source: (optional) The #array_parents of the form element containing
+ *       the human-readable name (i.e., as contained in the $form structure) to
+ *       use as source for the machine name. Defaults to array('name').
+ *     - label: (optional) A text to display as label for the machine name value
+ *       after the human-readable name form element. Defaults to "Machine name".
+ *     - replace_pattern: (optional) A regular expression (without delimiters)
+ *       matching disallowed characters in the machine name. Defaults to
+ *       '[^a-z0-9_]+'.
+ *     - replace: (optional) A character to replace disallowed characters in the
+ *       machine name via JavaScript. Defaults to '_' (underscore). When using a
+ *       different character, 'replace_pattern' needs to be set accordingly.
+ *     - error: (optional) A custom form error message string to show, if the
+ *       machine name contains disallowed characters.
+ *     - standalone: (optional) Whether the live preview should stay in its own
+ *       form element rather than in the suffix of the source element. Defaults
+ *       to FALSE.
+ *   - #maxlength: (optional) Should be set to the maximum allowed length of the
+ *     machine name. Defaults to 64.
+ *   - #disabled: (optional) Should be set to TRUE in case an existing machine
+ *     name must not be changed after initial creation.
+ */
+function form_process_machine_name($element, &$form_state) {
+  // Apply default form element properties.
+  $element += array(
+    '#title' => t('Machine-readable name'),
+    '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
+    '#machine_name' => array(),
+    '#field_prefix' => '',
+    '#field_suffix' => '',
+    '#suffix' => '',
+  );
+  // A form element that only wants to set one #machine_name property (usually
+  // 'source' only) would leave all other properties undefined, if the defaults
+  // were defined in hook_element_info(). Therefore, we apply the defaults here.
+  $element['#machine_name'] += array(
+    'source' => array('name'),
+    'target' => '#' . $element['#id'],
+    'label' => t('Machine name'),
+    'replace_pattern' => '[^a-z0-9_]+',
+    'replace' => '_',
+    'standalone' => FALSE,
+    'field_prefix' => $element['#field_prefix'],
+    'field_suffix' => $element['#field_suffix'],
+  );
+
+  // By default, machine names are restricted to Latin alphanumeric characters.
+  // So, default to LTR directionality.
+  if (!isset($element['#attributes'])) {
+    $element['#attributes'] = array();
+  }
+  $element['#attributes'] += array('dir' => 'ltr');
+
+  // The source element defaults to array('name'), but may have been overidden.
+  if (empty($element['#machine_name']['source'])) {
+    return $element;
+  }
+
+  // Retrieve the form element containing the human-readable name from the
+  // complete form in $form_state. By reference, because we may need to append
+  // a #field_suffix that will hold the live preview.
+  $key_exists = NULL;
+  $source = drupal_array_get_nested_value($form_state['complete form'], $element['#machine_name']['source'], $key_exists);
+  if (!$key_exists) {
+    return $element;
+  }
+
+  $suffix_id = $source['#id'] . '-machine-name-suffix';
+  $element['#machine_name']['suffix'] = '#' . $suffix_id;
+
+  if ($element['#machine_name']['standalone']) {
+    $element['#suffix'] .= ' <small id="' . $suffix_id . '">&nbsp;</small>';
+  }
+  else {
+    // Append a field suffix to the source form element, which will contain
+    // the live preview of the machine name.
+    $source += array('#field_suffix' => '');
+    $source['#field_suffix'] .= ' <small id="' . $suffix_id . '">&nbsp;</small>';
+
+    $parents = array_merge($element['#machine_name']['source'], array('#field_suffix'));
+    drupal_array_set_nested_value($form_state['complete form'], $parents, $source['#field_suffix']);
+  }
+
+  $js_settings = array(
+    'type' => 'setting',
+    'data' => array(
+      'machineName' => array(
+        '#' . $source['#id'] => $element['#machine_name'],
+      ),
+    ),
+  );
+  $element['#attached']['js'][] = 'misc/machine-name.js';
+  $element['#attached']['js'][] = $js_settings;
+
+  return $element;
+}
+
+/**
+ * Form element validation handler for machine_name elements.
+ *
+ * Note that #maxlength is validated by _form_validate() already.
+ */
+function form_validate_machine_name(&$element, &$form_state) {
+  // Verify that the machine name not only consists of replacement tokens.
+  if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) {
+    form_error($element, t('The machine-readable name must contain unique characters.'));
+  }
+
+  // Verify that the machine name contains no disallowed characters.
+  if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) {
+    if (!isset($element['#machine_name']['error'])) {
+      // Since a hyphen is the most common alternative replacement character,
+      // a corresponding validation error message is supported here.
+      if ($element['#machine_name']['replace'] == '-') {
+        form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
+      }
+      // Otherwise, we assume the default (underscore).
+      else {
+        form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
+      }
+    }
+    else {
+      form_error($element, $element['#machine_name']['error']);
+    }
+  }
+
+  // Verify that the machine name is unique.
+  if ($element['#default_value'] !== $element['#value']) {
+    $function = $element['#machine_name']['exists'];
+    if ($function($element['#value'], $element, $form_state)) {
+      form_error($element, t('The machine-readable name is already in use. It must be unique.'));
+    }
+  }
+}
+
+/**
+ * Arranges fieldsets into groups.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   fieldset. Note that $element must be taken by reference here, so processed
+ *   child elements are taken over into $form_state.
+ * @param $form_state
+ *   The $form_state array for the form this fieldset belongs to.
+ *
+ * @return
+ *   The processed element.
+ */
+function form_process_fieldset(&$element, &$form_state) {
+  $parents = implode('][', $element['#parents']);
+
+  // Each fieldset forms a new group. The #type 'vertical_tabs' basically only
+  // injects a new fieldset.
+  $form_state['groups'][$parents]['#group_exists'] = TRUE;
+  $element['#groups'] = &$form_state['groups'];
+
+  // Process vertical tabs group member fieldsets.
+  if (isset($element['#group'])) {
+    // Add this fieldset to the defined group (by reference).
+    $group = $element['#group'];
+    $form_state['groups'][$group][] = &$element;
+  }
+
+  // Contains form element summary functionalities.
+  $element['#attached']['library'][] = array('system', 'drupal.form');
+
+  // The .form-wrapper class is required for #states to treat fieldsets like
+  // containers.
+  if (!isset($element['#attributes']['class'])) {
+    $element['#attributes']['class'] = array();
+  }
+
+  // Collapsible fieldsets
+  if (!empty($element['#collapsible'])) {
+    $element['#attached']['library'][] = array('system', 'drupal.collapse');
+    $element['#attributes']['class'][] = 'collapsible';
+    if (!empty($element['#collapsed'])) {
+      $element['#attributes']['class'][] = 'collapsed';
+    }
+  }
+
+  return $element;
+}
+
+/**
+ * Adds members of this group as actual elements for rendering.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   fieldset.
+ *
+ * @return
+ *   The modified element with all group members.
+ */
+function form_pre_render_fieldset($element) {
+  // Fieldsets may be rendered outside of a Form API context.
+  if (!isset($element['#parents']) || !isset($element['#groups'])) {
+    return $element;
+  }
+  // Inject group member elements belonging to this group.
+  $parents = implode('][', $element['#parents']);
+  $children = element_children($element['#groups'][$parents]);
+  if (!empty($children)) {
+    foreach ($children as $key) {
+      // Break references and indicate that the element should be rendered as
+      // group member.
+      $child = (array) $element['#groups'][$parents][$key];
+      $child['#group_fieldset'] = TRUE;
+      // Inject the element as new child element.
+      $element[] = $child;
+
+      $sort = TRUE;
+    }
+    // Re-sort the element's children if we injected group member elements.
+    if (isset($sort)) {
+      $element['#sorted'] = FALSE;
+    }
+  }
+
+  if (isset($element['#group'])) {
+    $group = $element['#group'];
+    // If this element belongs to a group, but the group-holding element does
+    // not exist, we need to render it (at its original location).
+    if (!isset($element['#groups'][$group]['#group_exists'])) {
+      // Intentionally empty to clarify the flow; we simply return $element.
+    }
+    // If we injected this element into the group, then we want to render it.
+    elseif (!empty($element['#group_fieldset'])) {
+      // Intentionally empty to clarify the flow; we simply return $element.
+    }
+    // Otherwise, this element belongs to a group and the group exists, so we do
+    // not render it.
+    elseif (element_children($element['#groups'][$group])) {
+      $element['#printed'] = TRUE;
+    }
+  }
+
+  return $element;
+}
+
+/**
+ * Creates a group formatted as vertical tabs.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   fieldset.
+ * @param $form_state
+ *   The $form_state array for the form this vertical tab widget belongs to.
+ *
+ * @return
+ *   The processed element.
+ */
+function form_process_vertical_tabs($element, &$form_state) {
+  // Inject a new fieldset as child, so that form_process_fieldset() processes
+  // this fieldset like any other fieldset.
+  $element['group'] = array(
+    '#type' => 'fieldset',
+    '#theme_wrappers' => array(),
+    '#parents' => $element['#parents'],
+  );
+
+  // The JavaScript stores the currently selected tab in this hidden
+  // field so that the active tab can be restored the next time the
+  // form is rendered, e.g. on preview pages or when form validation
+  // fails.
+  $name = implode('__', $element['#parents']);
+  if (isset($form_state['values'][$name . '__active_tab'])) {
+    $element['#default_tab'] = $form_state['values'][$name . '__active_tab'];
+  }
+  $element[$name . '__active_tab'] = array(
+    '#type' => 'hidden',
+    '#default_value' => $element['#default_tab'],
+    '#attributes' => array('class' => array('vertical-tabs-active-tab')),
+  );
+
+  return $element;
+}
+
+/**
+ * Returns HTML for an element's children fieldsets as vertical tabs.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties and children of
+ *     the fieldset. Properties used: #children.
+ *
+ * @ingroup themeable
+ */
+function theme_vertical_tabs($variables) {
+  $element = $variables['element'];
+  // Add required JavaScript and Stylesheet.
+  drupal_add_library('system', 'drupal.vertical-tabs');
+
+  $output = '<h2 class="element-invisible">' . t('Vertical Tabs') . '</h2>';
+  $output .= '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
+  return $output;
+}
+
+/**
+ * Returns HTML for a submit button form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #attributes, #button_type, #name, #value.
+ *
+ * @ingroup themeable
+ */
+function theme_submit($variables) {
+  return theme('button', $variables['element']);
+}
+
+/**
+ * Returns HTML for a button form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #attributes, #button_type, #name, #value.
+ *
+ * @ingroup themeable
+ */
+function theme_button($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'submit';
+  element_set_attributes($element, array('id', 'name', 'value'));
+
+  $element['#attributes']['class'][] = 'form-' . $element['#button_type'];
+  if (!empty($element['#attributes']['disabled'])) {
+    $element['#attributes']['class'][] = 'form-button-disabled';
+  }
+
+  return '<input' . drupal_attributes($element['#attributes']) . ' />';
+}
+
+/**
+ * Returns HTML for an image button form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #attributes, #button_type, #name, #value, #title, #src.
+ *
+ * @ingroup themeable
+ */
+function theme_image_button($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'image';
+  element_set_attributes($element, array('id', 'name', 'value'));
+
+  $element['#attributes']['src'] = file_create_url($element['#src']);
+  if (!empty($element['#title'])) {
+    $element['#attributes']['alt'] = $element['#title'];
+    $element['#attributes']['title'] = $element['#title'];
+  }
+
+  $element['#attributes']['class'][] = 'form-' . $element['#button_type'];
+  if (!empty($element['#attributes']['disabled'])) {
+    $element['#attributes']['class'][] = 'form-button-disabled';
+  }
+
+  return '<input' . drupal_attributes($element['#attributes']) . ' />';
+}
+
+/**
+ * Returns HTML for a hidden form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #name, #value, #attributes.
+ *
+ * @ingroup themeable
+ */
+function theme_hidden($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'hidden';
+  element_set_attributes($element, array('name', 'value'));
+  return '<input' . drupal_attributes($element['#attributes']) . " />\n";
+}
+
+/**
+ * Returns HTML for a textfield form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #description, #size, #maxlength,
+ *     #required, #attributes, #autocomplete_path.
+ *
+ * @ingroup themeable
+ */
+function theme_textfield($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'text';
+  element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength'));
+  _form_set_class($element, array('form-text'));
+
+  $extra = '';
+  if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) {
+    drupal_add_library('system', 'drupal.autocomplete');
+    $element['#attributes']['class'][] = 'form-autocomplete';
+
+    $attributes = array();
+    $attributes['type'] = 'hidden';
+    $attributes['id'] = $element['#attributes']['id'] . '-autocomplete';
+    $attributes['value'] = url($element['#autocomplete_path'], array('absolute' => TRUE));
+    $attributes['disabled'] = 'disabled';
+    $attributes['class'][] = 'autocomplete';
+    $extra = '<input' . drupal_attributes($attributes) . ' />';
+  }
+
+  $output = '<input' . drupal_attributes($element['#attributes']) . ' />';
+
+  return $output . $extra;
+}
+
+/**
+ * Returns HTML for a form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #action, #method, #attributes, #children
+ *
+ * @ingroup themeable
+ */
+function theme_form($variables) {
+  $element = $variables['element'];
+  if (isset($element['#action'])) {
+    $element['#attributes']['action'] = drupal_strip_dangerous_protocols($element['#action']);
+  }
+  element_set_attributes($element, array('method', 'id'));
+  if (empty($element['#attributes']['accept-charset'])) {
+    $element['#attributes']['accept-charset'] = "UTF-8";
+  }
+  // Anonymous DIV to satisfy XHTML compliance.
+  return '<form' . drupal_attributes($element['#attributes']) . '><div>' . $element['#children'] . '</div></form>';
+}
+
+/**
+ * Returns HTML for a textarea form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #description, #rows, #cols, #required,
+ *     #attributes
+ *
+ * @ingroup themeable
+ */
+function theme_textarea($variables) {
+  $element = $variables['element'];
+  element_set_attributes($element, array('id', 'name', 'cols', 'rows'));
+  _form_set_class($element, array('form-textarea'));
+
+  $wrapper_attributes = array(
+    'class' => array('form-textarea-wrapper'),
+  );
+
+  // Add resizable behavior.
+  if (!empty($element['#resizable'])) {
+    drupal_add_library('system', 'drupal.textarea');
+    $wrapper_attributes['class'][] = 'resizable';
+  }
+
+  $output = '<div' . drupal_attributes($wrapper_attributes) . '>';
+  $output .= '<textarea' . drupal_attributes($element['#attributes']) . '>' . check_plain($element['#value']) . '</textarea>';
+  $output .= '</div>';
+  return $output;
+}
+
+/**
+ * Returns HTML for a password form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #description, #size, #maxlength,
+ *     #required, #attributes.
+ *
+ * @ingroup themeable
+ */
+function theme_password($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'password';
+  element_set_attributes($element, array('id', 'name', 'size', 'maxlength'));
+  _form_set_class($element, array('form-text'));
+
+  return '<input' . drupal_attributes($element['#attributes']) . ' />';
+}
+
+/**
+ * Expands a weight element into a select element.
+ */
+function form_process_weight($element) {
+  $element['#is_weight'] = TRUE;
+
+  // If the number of options is small enough, use a select field.
+  $max_elements = variable_get('drupal_weight_select_max', DRUPAL_WEIGHT_SELECT_MAX);
+  if ($element['#delta'] <= $max_elements) {
+    $element['#type'] = 'select';
+    for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) {
+      $weights[$n] = $n;
+    }
+    $element['#options'] = $weights;
+    $element += element_info('select');
+  }
+  // Otherwise, use a text field.
+  else {
+    $element['#type'] = 'textfield';
+    // Use a field big enough to fit most weights.
+    $element['#size'] = 10;
+    $element['#element_validate'] = array('element_validate_integer');
+    $element += element_info('textfield');
+  }
+
+  return $element;
+}
+
+/**
+ * Returns HTML for a file upload form element.
+ *
+ * For assistance with handling the uploaded file correctly, see the API
+ * provided by file.inc.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #name, #size, #description, #required,
+ *     #attributes.
+ *
+ * @ingroup themeable
+ */
+function theme_file($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'file';
+  element_set_attributes($element, array('id', 'name', 'size'));
+  _form_set_class($element, array('form-file'));
+
+  return '<input' . drupal_attributes($element['#attributes']) . ' />';
+}
+
+/**
+ * Returns HTML for a form element.
+ *
+ * Each form element is wrapped in a DIV container having the following CSS
+ * classes:
+ * - form-item: Generic for all form elements.
+ * - form-type-#type: The internal element #type.
+ * - form-item-#name: The internal form element #name (usually derived from the
+ *   $form structure and set via form_builder()).
+ * - form-disabled: Only set if the form element is #disabled.
+ *
+ * In addition to the element itself, the DIV contains a label for the element
+ * based on the optional #title_display property, and an optional #description.
+ *
+ * The optional #title_display property can have these values:
+ * - before: The label is output before the element. This is the default.
+ *   The label includes the #title and the required marker, if #required.
+ * - after: The label is output after the element. For example, this is used
+ *   for radio and checkbox #type elements as set in system_element_info().
+ *   If the #title is empty but the field is #required, the label will
+ *   contain only the required marker.
+ * - invisible: Labels are critical for screen readers to enable them to
+ *   properly navigate through forms but can be visually distracting. This
+ *   property hides the label for everyone except screen readers.
+ * - attribute: Set the title attribute on the element to create a tooltip
+ *   but output no label element. This is supported only for checkboxes
+ *   and radios in form_pre_render_conditional_form_element(). It is used
+ *   where a visual label is not needed, such as a table of checkboxes where
+ *   the row and column provide the context. The tooltip will include the
+ *   title and required marker.
+ *
+ * If the #title property is not set, then the label and any required marker
+ * will not be output, regardless of the #title_display or #required values.
+ * This can be useful in cases such as the password_confirm element, which
+ * creates children elements that have their own labels and required markers,
+ * but the parent element should have neither. Use this carefully because a
+ * field without an associated label can cause accessibility challenges.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #title_display, #description, #id, #required,
+ *     #children, #type, #name.
+ *
+ * @ingroup themeable
+ */
+function theme_form_element($variables) {
+  $element = &$variables['element'];
+
+  // This function is invoked as theme wrapper, but the rendered form element
+  // may not necessarily have been processed by form_builder().
+  $element += array(
+    '#title_display' => 'before',
+  );
+
+  // Add element #id for #type 'item'.
+  if (isset($element['#markup']) && !empty($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  // Add element's #type and #name as class to aid with JS/CSS selectors.
+  $attributes['class'] = array('form-item');
+  if (!empty($element['#type'])) {
+    $attributes['class'][] = 'form-type-' . strtr($element['#type'], '_', '-');
+  }
+  if (!empty($element['#name'])) {
+    $attributes['class'][] = 'form-item-' . strtr($element['#name'], array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
+  }
+  // Add a class for disabled elements to facilitate cross-browser styling.
+  if (!empty($element['#attributes']['disabled'])) {
+    $attributes['class'][] = 'form-disabled';
+  }
+  $output = '<div' . drupal_attributes($attributes) . '>' . "\n";
+
+  // If #title is not set, we don't display any label or required marker.
+  if (!isset($element['#title'])) {
+    $element['#title_display'] = 'none';
+  }
+  $prefix = isset($element['#field_prefix']) ? '<span class="field-prefix">' . $element['#field_prefix'] . '</span> ' : '';
+  $suffix = isset($element['#field_suffix']) ? ' <span class="field-suffix">' . $element['#field_suffix'] . '</span>' : '';
+
+  switch ($element['#title_display']) {
+    case 'before':
+    case 'invisible':
+      $output .= ' ' . theme('form_element_label', $variables);
+      $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n";
+      break;
+
+    case 'after':
+      $output .= ' ' . $prefix . $element['#children'] . $suffix;
+      $output .= ' ' . theme('form_element_label', $variables) . "\n";
+      break;
+
+    case 'none':
+    case 'attribute':
+      // Output no label and no required marker, only the children.
+      $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n";
+      break;
+  }
+
+  if (!empty($element['#description'])) {
+    $output .= '<div class="description">' . $element['#description'] . "</div>\n";
+  }
+
+  $output .= "</div>\n";
+
+  return $output;
+}
+
+/**
+ * Returns HTML for a marker for required form elements.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *
+ * @ingroup themeable
+ */
+function theme_form_required_marker($variables) {
+  // This is also used in the installer, pre-database setup.
+  $t = get_t();
+  $attributes = array(
+    'class' => 'form-required',
+    'title' => $t('This field is required.'),
+  );
+  return '<span' . drupal_attributes($attributes) . '>*</span>';
+}
+
+/**
+ * Returns HTML for a form element label and required marker.
+ *
+ * Form element labels include the #title and a #required marker. The label is
+ * associated with the element itself by the element #id. Labels may appear
+ * before or after elements, depending on theme_form_element() and
+ * #title_display.
+ *
+ * This function will not be called for elements with no labels, depending on
+ * #title_display. For elements that have an empty #title and are not required,
+ * this function will output no label (''). For required elements that have an
+ * empty #title, this will output the required marker alone within the label.
+ * The label will use the #id to associate the marker with the field that is
+ * required. That is especially important for screenreader users to know
+ * which field is required.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #required, #title, #id, #value, #description.
+ *
+ * @ingroup themeable
+ */
+function theme_form_element_label($variables) {
+  $element = $variables['element'];
+  // This is also used in the installer, pre-database setup.
+  $t = get_t();
+
+  // If title and required marker are both empty, output no label.
+  if ((!isset($element['#title']) || $element['#title'] === '') && empty($element['#required'])) {
+    return '';
+  }
+
+  // If the element is required, a required marker is appended to the label.
+  $required = !empty($element['#required']) ? theme('form_required_marker', array('element' => $element)) : '';
+
+  $title = filter_xss_admin($element['#title']);
+
+  $attributes = array();
+  // Style the label as class option to display inline with the element.
+  if ($element['#title_display'] == 'after') {
+    $attributes['class'] = 'option';
+  }
+  // Show label only to screen readers to avoid disruption in visual flows.
+  elseif ($element['#title_display'] == 'invisible') {
+    $attributes['class'] = 'element-invisible';
+  }
+
+  if (!empty($element['#id'])) {
+    $attributes['for'] = $element['#id'];
+  }
+
+  // The leading whitespace helps visually separate fields from inline labels.
+  return ' <label' . drupal_attributes($attributes) . '>' . $t('!title !required', array('!title' => $title, '!required' => $required)) . "</label>\n";
+}
+
+/**
+ * Sets a form element's class attribute.
+ *
+ * Adds 'required' and 'error' classes as needed.
+ *
+ * @param $element
+ *   The form element.
+ * @param $name
+ *   Array of new class names to be added.
+ */
+function _form_set_class(&$element, $class = array()) {
+  if (!empty($class)) {
+    if (!isset($element['#attributes']['class'])) {
+      $element['#attributes']['class'] = array();
+    }
+    $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class);
+  }
+  // This function is invoked from form element theme functions, but the
+  // rendered form element may not necessarily have been processed by
+  // form_builder().
+  if (!empty($element['#required'])) {
+    $element['#attributes']['class'][] = 'required';
+  }
+  if (isset($element['#parents']) && form_get_error($element) !== NULL && !empty($element['#validated'])) {
+    $element['#attributes']['class'][] = 'error';
+  }
+}
+
+/**
+ * Form element validation handler for integer elements.
+ */
+function element_validate_integer($element, &$form_state) {
+  $value = $element['#value'];
+  if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) {
+    form_error($element, t('%name must be an integer.', array('%name' => $element['#title'])));
+  }
+}
+
+/**
+ * Form element validation handler for integer elements that must be positive.
+ */
+function element_validate_integer_positive($element, &$form_state) {
+  $value = $element['#value'];
+  if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value <= 0)) {
+    form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title'])));
+  }
+}
+
+/**
+ * Form element validation handler for number elements.
+ */
+function element_validate_number($element, &$form_state) {
+  $value = $element['#value'];
+  if ($value != '' && !is_numeric($value)) {
+    form_error($element, t('%name must be a number.', array('%name' => $element['#title'])));
+  }
+}
+
+/**
+ * @} End of "defgroup form_api".
+ */
+
+/**
+ * @defgroup batch Batch operations
+ * @{
+ * Creates and processes batch operations.
+ *
+ * Functions allowing forms processing to be spread out over several page
+ * requests, thus ensuring that the processing does not get interrupted
+ * because of a PHP timeout, while allowing the user to receive feedback
+ * on the progress of the ongoing operations.
+ *
+ * The API is primarily designed to integrate nicely with the Form API
+ * workflow, but can also be used by non-Form API scripts (like update.php)
+ * or even simple page callbacks (which should probably be used sparingly).
+ *
+ * Example:
+ * @code
+ * $batch = array(
+ *   'title' => t('Exporting'),
+ *   'operations' => array(
+ *     array('my_function_1', array($account->uid, 'story')),
+ *     array('my_function_2', array()),
+ *   ),
+ *   'finished' => 'my_finished_callback',
+ *   'file' => 'path_to_file_containing_myfunctions',
+ * );
+ * batch_set($batch);
+ * // Only needed if not inside a form _submit handler.
+ * // Setting redirect in batch_process.
+ * batch_process('node/1');
+ * @endcode
+ *
+ * Note: if the batch 'title', 'init_message', 'progress_message', or
+ * 'error_message' could contain any user input, it is the responsibility of
+ * the code calling batch_set() to sanitize them first with a function like
+ * check_plain() or filter_xss(). Furthermore, if the batch operation
+ * returns any user input in the 'results' or 'message' keys of $context,
+ * it must also sanitize them first.
+ *
+ * Sample batch operations:
+ * @code
+ * // Simple and artificial: load a node of a given type for a given user
+ * function my_function_1($uid, $type, &$context) {
+ *   // The $context array gathers batch context information about the execution (read),
+ *   // as well as 'return values' for the current operation (write)
+ *   // The following keys are provided :
+ *   // 'results' (read / write): The array of results gathered so far by
+ *   //   the batch processing, for the current operation to append its own.
+ *   // 'message' (write): A text message displayed in the progress page.
+ *   // The following keys allow for multi-step operations :
+ *   // 'sandbox' (read / write): An array that can be freely used to
+ *   //   store persistent data between iterations. It is recommended to
+ *   //   use this instead of $_SESSION, which is unsafe if the user
+ *   //   continues browsing in a separate window while the batch is processing.
+ *   // 'finished' (write): A float number between 0 and 1 informing
+ *   //   the processing engine of the completion level for the operation.
+ *   //   1 (or no value explicitly set) means the operation is finished
+ *   //   and the batch processing can continue to the next operation.
+ *
+ *   $node = node_load(array('uid' => $uid, 'type' => $type));
+ *   $context['results'][] = $node->nid . ' : ' . check_plain($node->title);
+ *   $context['message'] = check_plain($node->title);
+ * }
+ *
+ * // More advanced example: multi-step operation - load all nodes, five by five
+ * function my_function_2(&$context) {
+ *   if (empty($context['sandbox'])) {
+ *     $context['sandbox']['progress'] = 0;
+ *     $context['sandbox']['current_node'] = 0;
+ *     $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
+ *   }
+ *   $limit = 5;
+ *   $result = db_select('node')
+ *     ->fields('node', array('nid'))
+ *     ->condition('nid', $context['sandbox']['current_node'], '>')
+ *     ->orderBy('nid')
+ *     ->range(0, $limit)
+ *     ->execute();
+ *   foreach ($result as $row) {
+ *     $node = node_load($row->nid, NULL, TRUE);
+ *     $context['results'][] = $node->nid . ' : ' . check_plain($node->title);
+ *     $context['sandbox']['progress']++;
+ *     $context['sandbox']['current_node'] = $node->nid;
+ *     $context['message'] = check_plain($node->title);
+ *   }
+ *   if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+ *     $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+ *   }
+ * }
+ * @endcode
+ *
+ * Sample 'finished' callback:
+ * @code
+ * function batch_test_finished($success, $results, $operations) {
+ *   // The 'success' parameter means no fatal PHP errors were detected. All
+ *   // other error management should be handled using 'results'.
+ *   if ($success) {
+ *     $message = format_plural(count($results), 'One post processed.', '@count posts processed.');
+ *   }
+ *   else {
+ *     $message = t('Finished with an error.');
+ *   }
+ *   drupal_set_message($message);
+ *   // Providing data for the redirected page is done through $_SESSION.
+ *   foreach ($results as $result) {
+ *     $items[] = t('Loaded node %title.', array('%title' => $result));
+ *   }
+ *   $_SESSION['my_batch_results'] = $items;
+ * }
+ * @endcode
+ */
+
+/**
+ * Adds a new batch.
+ *
+ * Batch operations are added as new batch sets. Batch sets are used to spread
+ * processing (primarily, but not exclusively, forms processing) over several
+ * page requests. This helps to ensure that the processing is not interrupted
+ * due to PHP timeouts, while users are still able to receive feedback on the
+ * progress of the ongoing operations. Combining related operations into
+ * distinct batch sets provides clean code independence for each batch set,
+ * ensuring that two or more batches, submitted independently, can be processed
+ * without mutual interference. Each batch set may specify its own set of
+ * operations and results, produce its own UI messages, and trigger its own
+ * 'finished' callback. Batch sets are processed sequentially, with the progress
+ * bar starting afresh for each new set.
+ *
+ * @param $batch_definition
+ *   An associative array defining the batch, with the following elements (all
+ *   are optional except as noted):
+ *   - operations: (required) Array of function calls to be performed.
+ *     Example:
+ *     @code
+ *     array(
+ *       array('my_function_1', array($arg1)),
+ *       array('my_function_2', array($arg2_1, $arg2_2)),
+ *     )
+ *     @endcode
+ *   - title: A safe, translated string to use as the title for the progress
+ *     page. Defaults to t('Processing').
+ *   - init_message: Message displayed while the processing is initialized.
+ *     Defaults to t('Initializing.').
+ *   - progress_message: Message displayed while processing the batch. Available
+ *     placeholders are @current, @remaining, @total, @percentage, @estimate and
+ *     @elapsed. Defaults to t('Completed @current of @total.').
+ *   - error_message: Message displayed if an error occurred while processing
+ *     the batch. Defaults to t('An error has occurred.').
+ *   - finished: Name of a function to be executed after the batch has
+ *     completed. This should be used to perform any result massaging that may
+ *     be needed, and possibly save data in $_SESSION for display after final
+ *     page redirection.
+ *   - file: Path to the file containing the definitions of the 'operations' and
+ *     'finished' functions, for instance if they don't reside in the main
+ *     .module file. The path should be relative to base_path(), and thus should
+ *     be built using drupal_get_path().
+ *   - css: Array of paths to CSS files to be used on the progress page.
+ *   - url_options: options passed to url() when constructing redirect URLs for
+ *     the batch.
+ */
+function batch_set($batch_definition) {
+  if ($batch_definition) {
+    $batch =& batch_get();
+
+    // Initialize the batch if needed.
+    if (empty($batch)) {
+      $batch = array(
+        'sets' => array(),
+        'has_form_submits' => FALSE,
+      );
+    }
+
+    // Base and default properties for the batch set.
+    // Use get_t() to allow batches during installation.
+    $t = get_t();
+    $init = array(
+      'sandbox' => array(),
+      'results' => array(),
+      'success' => FALSE,
+      'start' => 0,
+      'elapsed' => 0,
+    );
+    $defaults = array(
+      'title' => $t('Processing'),
+      'init_message' => $t('Initializing.'),
+      'progress_message' => $t('Completed @current of @total.'),
+      'error_message' => $t('An error has occurred.'),
+      'css' => array(),
+    );
+    $batch_set = $init + $batch_definition + $defaults;
+
+    // Tweak init_message to avoid the bottom of the page flickering down after
+    // init phase.
+    $batch_set['init_message'] .= '<br/>&nbsp;';
+
+    // The non-concurrent workflow of batch execution allows us to save
+    // numberOfItems() queries by handling our own counter.
+    $batch_set['total'] = count($batch_set['operations']);
+    $batch_set['count'] = $batch_set['total'];
+
+    // Add the set to the batch.
+    if (empty($batch['id'])) {
+      // The batch is not running yet. Simply add the new set.
+      $batch['sets'][] = $batch_set;
+    }
+    else {
+      // The set is being added while the batch is running. Insert the new set
+      // right after the current one to ensure execution order, and store its
+      // operations in a queue.
+      $index = $batch['current_set'] + 1;
+      $slice1 = array_slice($batch['sets'], 0, $index);
+      $slice2 = array_slice($batch['sets'], $index);
+      $batch['sets'] = array_merge($slice1, array($batch_set), $slice2);
+      _batch_populate_queue($batch, $index);
+    }
+  }
+}
+
+/**
+ * Processes the batch.
+ *
+ * Unless the batch has been marked with 'progressive' = FALSE, the function
+ * issues a drupal_goto and thus ends page execution.
+ *
+ * This function is generally not needed in form submit handlers;
+ * Form API takes care of batches that were set during form submission.
+ *
+ * @param $redirect
+ *   (optional) Path to redirect to when the batch has finished processing.
+ * @param $url
+ *   (optional - should only be used for separate scripts like update.php)
+ *   URL of the batch processing page.
+ * @param $redirect_callback
+ *   (optional) Specify a function to be called to redirect to the progressive
+ *   processing page. By default drupal_goto() will be used to redirect to a
+ *   page which will do the progressive page. Specifying another function will
+ *   allow the progressive processing to be processed differently.
+ */
+function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'drupal_goto') {
+  $batch =& batch_get();
+
+  drupal_theme_initialize();
+
+  if (isset($batch)) {
+    // Add process information
+    $process_info = array(
+      'current_set' => 0,
+      'progressive' => TRUE,
+      'url' => $url,
+      'url_options' => array(),
+      'source_url' => $_GET['q'],
+      'redirect' => $redirect,
+      'theme' => $GLOBALS['theme_key'],
+      'redirect_callback' => $redirect_callback,
+    );
+    $batch += $process_info;
+
+    // The batch is now completely built. Allow other modules to make changes
+    // to the batch so that it is easier to reuse batch processes in other
+    // environments.
+    drupal_alter('batch', $batch);
+
+    // Assign an arbitrary id: don't rely on a serial column in the 'batch'
+    // table, since non-progressive batches skip database storage completely.
+    $batch['id'] = db_next_id();
+
+    // Move operations to a job queue. Non-progressive batches will use a
+    // memory-based queue.
+    foreach ($batch['sets'] as $key => $batch_set) {
+      _batch_populate_queue($batch, $key);
+    }
+
+    // Initiate processing.
+    if ($batch['progressive']) {
+      // Now that we have a batch id, we can generate the redirection link in
+      // the generic error message.
+      $t = get_t();
+      $batch['error_message'] = $t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished')))));
+
+      // Clear the way for the drupal_goto() redirection to the batch processing
+      // page, by saving and unsetting the 'destination', if there is any.
+      if (isset($_GET['destination'])) {
+        $batch['destination'] = $_GET['destination'];
+        unset($_GET['destination']);
+      }
+
+      // Store the batch.
+      db_insert('batch')
+        ->fields(array(
+          'bid' => $batch['id'],
+          'timestamp' => REQUEST_TIME,
+          'token' => drupal_get_token($batch['id']),
+          'batch' => serialize($batch),
+        ))
+        ->execute();
+
+      // Set the batch number in the session to guarantee that it will stay alive.
+      $_SESSION['batches'][$batch['id']] = TRUE;
+
+      // Redirect for processing.
+      $function = $batch['redirect_callback'];
+      if (function_exists($function)) {
+        $function($batch['url'], array('query' => array('op' => 'start', 'id' => $batch['id'])));
+      }
+    }
+    else {
+      // Non-progressive execution: bypass the whole progressbar workflow
+      // and execute the batch in one pass.
+      require_once DRUPAL_ROOT . '/includes/batch.inc';
+      _batch_process();
+    }
+  }
+}
+
+/**
+ * Retrieves the current batch.
+ */
+function &batch_get() {
+  // Not drupal_static(), because Batch API operates at a lower level than most
+  // use-cases for resetting static variables, and we specifically do not want a
+  // global drupal_static_reset() resetting the batch information. Functions
+  // that are part of the Batch API and need to reset the batch information may
+  // call batch_get() and manipulate the result by reference. Functions that are
+  // not part of the Batch API can also do this, but shouldn't.
+  static $batch = array();
+  return $batch;
+}
+
+/**
+ * Populates a job queue with the operations of a batch set.
+ *
+ * Depending on whether the batch is progressive or not, the BatchQueue or
+ * BatchMemoryQueue handler classes will be used.
+ *
+ * @param $batch
+ *   The batch array.
+ * @param $set_id
+ *   The id of the set to process.
+ *
+ * @return
+ *   The name and class of the queue are added by reference to the batch set.
+ */
+function _batch_populate_queue(&$batch, $set_id) {
+  $batch_set = &$batch['sets'][$set_id];
+
+  if (isset($batch_set['operations'])) {
+    $batch_set += array(
+      'queue' => array(
+        'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id,
+        'class' => $batch['progressive'] ? 'BatchQueue' : 'BatchMemoryQueue',
+      ),
+    );
+
+    $queue = _batch_queue($batch_set);
+    $queue->createQueue();
+    foreach ($batch_set['operations'] as $operation) {
+      $queue->createItem($operation);
+    }
+
+    unset($batch_set['operations']);
+  }
+}
+
+/**
+ * Returns a queue object for a batch set.
+ *
+ * @param $batch_set
+ *   The batch set.
+ *
+ * @return
+ *   The queue object.
+ */
+function _batch_queue($batch_set) {
+  static $queues;
+
+  // The class autoloader is not available when running update.php, so make
+  // sure the files are manually included.
+  if (!isset($queues)) {
+    $queues = array();
+    require_once DRUPAL_ROOT . '/modules/system/system.queue.inc';
+    require_once DRUPAL_ROOT . '/includes/batch.queue.inc';
+  }
+
+  if (isset($batch_set['queue'])) {
+    $name = $batch_set['queue']['name'];
+    $class = $batch_set['queue']['class'];
+
+    if (!isset($queues[$class][$name])) {
+      $queues[$class][$name] = new $class($name);
+    }
+    return $queues[$class][$name];
+  }
+}
+
+/**
+ * @} End of "defgroup batch".
+ */

+ 145 - 0
includes/graph.inc

@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * @file
+ * Directed acyclic graph manipulation.
+ */
+
+
+/**
+ * Performs a depth-first search and sort on a directed acyclic graph.
+ *
+ * @param $graph
+ *   A three dimensional associated array, with the first keys being the names
+ *   of the vertices, these can be strings or numbers. The second key is
+ *   'edges' and the third one are again vertices, each such key representing
+ *   an edge. Values of array elements are copied over.
+ *
+ *   Example:
+ *   @code
+ *     $graph[1]['edges'][2] = 1;
+ *     $graph[2]['edges'][3] = 1;
+ *     $graph[2]['edges'][4] = 1;
+ *     $graph[3]['edges'][4] = 1;
+ *   @endcode
+ *
+ *   On return you will also have:
+ *   @code
+ *     $graph[1]['paths'][2] = 1;
+ *     $graph[1]['paths'][3] = 1;
+ *     $graph[2]['reverse_paths'][1] = 1;
+ *     $graph[3]['reverse_paths'][1] = 1;
+ *   @endcode
+ *
+ * @return
+ *   The passed-in $graph with more secondary keys filled in:
+ *   - 'paths': Contains a list of vertices than can be reached on a path from
+ *     this vertex.
+ *   - 'reverse_paths': Contains a list of vertices that has a path from them
+ *     to this vertex.
+ *   - 'weight': If there is a path from a vertex to another then the weight of
+ *     the latter is higher.
+ *   - 'component': Vertices in the same component have the same component
+ *     identifier.
+ *
+ * @see _drupal_depth_first_search()
+ */
+function drupal_depth_first_search(&$graph) {
+  $state = array(
+    // The order of last visit of the depth first search. This is the reverse
+    // of the topological order if the graph is acyclic.
+    'last_visit_order' => array(),
+    // The components of the graph.
+    'components' => array(),
+  );
+  // Perform the actual search.
+  foreach ($graph as $start => $data) {
+    _drupal_depth_first_search($graph, $state, $start);
+  }
+
+  // We do such a numbering that every component starts with 0. This is useful
+  // for module installs as we can install every 0 weighted module in one
+  // request, and then every 1 weighted etc.
+  $component_weights = array();
+
+  foreach ($state['last_visit_order'] as $vertex) {
+    $component = $graph[$vertex]['component'];
+    if (!isset($component_weights[$component])) {
+      $component_weights[$component] = 0;
+    }
+    $graph[$vertex]['weight'] = $component_weights[$component]--;
+  }
+}
+
+/**
+ * Performs a depth-first search on a graph.
+ *
+ * @param $graph
+ *   A three dimensional associated graph array.
+ * @param $state
+ *   An associative array. The key 'last_visit_order' stores a list of the
+ *   vertices visited. The key components stores list of vertices belonging
+ *   to the same the component.
+ * @param $start
+ *   An arbitrary vertex where we started traversing the graph.
+ * @param $component
+ *   The component of the last vertex.
+ *
+ * @see drupal_depth_first_search()
+ */
+function _drupal_depth_first_search(&$graph, &$state, $start, &$component = NULL) {
+  // Assign new component for each new vertex, i.e. when not called recursively.
+  if (!isset($component)) {
+    $component = $start;
+  }
+  // Nothing to do, if we already visited this vertex.
+  if (isset($graph[$start]['paths'])) {
+    return;
+  }
+  // Mark $start as visited.
+  $graph[$start]['paths'] = array();
+
+  // Assign $start to the current component.
+  $graph[$start]['component'] = $component;
+  $state['components'][$component][] = $start;
+
+  // Visit edges of $start.
+  if (isset($graph[$start]['edges'])) {
+    foreach ($graph[$start]['edges'] as $end => $v) {
+      // Mark that $start can reach $end.
+      $graph[$start]['paths'][$end] = $v;
+
+      if (isset($graph[$end]['component']) && $component != $graph[$end]['component']) {
+        // This vertex already has a component, use that from now on and
+        // reassign all the previously explored vertices.
+        $new_component = $graph[$end]['component'];
+        foreach ($state['components'][$component] as $vertex) {
+          $graph[$vertex]['component'] = $new_component;
+          $state['components'][$new_component][] = $vertex;
+        }
+        unset($state['components'][$component]);
+        $component = $new_component;
+      }
+      // Only visit existing vertices.
+      if (isset($graph[$end])) {
+        // Visit the connected vertex.
+        _drupal_depth_first_search($graph, $state, $end, $component);
+
+        // All vertices reachable by $end are also reachable by $start.
+        $graph[$start]['paths'] += $graph[$end]['paths'];
+      }
+    }
+  }
+
+  // Now that any other subgraph has been explored, add $start to all reverse
+  // paths.
+  foreach ($graph[$start]['paths'] as $end => $v) {
+    if (isset($graph[$end])) {
+      $graph[$end]['reverse_paths'][$start] = $v;
+    }
+  }
+
+  // Record the order of the last visit. This is the reverse of the
+  // topological order if the graph is acyclic.
+  $state['last_visit_order'][] = $start;
+}

+ 435 - 0
includes/image.inc

@@ -0,0 +1,435 @@
+<?php
+
+/**
+ * @file
+ * API for manipulating images.
+ */
+
+/**
+ * @defgroup image Image toolkits
+ * @{
+ * Functions for image file manipulations.
+ *
+ * Drupal's image toolkits provide an abstraction layer for common image file
+ * manipulations like scaling, cropping, and rotating. The abstraction frees
+ * module authors from the need to support multiple image libraries, and it
+ * allows site administrators to choose the library that's best for them.
+ *
+ * PHP includes the GD library by default so a GD toolkit is installed with
+ * Drupal. Other toolkits like ImageMagick are available from contrib modules.
+ * GD works well for small images, but using it with larger files may cause PHP
+ * to run out of memory. In contrast the ImageMagick library does not suffer
+ * from this problem, but it requires the ISP to have installed additional
+ * software.
+ *
+ * Image toolkits are discovered based on the associated module's
+ * hook_image_toolkits. Additionally the image toolkit include file
+ * must be identified in the files array in the module.info file. The
+ * toolkit must then be enabled using the admin/config/media/image-toolkit
+ * form.
+ *
+ * Only one toolkit may be selected at a time. If a module author wishes to call
+ * a specific toolkit they can check that it is installed by calling
+ * image_get_available_toolkits(), and then calling its functions directly.
+ */
+
+/**
+ * Gets a list of available toolkits.
+ *
+ * @return
+ *   An array with the toolkit names as keys and the descriptions as values.
+ */
+function image_get_available_toolkits() {
+  // hook_image_toolkits returns an array of toolkit names.
+  $toolkits = module_invoke_all('image_toolkits');
+
+  $output = array();
+  foreach ($toolkits as $name => $info) {
+    // Only allow modules that aren't marked as unavailable.
+    if ($info['available']) {
+      $output[$name] = $info['title'];
+    }
+  }
+
+  return $output;
+}
+
+/**
+ * Gets the name of the currently used toolkit.
+ *
+ * @return
+ *   String containing the name of the selected toolkit, or FALSE on error.
+ */
+function image_get_toolkit() {
+  static $toolkit;
+
+  if (!isset($toolkit)) {
+    $toolkits = image_get_available_toolkits();
+    $toolkit = variable_get('image_toolkit', 'gd');
+    if (!isset($toolkits[$toolkit]) || !function_exists('image_' . $toolkit . '_load')) {
+      // The selected toolkit isn't available so return the first one found. If
+      // none are available this will return FALSE.
+      reset($toolkits);
+      $toolkit = key($toolkits);
+    }
+  }
+
+  return $toolkit;
+}
+
+/**
+ * Invokes the given method using the currently selected toolkit.
+ *
+ * @param $method
+ *   A string containing the method to invoke.
+ * @param $image
+ *   An image object returned by image_load().
+ * @param $params
+ *   An optional array of parameters to pass to the toolkit method.
+ *
+ * @return
+ *   Mixed values (typically Boolean indicating successful operation).
+ */
+function image_toolkit_invoke($method, stdClass $image, array $params = array()) {
+  $function = 'image_' . $image->toolkit . '_' . $method;
+  if (function_exists($function)) {
+    array_unshift($params, $image);
+    return call_user_func_array($function, $params);
+  }
+  watchdog('image', 'The selected image handling toolkit %toolkit can not correctly process %function.', array('%toolkit' => $image->toolkit, '%function' => $function), WATCHDOG_ERROR);
+  return FALSE;
+}
+
+/**
+ * Gets details about an image.
+ *
+ * Drupal supports GIF, JPG and PNG file formats when used with the GD
+ * toolkit, and may support others, depending on which toolkits are
+ * installed.
+ *
+ * @param $filepath
+ *   String specifying the path of the image file.
+ * @param $toolkit
+ *   An optional image toolkit name to override the default.
+ *
+ * @return
+ *   FALSE, if the file could not be found or is not an image. Otherwise, a
+ *   keyed array containing information about the image:
+ *   - "width": Width, in pixels.
+ *   - "height": Height, in pixels.
+ *   - "extension": Commonly used file extension for the image.
+ *   - "mime_type": MIME type ('image/jpeg', 'image/gif', 'image/png').
+ *   - "file_size": File size in bytes.
+ */
+function image_get_info($filepath, $toolkit = FALSE) {
+  $details = FALSE;
+  if (!is_file($filepath) && !is_uploaded_file($filepath)) {
+    return $details;
+  }
+
+  if (!$toolkit) {
+    $toolkit = image_get_toolkit();
+  }
+  if ($toolkit) {
+    $image = new stdClass();
+    $image->source = $filepath;
+    $image->toolkit = $toolkit;
+    $details = image_toolkit_invoke('get_info', $image);
+    if (isset($details) && is_array($details)) {
+      $details['file_size'] = filesize($filepath);
+    }
+  }
+
+  return $details;
+}
+
+/**
+ * Scales an image to the exact width and height given.
+ *
+ * This function achieves the target aspect ratio by cropping the original image
+ * equally on both sides, or equally on the top and bottom. This function is
+ * useful to create uniform sized avatars from larger images.
+ *
+ * The resulting image always has the exact target dimensions.
+ *
+ * @param $image
+ *   An image object returned by image_load().
+ * @param $width
+ *   The target width, in pixels.
+ * @param $height
+ *   The target height, in pixels.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ *
+ * @see image_load()
+ * @see image_resize()
+ * @see image_crop()
+ */
+function image_scale_and_crop(stdClass $image, $width, $height) {
+  $scale = max($width / $image->info['width'], $height / $image->info['height']);
+  $x = ($image->info['width'] * $scale - $width) / 2;
+  $y = ($image->info['height'] * $scale - $height) / 2;
+
+  if (image_resize($image, $image->info['width'] * $scale, $image->info['height'] * $scale)) {
+    return image_crop($image, $x, $y, $width, $height);
+  }
+  return FALSE;
+}
+
+/**
+ * Scales image dimensions while maintaining aspect ratio.
+ *
+ * The resulting dimensions can be smaller for one or both target dimensions.
+ *
+ * @param $dimensions
+ *   Dimensions to be modified - an array with components width and height, in
+ *   pixels.
+ * @param $width
+ *   The target width, in pixels. If this value is NULL then the scaling will be
+ *   based only on the height value.
+ * @param $height
+ *   The target height, in pixels. If this value is NULL then the scaling will
+ *   be based only on the width value.
+ * @param $upscale
+ *   Boolean indicating that images smaller than the target dimensions will be
+ *   scaled up. This generally results in a low quality image.
+ *
+ * @return
+ *   TRUE if $dimensions was modified, FALSE otherwise.
+ *
+ * @see image_scale()
+ */
+function image_dimensions_scale(array &$dimensions, $width = NULL, $height = NULL, $upscale = FALSE) {
+  $aspect = $dimensions['height'] / $dimensions['width'];
+
+  // Calculate one of the dimensions from the other target dimension,
+  // ensuring the same aspect ratio as the source dimensions. If one of the
+  // target dimensions is missing, that is the one that is calculated. If both
+  // are specified then the dimension calculated is the one that would not be
+  // calculated to be bigger than its target.
+  if (($width && !$height) || ($width && $height && $aspect < $height / $width)) {
+    $height = (int) round($width * $aspect);
+  }
+  else {
+    $width = (int) round($height / $aspect);
+  }
+
+  // Don't upscale if the option isn't enabled.
+  if (!$upscale && ($width >= $dimensions['width'] || $height >= $dimensions['height'])) {
+    return FALSE;
+  }
+
+  $dimensions['width'] = $width;
+  $dimensions['height'] = $height;
+  return TRUE;
+}
+
+/**
+ * Scales an image while maintaining aspect ratio.
+ *
+ * The resulting image can be smaller for one or both target dimensions.
+ *
+ * @param $image
+ *   An image object returned by image_load().
+ * @param $width
+ *   The target width, in pixels. If this value is NULL then the scaling will
+ *   be based only on the height value.
+ * @param $height
+ *   The target height, in pixels. If this value is NULL then the scaling will
+ *   be based only on the width value.
+ * @param $upscale
+ *   Boolean indicating that files smaller than the dimensions will be scaled
+ *   up. This generally results in a low quality image.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ *
+ * @see image_dimensions_scale()
+ * @see image_load()
+ * @see image_scale_and_crop()
+ */
+function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale = FALSE) {
+  $dimensions = $image->info;
+
+  // Scale the dimensions - if they don't change then just return success.
+  if (!image_dimensions_scale($dimensions, $width, $height, $upscale)) {
+    return TRUE;
+  }
+
+  return image_resize($image, $dimensions['width'], $dimensions['height']);
+}
+
+/**
+ * Resizes an image to the given dimensions (ignoring aspect ratio).
+ *
+ * @param $image
+ *   An image object returned by image_load().
+ * @param $width
+ *   The target width, in pixels.
+ * @param $height
+ *   The target height, in pixels.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ *
+ * @see image_load()
+ * @see image_gd_resize()
+ */
+function image_resize(stdClass $image, $width, $height) {
+  $width = (int) round($width);
+  $height = (int) round($height);
+
+  return image_toolkit_invoke('resize', $image, array($width, $height));
+}
+
+/**
+ * Rotates an image by the given number of degrees.
+ *
+ * @param $image
+ *   An image object returned by image_load().
+ * @param $degrees
+ *   The number of (clockwise) degrees to rotate the image.
+ * @param $background
+ *   An hexadecimal integer specifying the background color to use for the
+ *   uncovered area of the image after the rotation. E.g. 0x000000 for black,
+ *   0xff00ff for magenta, and 0xffffff for white. For images that support
+ *   transparency, this will default to transparent. Otherwise it will
+ *   be white.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ *
+ * @see image_load()
+ * @see image_gd_rotate()
+ */
+function image_rotate(stdClass $image, $degrees, $background = NULL) {
+  return image_toolkit_invoke('rotate', $image, array($degrees, $background));
+}
+
+/**
+ * Crops an image to a rectangle specified by the given dimensions.
+ *
+ * @param $image
+ *   An image object returned by image_load().
+ * @param $x
+ *   The top left coordinate, in pixels, of the crop area (x axis value).
+ * @param $y
+ *   The top left coordinate, in pixels, of the crop area (y axis value).
+ * @param $width
+ *   The target width, in pixels.
+ * @param $height
+ *   The target height, in pixels.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ *
+ * @see image_load()
+ * @see image_scale_and_crop()
+ * @see image_gd_crop()
+ */
+function image_crop(stdClass $image, $x, $y, $width, $height) {
+  $aspect = $image->info['height'] / $image->info['width'];
+  if (empty($height)) $height = $width / $aspect;
+  if (empty($width)) $width = $height * $aspect;
+
+  $width = (int) round($width);
+  $height = (int) round($height);
+
+  return image_toolkit_invoke('crop', $image, array($x, $y, $width, $height));
+}
+
+/**
+ * Converts an image to grayscale.
+ *
+ * @param $image
+ *   An image object returned by image_load().
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ *
+ * @see image_load()
+ * @see image_gd_desaturate()
+ */
+function image_desaturate(stdClass $image) {
+  return image_toolkit_invoke('desaturate', $image);
+}
+
+/**
+ * Loads an image file and returns an image object.
+ *
+ * Any changes to the file are not saved until image_save() is called.
+ *
+ * @param $file
+ *   Path to an image file.
+ * @param $toolkit
+ *   An optional, image toolkit name to override the default.
+ *
+ * @return
+ *   An image object or FALSE if there was a problem loading the file. The
+ *   image object has the following properties:
+ *    - 'source' - The original file path.
+ *    - 'info' - The array of information returned by image_get_info()
+ *    - 'toolkit' - The name of the image toolkit requested when the image was
+ *      loaded.
+ *   Image toolkits may add additional properties. The caller is advised not to
+ *   monkey about with them.
+ *
+ * @see image_save()
+ * @see image_get_info()
+ * @see image_get_available_toolkits()
+ * @see image_gd_load()
+ */
+function image_load($file, $toolkit = FALSE) {
+  if (!$toolkit) {
+    $toolkit = image_get_toolkit();
+  }
+  if ($toolkit) {
+    $image = new stdClass();
+    $image->source = $file;
+    $image->info = image_get_info($file, $toolkit);
+    if (isset($image->info) && is_array($image->info)) {
+      $image->toolkit = $toolkit;
+      if (image_toolkit_invoke('load', $image)) {
+        return $image;
+      }
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Closes the image and saves the changes to a file.
+ *
+ * @param $image
+ *   An image object returned by image_load(). The object's 'info' property
+ *   will be updated if the file is saved successfully.
+ * @param $destination
+ *   Destination path where the image should be saved. If it is empty the
+ *   original image file will be overwritten.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ *
+ * @see image_load()
+ * @see image_gd_save()
+ */
+function image_save(stdClass $image, $destination = NULL) {
+  if (empty($destination)) {
+    $destination = $image->source;
+  }
+  if ($return = image_toolkit_invoke('save', $image, array($destination))) {
+    // Clear the cached file size and refresh the image information.
+    clearstatcache();
+    $image->info = image_get_info($destination, $image->toolkit);
+
+    if (drupal_chmod($destination)) {
+      return $return;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * @} End of "defgroup image".
+ */

File diff suppressed because it is too large
+ 1700 - 0
includes/install.core.inc


+ 1326 - 0
includes/install.inc

@@ -0,0 +1,1326 @@
+<?php
+
+/**
+* @file
+* API functions for installing modules and themes.
+*/
+
+/**
+ * Indicates that a module has not been installed yet.
+ */
+define('SCHEMA_UNINSTALLED', -1);
+
+/**
+ * Indicates that a module has been installed.
+ */
+define('SCHEMA_INSTALLED', 0);
+
+/**
+ * Requirement severity -- Informational message only.
+ */
+define('REQUIREMENT_INFO', -1);
+
+/**
+ * Requirement severity -- Requirement successfully met.
+ */
+define('REQUIREMENT_OK', 0);
+
+/**
+ * Requirement severity -- Warning condition; proceed but flag warning.
+ */
+define('REQUIREMENT_WARNING', 1);
+
+/**
+ * Requirement severity -- Error condition; abort installation.
+ */
+define('REQUIREMENT_ERROR', 2);
+
+/**
+ * File permission check -- File exists.
+ */
+define('FILE_EXIST', 1);
+
+/**
+ * File permission check -- File is readable.
+ */
+define('FILE_READABLE', 2);
+
+/**
+ * File permission check -- File is writable.
+ */
+define('FILE_WRITABLE', 4);
+
+/**
+ * File permission check -- File is executable.
+ */
+define('FILE_EXECUTABLE', 8);
+
+/**
+ * File permission check -- File does not exist.
+ */
+define('FILE_NOT_EXIST', 16);
+
+/**
+ * File permission check -- File is not readable.
+ */
+define('FILE_NOT_READABLE', 32);
+
+/**
+ * File permission check -- File is not writable.
+ */
+define('FILE_NOT_WRITABLE', 64);
+
+/**
+ * File permission check -- File is not executable.
+ */
+define('FILE_NOT_EXECUTABLE', 128);
+
+/**
+ * Loads .install files for installed modules to initialize the update system.
+ */
+function drupal_load_updates() {
+  foreach (drupal_get_installed_schema_version(NULL, FALSE, TRUE) as $module => $schema_version) {
+    if ($schema_version > -1) {
+      module_load_install($module);
+    }
+  }
+}
+
+/**
+ * Returns an array of available schema versions for a module.
+ *
+ * @param $module
+ *   A module name.
+ * @return
+ *   If the module has updates, an array of available updates sorted by version.
+ *   Otherwise, FALSE.
+ */
+function drupal_get_schema_versions($module) {
+  $updates = &drupal_static(__FUNCTION__, NULL);
+  if (!isset($updates[$module])) {
+    $updates = array();
+
+    foreach (module_list() as $loaded_module) {
+      $updates[$loaded_module] = array();
+    }
+
+    // Prepare regular expression to match all possible defined hook_update_N().
+    $regexp = '/^(?P<module>.+)_update_(?P<version>\d+)$/';
+    $functions = get_defined_functions();
+    // Narrow this down to functions ending with an integer, since all
+    // hook_update_N() functions end this way, and there are other
+    // possible functions which match '_update_'. We use preg_grep() here
+    // instead of foreaching through all defined functions, since the loop
+    // through all PHP functions can take significant page execution time
+    // and this function is called on every administrative page via
+    // system_requirements().
+    foreach (preg_grep('/_\d+$/', $functions['user']) as $function) {
+      // If this function is a module update function, add it to the list of
+      // module updates.
+      if (preg_match($regexp, $function, $matches)) {
+        $updates[$matches['module']][] = $matches['version'];
+      }
+    }
+    // Ensure that updates are applied in numerical order.
+    foreach ($updates as &$module_updates) {
+      sort($module_updates, SORT_NUMERIC);
+    }
+  }
+  return empty($updates[$module]) ? FALSE : $updates[$module];
+}
+
+/**
+ * Returns the currently installed schema version for a module.
+ *
+ * @param $module
+ *   A module name.
+ * @param $reset
+ *   Set to TRUE after modifying the system table.
+ * @param $array
+ *   Set to TRUE if you want to get information about all modules in the
+ *   system.
+ * @return
+ *   The currently installed schema version, or SCHEMA_UNINSTALLED if the
+ *   module is not installed.
+ */
+function drupal_get_installed_schema_version($module, $reset = FALSE, $array = FALSE) {
+  static $versions = array();
+
+  if ($reset) {
+    $versions = array();
+  }
+
+  if (!$versions) {
+    $versions = array();
+    $result = db_query("SELECT name, schema_version FROM {system} WHERE type = :type", array(':type' => 'module'));
+    foreach ($result as $row) {
+      $versions[$row->name] = $row->schema_version;
+    }
+  }
+
+  if ($array) {
+    return $versions;
+  }
+  else {
+    return isset($versions[$module]) ? $versions[$module] : SCHEMA_UNINSTALLED;
+  }
+}
+
+/**
+ * Update the installed version information for a module.
+ *
+ * @param $module
+ *   A module name.
+ * @param $version
+ *   The new schema version.
+ */
+function drupal_set_installed_schema_version($module, $version) {
+  db_update('system')
+    ->fields(array('schema_version' => $version))
+    ->condition('name', $module)
+    ->execute();
+
+  // Reset the static cache of module schema versions.
+  drupal_get_installed_schema_version(NULL, TRUE);
+}
+
+/**
+ * Loads the installation profile, extracting its defined distribution name.
+ *
+ * @return
+ *   The distribution name defined in the profile's .info file. Defaults to
+ *   "Drupal" if none is explicitly provided by the installation profile.
+ *
+ * @see install_profile_info()
+ */
+function drupal_install_profile_distribution_name() {
+  // During installation, the profile information is stored in the global
+  // installation state (it might not be saved anywhere yet).
+  if (drupal_installation_attempted()) {
+    global $install_state;
+    return $install_state['profile_info']['distribution_name'];
+  }
+  // At all other times, we load the profile via standard methods.
+  else {
+    $profile = drupal_get_profile();
+    $info = system_get_info('module', $profile);
+    return $info['distribution_name'];
+  }
+}
+
+/**
+ * Detects the base URL using the PHP $_SERVER variables.
+ *
+ * @param $file
+ *   The name of the file calling this function so we can strip it out of
+ *   the URI when generating the base_url.
+ *
+ * @return
+ *   The auto-detected $base_url that should be configured in settings.php
+ */
+function drupal_detect_baseurl($file = 'install.php') {
+  $proto = $_SERVER['HTTPS'] ? 'https://' : 'http://';
+  $host = $_SERVER['SERVER_NAME'];
+  $port = ($_SERVER['SERVER_PORT'] == 80 ? '' : ':' . $_SERVER['SERVER_PORT']);
+  $uri = preg_replace("/\?.*/", '', $_SERVER['REQUEST_URI']);
+  $dir = str_replace("/$file", '', $uri);
+
+  return "$proto$host$port$dir";
+}
+
+/**
+ * Detects all supported databases that are compiled into PHP.
+ *
+ * @return
+ *  An array of database types compiled into PHP.
+ */
+function drupal_detect_database_types() {
+  $databases = drupal_get_database_types();
+
+  foreach ($databases as $driver => $installer) {
+    $databases[$driver] = $installer->name();
+  }
+
+  return $databases;
+}
+
+/**
+ * Returns all supported database installer objects that are compiled into PHP.
+ *
+ * @return
+ *  An array of database installer objects compiled into PHP.
+ */
+function drupal_get_database_types() {
+  $databases = array();
+
+  // We define a driver as a directory in /includes/database that in turn
+  // contains a database.inc file. That allows us to drop in additional drivers
+  // without modifying the installer.
+  // Because we have no registry yet, we need to also include the install.inc
+  // file for the driver explicitly.
+  require_once DRUPAL_ROOT . '/includes/database/database.inc';
+  foreach (file_scan_directory(DRUPAL_ROOT . '/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) {
+    if (file_exists($file->uri . '/database.inc') && file_exists($file->uri . '/install.inc')) {
+      $drivers[$file->filename] = $file->uri;
+    }
+  }
+
+  foreach ($drivers as $driver => $file) {
+    $installer = db_installer_object($driver);
+    if ($installer->installable()) {
+      $databases[$driver] = $installer;
+    }
+  }
+
+  // Usability: unconditionally put the MySQL driver on top.
+  if (isset($databases['mysql'])) {
+    $mysql_database = $databases['mysql'];
+    unset($databases['mysql']);
+    $databases = array('mysql' => $mysql_database) + $databases;
+  }
+
+  return $databases;
+}
+
+/**
+ * Database installer structure.
+ *
+ * Defines basic Drupal requirements for databases.
+ */
+abstract class DatabaseTasks {
+
+  /**
+   * Structure that describes each task to run.
+   *
+   * @var array
+   *
+   * Each value of the tasks array is an associative array defining the function
+   * to call (optional) and any arguments to be passed to the function.
+   */
+  protected $tasks = array(
+    array(
+      'function'    => 'checkEngineVersion',
+      'arguments'   => array(),
+    ),
+    array(
+      'arguments'   => array(
+        'CREATE TABLE {drupal_install_test} (id int NULL)',
+        'Drupal can use CREATE TABLE database commands.',
+        'Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>',
+        TRUE,
+      ),
+    ),
+    array(
+      'arguments'   => array(
+        'INSERT INTO {drupal_install_test} (id) VALUES (1)',
+        'Drupal can use INSERT database commands.',
+        'Failed to <strong>INSERT</strong> a value into a test table on your database server. We tried inserting a value with the command %query and the server reported the following error: %error.',
+      ),
+    ),
+    array(
+      'arguments'   => array(
+        'UPDATE {drupal_install_test} SET id = 2',
+        'Drupal can use UPDATE database commands.',
+        'Failed to <strong>UPDATE</strong> a value in a test table on your database server. We tried updating a value with the command %query and the server reported the following error: %error.',
+      ),
+    ),
+    array(
+      'arguments'   => array(
+        'DELETE FROM {drupal_install_test}',
+        'Drupal can use DELETE database commands.',
+        'Failed to <strong>DELETE</strong> a value from a test table on your database server. We tried deleting a value with the command %query and the server reported the following error: %error.',
+      ),
+    ),
+    array(
+      'arguments'   => array(
+        'DROP TABLE {drupal_install_test}',
+        'Drupal can use DROP TABLE database commands.',
+        'Failed to <strong>DROP</strong> a test table from your database server. We tried dropping a table with the command %query and the server reported the following error %error.',
+      ),
+    ),
+  );
+
+  /**
+   * Results from tasks.
+   *
+   * @var array
+   */
+  protected $results = array();
+
+  /**
+   * Ensure the PDO driver is supported by the version of PHP in use.
+   */
+  protected function hasPdoDriver() {
+    return in_array($this->pdoDriver, PDO::getAvailableDrivers());
+  }
+
+  /**
+   * Assert test as failed.
+   */
+  protected function fail($message) {
+    $this->results[$message] = FALSE;
+  }
+
+  /**
+   * Assert test as a pass.
+   */
+  protected function pass($message) {
+    $this->results[$message] = TRUE;
+  }
+
+  /**
+   * Check whether Drupal is installable on the database.
+   */
+  public function installable() {
+    return $this->hasPdoDriver() && empty($this->error);
+  }
+
+  /**
+   * Return the human-readable name of the driver.
+   */
+  abstract public function name();
+
+  /**
+   * Return the minimum required version of the engine.
+   *
+   * @return
+   *   A version string. If not NULL, it will be checked against the version
+   *   reported by the Database engine using version_compare().
+   */
+  public function minimumVersion() {
+    return NULL;
+  }
+
+  /**
+   * Run database tasks and tests to see if Drupal can run on the database.
+   */
+  public function runTasks() {
+    // We need to establish a connection before we can run tests.
+    if ($this->connect()) {
+      foreach ($this->tasks as $task) {
+        if (!isset($task['function'])) {
+          $task['function'] = 'runTestQuery';
+        }
+        if (method_exists($this, $task['function'])) {
+          // Returning false is fatal. No other tasks can run.
+          if (FALSE === call_user_func_array(array($this, $task['function']), $task['arguments'])) {
+            break;
+          }
+        }
+        else {
+          throw new DatabaseTaskException(st("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
+        }
+      }
+    }
+    // Check for failed results and compile message
+    $message = '';
+    foreach ($this->results as $result => $success) {
+      if (!$success) {
+        $message .= '<p class="error">' . $result  . '</p>';
+      }
+    }
+    if (!empty($message)) {
+      $message = '<p>In order for Drupal to work, and to continue with the installation process, you must resolve all issues reported below. For more help with configuring your database server, see the <a href="http://drupal.org/getting-started/install">installation handbook</a>. If you are unsure what any of this means you should probably contact your hosting provider.</p>' . $message;
+      throw new DatabaseTaskException($message);
+    }
+  }
+
+  /**
+   * Check if we can connect to the database.
+   */
+  protected function connect() {
+    try {
+      // This doesn't actually test the connection.
+      db_set_active();
+      // Now actually do a check.
+      Database::getConnection();
+      $this->pass('Drupal can CONNECT to the database ok.');
+    }
+    catch (Exception $e) {
+      $this->fail(st('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', array('%error' => $e->getMessage())));
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+  /**
+   * Run SQL tests to ensure the database can execute commands with the current user.
+   */
+  protected function runTestQuery($query, $pass, $fail, $fatal = FALSE) {
+    try {
+      db_query($query);
+      $this->pass(st($pass));
+    }
+    catch (Exception $e) {
+      $this->fail(st($fail, array('%query' => $query, '%error' => $e->getMessage(), '%name' => $this->name())));
+      return !$fatal;
+    }
+  }
+
+  /**
+   * Check the engine version.
+   */
+  protected function checkEngineVersion() {
+    if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) {
+      $this->fail(st("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
+    }
+  }
+
+  /**
+   * Return driver specific configuration options.
+   *
+   * @param $database
+   *  An array of driver specific configuration options.
+   *
+   * @return
+   *   The options form array.
+   */
+  public function getFormOptions($database) {
+    $form['database'] = array(
+      '#type' => 'textfield',
+      '#title' => st('Database name'),
+      '#default_value' => empty($database['database']) ? '' : $database['database'],
+      '#size' => 45,
+      '#required' => TRUE,
+      '#description' => st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_distribution_name())),
+    );
+
+    $form['username'] = array(
+      '#type' => 'textfield',
+      '#title' => st('Database username'),
+      '#default_value' => empty($database['username']) ? '' : $database['username'],
+      '#required' => TRUE,
+      '#size' => 45,
+    );
+
+    $form['password'] = array(
+      '#type' => 'password',
+      '#title' => st('Database password'),
+      '#default_value' => empty($database['password']) ? '' : $database['password'],
+      '#required' => FALSE,
+      '#size' => 45,
+    );
+
+    $form['advanced_options'] = array(
+      '#type' => 'fieldset',
+      '#title' => st('Advanced options'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider."),
+      '#weight' => 10,
+    );
+
+    $profile = drupal_get_profile();
+    $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_';
+    $form['advanced_options']['db_prefix'] = array(
+      '#type' => 'textfield',
+      '#title' => st('Table prefix'),
+      '#default_value' => '',
+      '#size' => 45,
+      '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_distribution_name(), '%prefix' => $db_prefix)),
+      '#weight' => 10,
+    );
+
+    $form['advanced_options']['host'] = array(
+      '#type' => 'textfield',
+      '#title' => st('Database host'),
+      '#default_value' => empty($database['host']) ? 'localhost' : $database['host'],
+      '#size' => 45,
+      // Hostnames can be 255 characters long.
+      '#maxlength' => 255,
+      '#required' => TRUE,
+      '#description' => st('If your database is located on a different server, change this.'),
+    );
+
+    $form['advanced_options']['port'] = array(
+      '#type' => 'textfield',
+      '#title' => st('Database port'),
+      '#default_value' => empty($database['port']) ? '' : $database['port'],
+      '#size' => 45,
+      // The maximum port number is 65536, 5 digits.
+      '#maxlength' => 5,
+      '#description' => st('If your database server is listening to a non-standard port, enter its number.'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * Validates driver specific configuration settings.
+   *
+   * Checks to ensure correct basic database settings and that a proper
+   * connection to the database can be established.
+   *
+   * @param $database
+   *   An array of driver specific configuration options.
+   *
+   * @return
+   *   An array of driver configuration errors, keyed by form element name.
+   */
+  public function validateDatabaseSettings($database) {
+    $errors = array();
+
+    // Verify the table prefix.
+    if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) {
+      $errors[$database['driver'] . '][advanced_options][db_prefix'] = st('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $database['prefix']));
+    }
+
+    // Verify the database port.
+    if (!empty($database['port']) && !is_numeric($database['port'])) {
+      $errors[$database['driver'] . '][advanced_options][port'] =  st('Database port must be a number.');
+    }
+
+    return $errors;
+  }
+
+}
+
+/**
+ * Exception thrown if the database installer fails.
+ */
+class DatabaseTaskException extends Exception {
+}
+
+/**
+ * Replaces values in settings.php with values in the submitted array.
+ *
+ * @param $settings
+ *   An array of settings that need to be updated.
+ */
+function drupal_rewrite_settings($settings = array(), $prefix = '') {
+  $default_settings = 'sites/default/default.settings.php';
+  drupal_static_reset('conf_path');
+  $settings_file = conf_path(FALSE) . '/' . $prefix . 'settings.php';
+
+  // Build list of setting names and insert the values into the global namespace.
+  $keys = array();
+  foreach ($settings as $setting => $data) {
+    $GLOBALS[$setting] = $data['value'];
+    $keys[] = $setting;
+  }
+
+  $buffer = NULL;
+  $first = TRUE;
+  if ($fp = fopen(DRUPAL_ROOT . '/' . $default_settings, 'r')) {
+    // Step line by line through settings.php.
+    while (!feof($fp)) {
+      $line = fgets($fp);
+      if ($first && substr($line, 0, 5) != '<?php') {
+        $buffer = "<?php\n\n";
+      }
+      $first = FALSE;
+      // Check for constants.
+      if (substr($line, 0, 7) == 'define(') {
+        preg_match('/define\(\s*[\'"]([A-Z_-]+)[\'"]\s*,(.*?)\);/', $line, $variable);
+        if (in_array($variable[1], $keys)) {
+          $setting = $settings[$variable[1]];
+          $buffer .= str_replace($variable[2], " '" . $setting['value'] . "'", $line);
+          unset($settings[$variable[1]]);
+          unset($settings[$variable[2]]);
+        }
+        else {
+          $buffer .= $line;
+        }
+      }
+      // Check for variables.
+      elseif (substr($line, 0, 1) == '$') {
+        preg_match('/\$([^ ]*) /', $line, $variable);
+        if (in_array($variable[1], $keys)) {
+          // Write new value to settings.php in the following format:
+          //    $'setting' = 'value'; // 'comment'
+          $setting = $settings[$variable[1]];
+          $buffer .= '$' . $variable[1] . " = " . var_export($setting['value'], TRUE) . ";" . (!empty($setting['comment']) ? ' // ' . $setting['comment'] . "\n" : "\n");
+          unset($settings[$variable[1]]);
+        }
+        else {
+          $buffer .= $line;
+        }
+      }
+      else {
+        $buffer .= $line;
+      }
+    }
+    fclose($fp);
+
+    // Add required settings that were missing from settings.php.
+    foreach ($settings as $setting => $data) {
+      if ($data['required']) {
+        $buffer .= "\$$setting = " . var_export($data['value'], TRUE) . ";\n";
+      }
+    }
+
+    $fp = fopen(DRUPAL_ROOT . '/' . $settings_file, 'w');
+    if ($fp && fwrite($fp, $buffer) === FALSE) {
+      throw new Exception(st('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file)));
+    }
+  }
+  else {
+    throw new Exception(st('Failed to open %settings. Verify the file permissions.', array('%settings' => $default_settings)));
+  }
+}
+
+/**
+ * Verifies an installation profile for installation.
+ *
+ * @param $install_state
+ *   An array of information about the current installation state.
+ *
+ * @return
+ *   The list of modules to install.
+ */
+function drupal_verify_profile($install_state) {
+  $profile = $install_state['parameters']['profile'];
+  $locale = $install_state['parameters']['locale'];
+
+  include_once DRUPAL_ROOT . '/includes/file.inc';
+  include_once DRUPAL_ROOT . '/includes/common.inc';
+
+  $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile";
+
+  if (!isset($profile) || !file_exists($profile_file)) {
+    throw new Exception(install_no_profile_error());
+  }
+  $info = $install_state['profile_info'];
+
+  // Get a list of modules that exist in Drupal's assorted subdirectories.
+  $present_modules = array();
+  foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0) as $present_module) {
+    $present_modules[] = $present_module->name;
+  }
+
+  // The installation profile is also a module, which needs to be installed
+  // after all the other dependencies have been installed.
+  $present_modules[] = drupal_get_profile();
+
+  // Verify that all of the profile's required modules are present.
+  $missing_modules = array_diff($info['dependencies'], $present_modules);
+
+  $requirements = array();
+
+  if (count($missing_modules)) {
+    $modules = array();
+    foreach ($missing_modules as $module) {
+      $modules[] = '<span class="admin-missing">' . drupal_ucfirst($module) . '</span>';
+    }
+    $requirements['required_modules'] = array(
+      'title'       => st('Required modules'),
+      'value'       => st('Required modules not found.'),
+      'severity'    => REQUIREMENT_ERROR,
+      'description' => st('The following modules are required but were not found. Move them into the appropriate modules subdirectory, such as <em>sites/all/modules</em>. Missing modules: !modules', array('!modules' => implode(', ', $modules))),
+    );
+  }
+  return $requirements;
+}
+
+/**
+ * Installs the system module.
+ *
+ * Separated from the installation of other modules so core system
+ * functions can be made available while other modules are installed.
+ */
+function drupal_install_system() {
+  $system_path = drupal_get_path('module', 'system');
+  require_once DRUPAL_ROOT . '/' . $system_path . '/system.install';
+  module_invoke('system', 'install');
+
+  $system_versions = drupal_get_schema_versions('system');
+  $system_version = $system_versions ? max($system_versions) : SCHEMA_INSTALLED;
+  db_insert('system')
+    ->fields(array('filename', 'name', 'type', 'owner', 'status', 'schema_version', 'bootstrap'))
+    ->values(array(
+        'filename' => $system_path . '/system.module',
+        'name' => 'system',
+        'type' => 'module',
+        'owner' => '',
+        'status' => 1,
+        'schema_version' => $system_version,
+        'bootstrap' => 0,
+      ))
+    ->execute();
+  system_rebuild_module_data();
+}
+
+/**
+ * Uninstalls a given list of modules.
+ *
+ * @param $module_list
+ *   The modules to uninstall.
+ * @param $uninstall_dependents
+ *   If TRUE, the function will check that all modules which depend on the
+ *   passed-in module list either are already uninstalled or contained in the
+ *   list, and it will ensure that the modules are uninstalled in the correct
+ *   order. This incurs a significant performance cost, so use FALSE if you
+ *   know $module_list is already complete and in the correct order.
+ *
+ * @return
+ *   FALSE if one or more dependent modules are missing from the list, TRUE
+ *   otherwise.
+ */
+function drupal_uninstall_modules($module_list = array(), $uninstall_dependents = TRUE) {
+  if ($uninstall_dependents) {
+    // Get all module data so we can find dependents and sort.
+    $module_data = system_rebuild_module_data();
+    // Create an associative array with weights as values.
+    $module_list = array_flip(array_values($module_list));
+
+    $profile = drupal_get_profile();
+    while (list($module) = each($module_list)) {
+      if (!isset($module_data[$module]) || drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) {
+        // This module doesn't exist or is already uninstalled, skip it.
+        unset($module_list[$module]);
+        continue;
+      }
+      $module_list[$module] = $module_data[$module]->sort;
+
+      // If the module has any dependents which are not already uninstalled and
+      // not included in the passed-in list, abort. It is not safe to uninstall
+      // them automatically because uninstalling a module is a destructive
+      // operation.
+      foreach (array_keys($module_data[$module]->required_by) as $dependent) {
+        if (!isset($module_list[$dependent]) && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED && $dependent != $profile) {
+          return FALSE;
+        }
+      }
+    }
+
+    // Sort the module list by pre-calculated weights.
+    asort($module_list);
+    $module_list = array_keys($module_list);
+  }
+
+  foreach ($module_list as $module) {
+    // Uninstall the module.
+    module_load_install($module);
+    module_invoke($module, 'uninstall');
+    drupal_uninstall_schema($module);
+
+    watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO);
+    drupal_set_installed_schema_version($module, SCHEMA_UNINSTALLED);
+  }
+
+  if (!empty($module_list)) {
+    // Call hook_module_uninstall to let other modules act
+    module_invoke_all('modules_uninstalled', $module_list);
+  }
+
+  return TRUE;
+}
+
+/**
+ * Verifies the state of the specified file.
+ *
+ * @param $file
+ *   The file to check for.
+ * @param $mask
+ *   An optional bitmask created from various FILE_* constants.
+ * @param $type
+ *   The type of file. Can be file (default), dir, or link.
+ *
+ * @return
+ *   TRUE on success or FALSE on failure. A message is set for the latter.
+ */
+function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
+  $return = TRUE;
+  // Check for files that shouldn't be there.
+  if (isset($mask) && ($mask & FILE_NOT_EXIST) && file_exists($file)) {
+    return FALSE;
+  }
+  // Verify that the file is the type of file it is supposed to be.
+  if (isset($type) && file_exists($file)) {
+    $check = 'is_' . $type;
+    if (!function_exists($check) || !$check($file)) {
+      $return = FALSE;
+    }
+  }
+
+  // Verify file permissions.
+  if (isset($mask)) {
+    $masks = array(FILE_EXIST, FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE);
+    foreach ($masks as $current_mask) {
+      if ($mask & $current_mask) {
+        switch ($current_mask) {
+          case FILE_EXIST:
+            if (!file_exists($file)) {
+              if ($type == 'dir') {
+                drupal_install_mkdir($file, $mask);
+              }
+              if (!file_exists($file)) {
+                $return = FALSE;
+              }
+            }
+            break;
+          case FILE_READABLE:
+            if (!is_readable($file) && !drupal_install_fix_file($file, $mask)) {
+              $return = FALSE;
+            }
+            break;
+          case FILE_WRITABLE:
+            if (!is_writable($file) && !drupal_install_fix_file($file, $mask)) {
+              $return = FALSE;
+            }
+            break;
+          case FILE_EXECUTABLE:
+            if (!is_executable($file) && !drupal_install_fix_file($file, $mask)) {
+              $return = FALSE;
+            }
+            break;
+          case FILE_NOT_READABLE:
+            if (is_readable($file) && !drupal_install_fix_file($file, $mask)) {
+              $return = FALSE;
+            }
+            break;
+          case FILE_NOT_WRITABLE:
+            if (is_writable($file) && !drupal_install_fix_file($file, $mask)) {
+              $return = FALSE;
+            }
+            break;
+          case FILE_NOT_EXECUTABLE:
+            if (is_executable($file) && !drupal_install_fix_file($file, $mask)) {
+              $return = FALSE;
+            }
+            break;
+        }
+      }
+    }
+  }
+  return $return;
+}
+
+/**
+ * Creates a directory with the specified permissions.
+ *
+ * @param $file
+ *  The name of the directory to create;
+ * @param $mask
+ *  The permissions of the directory to create.
+ * @param $message
+ *  (optional) Whether to output messages. Defaults to TRUE.
+ *
+ * @return
+ *  TRUE/FALSE whether or not the directory was successfully created.
+ */
+function drupal_install_mkdir($file, $mask, $message = TRUE) {
+  $mod = 0;
+  $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE);
+  foreach ($masks as $m) {
+    if ($mask & $m) {
+      switch ($m) {
+        case FILE_READABLE:
+          $mod |= 0444;
+          break;
+        case FILE_WRITABLE:
+          $mod |= 0222;
+          break;
+        case FILE_EXECUTABLE:
+          $mod |= 0111;
+          break;
+      }
+    }
+  }
+
+  if (@drupal_mkdir($file, $mod)) {
+    return TRUE;
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Attempts to fix file permissions.
+ *
+ * The general approach here is that, because we do not know the security
+ * setup of the webserver, we apply our permission changes to all three
+ * digits of the file permission (i.e. user, group and all).
+ *
+ * To ensure that the values behave as expected (and numbers don't carry
+ * from one digit to the next) we do the calculation on the octal value
+ * using bitwise operations. This lets us remove, for example, 0222 from
+ * 0700 and get the correct value of 0500.
+ *
+ * @param $file
+ *  The name of the file with permissions to fix.
+ * @param $mask
+ *  The desired permissions for the file.
+ * @param $message
+ *  (optional) Whether to output messages. Defaults to TRUE.
+ *
+ * @return
+ *  TRUE/FALSE whether or not we were able to fix the file's permissions.
+ */
+function drupal_install_fix_file($file, $mask, $message = TRUE) {
+  // If $file does not exist, fileperms() issues a PHP warning.
+  if (!file_exists($file)) {
+    return FALSE;
+  }
+
+  $mod = fileperms($file) & 0777;
+  $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE);
+
+  // FILE_READABLE, FILE_WRITABLE, and FILE_EXECUTABLE permission strings
+  // can theoretically be 0400, 0200, and 0100 respectively, but to be safe
+  // we set all three access types in case the administrator intends to
+  // change the owner of settings.php after installation.
+  foreach ($masks as $m) {
+    if ($mask & $m) {
+      switch ($m) {
+        case FILE_READABLE:
+          if (!is_readable($file)) {
+            $mod |= 0444;
+          }
+          break;
+        case FILE_WRITABLE:
+          if (!is_writable($file)) {
+            $mod |= 0222;
+          }
+          break;
+        case FILE_EXECUTABLE:
+          if (!is_executable($file)) {
+            $mod |= 0111;
+          }
+          break;
+        case FILE_NOT_READABLE:
+          if (is_readable($file)) {
+            $mod &= ~0444;
+          }
+          break;
+        case FILE_NOT_WRITABLE:
+          if (is_writable($file)) {
+            $mod &= ~0222;
+          }
+          break;
+        case FILE_NOT_EXECUTABLE:
+          if (is_executable($file)) {
+            $mod &= ~0111;
+          }
+          break;
+      }
+    }
+  }
+
+  // chmod() will work if the web server is running as owner of the file.
+  // If PHP safe_mode is enabled the currently executing script must also
+  // have the same owner.
+  if (@chmod($file, $mod)) {
+    return TRUE;
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Sends the user to a different installer page.
+ *
+ * This issues an on-site HTTP redirect. Messages (and errors) are erased.
+ *
+ * @param $path
+ *   An installer path.
+ */
+function install_goto($path) {
+  global $base_url;
+  include_once DRUPAL_ROOT . '/includes/common.inc';
+  header('Location: ' . $base_url . '/' . $path);
+  header('Cache-Control: no-cache'); // Not a permanent redirect.
+  drupal_exit();
+}
+
+/**
+ * Returns the URL of the current script, with modified query parameters.
+ *
+ * This function can be called by low-level scripts (such as install.php and
+ * update.php) and returns the URL of the current script. Existing query
+ * parameters are preserved by default, but new ones can optionally be merged
+ * in.
+ *
+ * This function is used when the script must maintain certain query parameters
+ * over multiple page requests in order to work correctly. In such cases (for
+ * example, update.php, which requires the 'continue=1' parameter to remain in
+ * the URL throughout the update process if there are any requirement warnings
+ * that need to be bypassed), using this function to generate the URL for links
+ * to the next steps of the script ensures that the links will work correctly.
+ *
+ * @param $query
+ *   (optional) An array of query parameters to merge in to the existing ones.
+ *
+ * @return
+ *   The URL of the current script, with query parameters modified by the
+ *   passed-in $query. The URL is not sanitized, so it still needs to be run
+ *   through check_url() if it will be used as an HTML attribute value.
+ *
+ * @see drupal_requirements_url()
+ */
+function drupal_current_script_url($query = array()) {
+  $uri = $_SERVER['SCRIPT_NAME'];
+  $query = array_merge(drupal_get_query_parameters(), $query);
+  if (!empty($query)) {
+    $uri .= '?' . drupal_http_build_query($query);
+  }
+  return $uri;
+}
+
+/**
+ * Returns a URL for proceeding to the next page after a requirements problem.
+ *
+ * This function can be called by low-level scripts (such as install.php and
+ * update.php) and returns a URL that can be used to attempt to proceed to the
+ * next step of the script.
+ *
+ * @param $severity
+ *   The severity of the requirements problem, as returned by
+ *   drupal_requirements_severity().
+ *
+ * @return
+ *   A URL for attempting to proceed to the next step of the script. The URL is
+ *   not sanitized, so it still needs to be run through check_url() if it will
+ *   be used as an HTML attribute value.
+ *
+ * @see drupal_current_script_url()
+ */
+function drupal_requirements_url($severity) {
+  $query = array();
+  // If there are no errors, only warnings, append 'continue=1' to the URL so
+  // the user can bypass this screen on the next page load.
+  if ($severity == REQUIREMENT_WARNING) {
+    $query['continue'] = 1;
+  }
+  return drupal_current_script_url($query);
+}
+
+/**
+ * Translates a string when some systems are not available.
+ *
+ * Used during the install process, when database, theme, and localization
+ * system is possibly not yet available.
+ *
+ * Use t() if your code will never run during the Drupal installation phase.
+ * Use st() if your code will only run during installation and never any other
+ * time. Use get_t() if your code could run in either circumstance.
+ *
+ * @see t()
+ * @see get_t()
+ * @ingroup sanitization
+ */
+function st($string, array $args = array(), array $options = array()) {
+  static $locale_strings = NULL;
+  global $install_state;
+
+  if (empty($options['context'])) {
+    $options['context'] = '';
+  }
+
+  if (!isset($locale_strings)) {
+    $locale_strings = array();
+    if (isset($install_state['parameters']['profile']) && isset($install_state['parameters']['locale'])) {
+      // If the given locale was selected, there should be at least one .po file
+      // with its name ending in {$install_state['parameters']['locale']}.po
+      // This might or might not be the entire filename. It is also possible
+      // that multiple files end with the same extension, even if unlikely.
+      $po_files = file_scan_directory('./profiles/' . $install_state['parameters']['profile'] . '/translations', '/'. $install_state['parameters']['locale'] .'\.po$/', array('recurse' => FALSE));
+      if (count($po_files)) {
+        require_once DRUPAL_ROOT . '/includes/locale.inc';
+        foreach ($po_files as $po_file) {
+          _locale_import_read_po('mem-store', $po_file);
+        }
+        $locale_strings = _locale_import_one_string('mem-report');
+      }
+    }
+  }
+
+  require_once DRUPAL_ROOT . '/includes/theme.inc';
+  // Transform arguments before inserting them
+  foreach ($args as $key => $value) {
+    switch ($key[0]) {
+      // Escaped only
+      case '@':
+        $args[$key] = check_plain($value);
+        break;
+      // Escaped and placeholder
+      case '%':
+      default:
+        $args[$key] = '<em>' . check_plain($value) . '</em>';
+        break;
+      // Pass-through
+      case '!':
+    }
+  }
+  return strtr((!empty($locale_strings[$options['context']][$string]) ? $locale_strings[$options['context']][$string] : $string), $args);
+}
+
+/**
+ * Checks an installation profile's requirements.
+ *
+ * @param $profile
+ *   Name of installation profile to check.
+ * @return
+ *   Array of the installation profile's requirements.
+ */
+function drupal_check_profile($profile) {
+  include_once DRUPAL_ROOT . '/includes/file.inc';
+
+  $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile";
+
+  if (!isset($profile) || !file_exists($profile_file)) {
+    throw new Exception(install_no_profile_error());
+  }
+
+  $info = install_profile_info($profile);
+
+  // Collect requirement testing results.
+  $requirements = array();
+  foreach ($info['dependencies'] as $module) {
+    module_load_install($module);
+    $function = $module . '_requirements';
+    if (function_exists($function)) {
+      $requirements = array_merge($requirements, $function('install'));
+    }
+  }
+  return $requirements;
+}
+
+/**
+ * Extracts the highest severity from the requirements array.
+ *
+ * @param $requirements
+ *   An array of requirements, in the same format as is returned by
+ *   hook_requirements().
+ *
+ * @return
+ *   The highest severity in the array.
+ */
+function drupal_requirements_severity(&$requirements) {
+  $severity = REQUIREMENT_OK;
+  foreach ($requirements as $requirement) {
+    if (isset($requirement['severity'])) {
+      $severity = max($severity, $requirement['severity']);
+    }
+  }
+  return $severity;
+}
+
+/**
+ * Checks a module's requirements.
+ *
+ * @param $module
+ *   Machine name of module to check.
+ *
+ * @return
+ *   TRUE or FALSE, depending on whether the requirements are met.
+ */
+function drupal_check_module($module) {
+  module_load_install($module);
+  if (module_hook($module, 'requirements')) {
+    // Check requirements
+    $requirements = module_invoke($module, 'requirements', 'install');
+    if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) {
+      // Print any error messages
+      foreach ($requirements as $requirement) {
+        if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) {
+          $message = $requirement['description'];
+          if (isset($requirement['value']) && $requirement['value']) {
+            $message .= ' (' . t('Currently using !item !version', array('!item' => $requirement['title'], '!version' => $requirement['value'])) . ')';
+          }
+          drupal_set_message($message, 'error');
+        }
+      }
+      return FALSE;
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Retrieves information about an installation profile from its .info file.
+ *
+ * The information stored in a profile .info file is similar to that stored in
+ * a normal Drupal module .info file. For example:
+ * - name: The real name of the installation profile for display purposes.
+ * - description: A brief description of the profile.
+ * - dependencies: An array of shortnames of other modules that this install
+ *   profile requires.
+ *
+ * Additional, less commonly-used information that can appear in a profile.info
+ * file but not in a normal Drupal module .info file includes:
+ * - distribution_name: The name of the Drupal distribution that is being
+ *   installed, to be shown throughout the installation process. Defaults to
+ *   'Drupal'.
+ * - exclusive: If the install profile is intended to be the only eligible
+ *   choice in a distribution, setting exclusive = TRUE will auto-select it
+ *   during installation, and the install profile selection screen will be
+ *   skipped. If more than one profile is found where exclusive = TRUE then
+ *   this property will have no effect and the profile selection screen will
+ *   be shown as normal with all available profiles shown.
+ *
+ * Note that this function does an expensive file system scan to get info file
+ * information for dependencies. If you only need information from the info
+ * file itself, use system_get_info().
+ *
+ * Example of .info file:
+ * @code
+ *    name = Minimal
+ *    description = Start fresh, with only a few modules enabled.
+ *    dependencies[] = block
+ *    dependencies[] = dblog
+ * @endcode
+ *
+ * @param $profile
+ *   Name of profile.
+ * @param $locale
+ *   Name of locale used (if any).
+ *
+ * @return
+ *   The info array.
+ */
+function install_profile_info($profile, $locale = 'en') {
+  $cache = &drupal_static(__FUNCTION__, array());
+
+  if (!isset($cache[$profile])) {
+    // Set defaults for module info.
+    $defaults = array(
+      'dependencies' => array(),
+      'description' => '',
+      'distribution_name' => 'Drupal',
+      'version' => NULL,
+      'hidden' => FALSE,
+      'php' => DRUPAL_MINIMUM_PHP,
+    );
+    $info = drupal_parse_info_file("profiles/$profile/$profile.info") + $defaults;
+    $info['dependencies'] = array_unique(array_merge(
+      drupal_required_modules(),
+      $info['dependencies'],
+      ($locale != 'en' && !empty($locale) ? array('locale') : array()))
+    );
+
+    // drupal_required_modules() includes the current profile as a dependency.
+    // Since a module can't depend on itself we remove that element of the array.
+    array_shift($info['dependencies']);
+
+    $cache[$profile] = $info;
+  }
+  return $cache[$profile];
+}
+
+/**
+ * Ensures the environment for a Drupal database on a predefined connection.
+ *
+ * This will run tasks that check that Drupal can perform all of the functions
+ * on a database, that Drupal needs. Tasks include simple checks like CREATE
+ * TABLE to database specific functions like stored procedures and client
+ * encoding.
+ */
+function db_run_tasks($driver) {
+  db_installer_object($driver)->runTasks();
+  return TRUE;
+}
+
+/**
+ * Returns a database installer object.
+ *
+ * @param $driver
+ *   The name of the driver.
+ */
+function db_installer_object($driver) {
+  Database::loadDriverFile($driver, array('install.inc'));
+  $task_class = 'DatabaseTasks_' . $driver;
+  return new $task_class();
+}

+ 483 - 0
includes/iso.inc

@@ -0,0 +1,483 @@
+<?php
+
+/**
+ * @file
+ * Provides a list of countries and languages based on ISO standards.
+ */
+
+/**
+ * Get an array of all country code => country name pairs.
+ *
+ * Get an array of all country code => country name pairs as laid out
+ * in ISO 3166-1 alpha-2.
+ * Grabbed from location project (http://drupal.org/project/location).
+ * @return
+ *   An array of all country code => country name pairs.
+ */
+function _country_get_predefined_list() {
+  static $countries;
+
+  if (isset($countries)) {
+    return $countries;
+  }
+  $t = get_t();
+
+  $countries = array(
+    'AD' => $t('Andorra'),
+    'AE' => $t('United Arab Emirates'),
+    'AF' => $t('Afghanistan'),
+    'AG' => $t('Antigua and Barbuda'),
+    'AI' => $t('Anguilla'),
+    'AL' => $t('Albania'),
+    'AM' => $t('Armenia'),
+    'AN' => $t('Netherlands Antilles'),
+    'AO' => $t('Angola'),
+    'AQ' => $t('Antarctica'),
+    'AR' => $t('Argentina'),
+    'AS' => $t('American Samoa'),
+    'AT' => $t('Austria'),
+    'AU' => $t('Australia'),
+    'AW' => $t('Aruba'),
+    'AX' => $t('Aland Islands'),
+    'AZ' => $t('Azerbaijan'),
+    'BA' => $t('Bosnia and Herzegovina'),
+    'BB' => $t('Barbados'),
+    'BD' => $t('Bangladesh'),
+    'BE' => $t('Belgium'),
+    'BF' => $t('Burkina Faso'),
+    'BG' => $t('Bulgaria'),
+    'BH' => $t('Bahrain'),
+    'BI' => $t('Burundi'),
+    'BJ' => $t('Benin'),
+    'BL' => $t('Saint Barthélemy'),
+    'BM' => $t('Bermuda'),
+    'BN' => $t('Brunei'),
+    'BO' => $t('Bolivia'),
+    'BR' => $t('Brazil'),
+    'BS' => $t('Bahamas'),
+    'BT' => $t('Bhutan'),
+    'BV' => $t('Bouvet Island'),
+    'BW' => $t('Botswana'),
+    'BY' => $t('Belarus'),
+    'BZ' => $t('Belize'),
+    'CA' => $t('Canada'),
+    'CC' => $t('Cocos (Keeling) Islands'),
+    'CD' => $t('Congo (Kinshasa)'),
+    'CF' => $t('Central African Republic'),
+    'CG' => $t('Congo (Brazzaville)'),
+    'CH' => $t('Switzerland'),
+    'CI' => $t('Ivory Coast'),
+    'CK' => $t('Cook Islands'),
+    'CL' => $t('Chile'),
+    'CM' => $t('Cameroon'),
+    'CN' => $t('China'),
+    'CO' => $t('Colombia'),
+    'CR' => $t('Costa Rica'),
+    'CU' => $t('Cuba'),
+    'CW' => $t('Curaçao'),
+    'CV' => $t('Cape Verde'),
+    'CX' => $t('Christmas Island'),
+    'CY' => $t('Cyprus'),
+    'CZ' => $t('Czech Republic'),
+    'DE' => $t('Germany'),
+    'DJ' => $t('Djibouti'),
+    'DK' => $t('Denmark'),
+    'DM' => $t('Dominica'),
+    'DO' => $t('Dominican Republic'),
+    'DZ' => $t('Algeria'),
+    'EC' => $t('Ecuador'),
+    'EE' => $t('Estonia'),
+    'EG' => $t('Egypt'),
+    'EH' => $t('Western Sahara'),
+    'ER' => $t('Eritrea'),
+    'ES' => $t('Spain'),
+    'ET' => $t('Ethiopia'),
+    'FI' => $t('Finland'),
+    'FJ' => $t('Fiji'),
+    'FK' => $t('Falkland Islands'),
+    'FM' => $t('Micronesia'),
+    'FO' => $t('Faroe Islands'),
+    'FR' => $t('France'),
+    'GA' => $t('Gabon'),
+    'GB' => $t('United Kingdom'),
+    'GD' => $t('Grenada'),
+    'GE' => $t('Georgia'),
+    'GF' => $t('French Guiana'),
+    'GG' => $t('Guernsey'),
+    'GH' => $t('Ghana'),
+    'GI' => $t('Gibraltar'),
+    'GL' => $t('Greenland'),
+    'GM' => $t('Gambia'),
+    'GN' => $t('Guinea'),
+    'GP' => $t('Guadeloupe'),
+    'GQ' => $t('Equatorial Guinea'),
+    'GR' => $t('Greece'),
+    'GS' => $t('South Georgia and the South Sandwich Islands'),
+    'GT' => $t('Guatemala'),
+    'GU' => $t('Guam'),
+    'GW' => $t('Guinea-Bissau'),
+    'GY' => $t('Guyana'),
+    'HK' => $t('Hong Kong S.A.R., China'),
+    'HM' => $t('Heard Island and McDonald Islands'),
+    'HN' => $t('Honduras'),
+    'HR' => $t('Croatia'),
+    'HT' => $t('Haiti'),
+    'HU' => $t('Hungary'),
+    'ID' => $t('Indonesia'),
+    'IE' => $t('Ireland'),
+    'IL' => $t('Israel'),
+    'IM' => $t('Isle of Man'),
+    'IN' => $t('India'),
+    'IO' => $t('British Indian Ocean Territory'),
+    'IQ' => $t('Iraq'),
+    'IR' => $t('Iran'),
+    'IS' => $t('Iceland'),
+    'IT' => $t('Italy'),
+    'JE' => $t('Jersey'),
+    'JM' => $t('Jamaica'),
+    'JO' => $t('Jordan'),
+    'JP' => $t('Japan'),
+    'KE' => $t('Kenya'),
+    'KG' => $t('Kyrgyzstan'),
+    'KH' => $t('Cambodia'),
+    'KI' => $t('Kiribati'),
+    'KM' => $t('Comoros'),
+    'KN' => $t('Saint Kitts and Nevis'),
+    'KP' => $t('North Korea'),
+    'KR' => $t('South Korea'),
+    'KW' => $t('Kuwait'),
+    'KY' => $t('Cayman Islands'),
+    'KZ' => $t('Kazakhstan'),
+    'LA' => $t('Laos'),
+    'LB' => $t('Lebanon'),
+    'LC' => $t('Saint Lucia'),
+    'LI' => $t('Liechtenstein'),
+    'LK' => $t('Sri Lanka'),
+    'LR' => $t('Liberia'),
+    'LS' => $t('Lesotho'),
+    'LT' => $t('Lithuania'),
+    'LU' => $t('Luxembourg'),
+    'LV' => $t('Latvia'),
+    'LY' => $t('Libya'),
+    'MA' => $t('Morocco'),
+    'MC' => $t('Monaco'),
+    'MD' => $t('Moldova'),
+    'ME' => $t('Montenegro'),
+    'MF' => $t('Saint Martin (French part)'),
+    'MG' => $t('Madagascar'),
+    'MH' => $t('Marshall Islands'),
+    'MK' => $t('Macedonia'),
+    'ML' => $t('Mali'),
+    'MM' => $t('Myanmar'),
+    'MN' => $t('Mongolia'),
+    'MO' => $t('Macao S.A.R., China'),
+    'MP' => $t('Northern Mariana Islands'),
+    'MQ' => $t('Martinique'),
+    'MR' => $t('Mauritania'),
+    'MS' => $t('Montserrat'),
+    'MT' => $t('Malta'),
+    'MU' => $t('Mauritius'),
+    'MV' => $t('Maldives'),
+    'MW' => $t('Malawi'),
+    'MX' => $t('Mexico'),
+    'MY' => $t('Malaysia'),
+    'MZ' => $t('Mozambique'),
+    'NA' => $t('Namibia'),
+    'NC' => $t('New Caledonia'),
+    'NE' => $t('Niger'),
+    'NF' => $t('Norfolk Island'),
+    'NG' => $t('Nigeria'),
+    'NI' => $t('Nicaragua'),
+    'NL' => $t('Netherlands'),
+    'NO' => $t('Norway'),
+    'NP' => $t('Nepal'),
+    'NR' => $t('Nauru'),
+    'NU' => $t('Niue'),
+    'NZ' => $t('New Zealand'),
+    'OM' => $t('Oman'),
+    'PA' => $t('Panama'),
+    'PE' => $t('Peru'),
+    'PF' => $t('French Polynesia'),
+    'PG' => $t('Papua New Guinea'),
+    'PH' => $t('Philippines'),
+    'PK' => $t('Pakistan'),
+    'PL' => $t('Poland'),
+    'PM' => $t('Saint Pierre and Miquelon'),
+    'PN' => $t('Pitcairn'),
+    'PR' => $t('Puerto Rico'),
+    'PS' => $t('Palestinian Territory'),
+    'PT' => $t('Portugal'),
+    'PW' => $t('Palau'),
+    'PY' => $t('Paraguay'),
+    'QA' => $t('Qatar'),
+    'RE' => $t('Reunion'),
+    'RO' => $t('Romania'),
+    'RS' => $t('Serbia'),
+    'RU' => $t('Russia'),
+    'RW' => $t('Rwanda'),
+    'SA' => $t('Saudi Arabia'),
+    'SB' => $t('Solomon Islands'),
+    'SC' => $t('Seychelles'),
+    'SD' => $t('Sudan'),
+    'SE' => $t('Sweden'),
+    'SG' => $t('Singapore'),
+    'SH' => $t('Saint Helena'),
+    'SI' => $t('Slovenia'),
+    'SJ' => $t('Svalbard and Jan Mayen'),
+    'SK' => $t('Slovakia'),
+    'SL' => $t('Sierra Leone'),
+    'SM' => $t('San Marino'),
+    'SN' => $t('Senegal'),
+    'SO' => $t('Somalia'),
+    'SR' => $t('Suriname'),
+    'ST' => $t('Sao Tome and Principe'),
+    'SV' => $t('El Salvador'),
+    'SY' => $t('Syria'),
+    'SZ' => $t('Swaziland'),
+    'TC' => $t('Turks and Caicos Islands'),
+    'TD' => $t('Chad'),
+    'TF' => $t('French Southern Territories'),
+    'TG' => $t('Togo'),
+    'TH' => $t('Thailand'),
+    'TJ' => $t('Tajikistan'),
+    'TK' => $t('Tokelau'),
+    'TL' => $t('Timor-Leste'),
+    'TM' => $t('Turkmenistan'),
+    'TN' => $t('Tunisia'),
+    'TO' => $t('Tonga'),
+    'TR' => $t('Turkey'),
+    'TT' => $t('Trinidad and Tobago'),
+    'TV' => $t('Tuvalu'),
+    'TW' => $t('Taiwan'),
+    'TZ' => $t('Tanzania'),
+    'UA' => $t('Ukraine'),
+    'UG' => $t('Uganda'),
+    'UM' => $t('United States Minor Outlying Islands'),
+    'US' => $t('United States'),
+    'UY' => $t('Uruguay'),
+    'UZ' => $t('Uzbekistan'),
+    'VA' => $t('Vatican'),
+    'VC' => $t('Saint Vincent and the Grenadines'),
+    'VE' => $t('Venezuela'),
+    'VG' => $t('British Virgin Islands'),
+    'VI' => $t('U.S. Virgin Islands'),
+    'VN' => $t('Vietnam'),
+    'VU' => $t('Vanuatu'),
+    'WF' => $t('Wallis and Futuna'),
+    'WS' => $t('Samoa'),
+    'YE' => $t('Yemen'),
+    'YT' => $t('Mayotte'),
+    'ZA' => $t('South Africa'),
+    'ZM' => $t('Zambia'),
+    'ZW' => $t('Zimbabwe'),
+  );
+
+  // Sort the list.
+  natcasesort($countries);
+
+  return $countries;
+}
+
+/**
+ * @ingroup locale-api-predefined List of predefined languages
+ * @{
+ */
+
+/**
+ * Some of the common languages with their English and native names
+ *
+ * Based on ISO 639 and http://people.w3.org/rishida/names/languages.html
+ */
+function _locale_get_predefined_list() {
+  return array(
+    'aa' => array('Afar'),
+    'ab' => array('Abkhazian', 'аҧсуа бызшәа'),
+    'ae' => array('Avestan'),
+    'af' => array('Afrikaans'),
+    'ak' => array('Akan'),
+    'am' => array('Amharic', 'አማርኛ'),
+    'ar' => array('Arabic', /* Left-to-right marker "‭" */ 'العربية', LANGUAGE_RTL),
+    'as' => array('Assamese'),
+    'ast' => array('Asturian'),
+    'av' => array('Avar'),
+    'ay' => array('Aymara'),
+    'az' => array('Azerbaijani', 'azərbaycan'),
+    'ba' => array('Bashkir'),
+    'be' => array('Belarusian', 'Беларуская'),
+    'bg' => array('Bulgarian', 'Български'),
+    'bh' => array('Bihari'),
+    'bi' => array('Bislama'),
+    'bm' => array('Bambara', 'Bamanankan'),
+    'bn' => array('Bengali'),
+    'bo' => array('Tibetan'),
+    'br' => array('Breton'),
+    'bs' => array('Bosnian', 'Bosanski'),
+    'ca' => array('Catalan', 'Català'),
+    'ce' => array('Chechen'),
+    'ch' => array('Chamorro'),
+    'co' => array('Corsican'),
+    'cr' => array('Cree'),
+    'cs' => array('Czech', 'Čeština'),
+    'cu' => array('Old Slavonic'),
+    'cv' => array('Chuvash'),
+    'cy' => array('Welsh', 'Cymraeg'),
+    'da' => array('Danish', 'Dansk'),
+    'de' => array('German', 'Deutsch'),
+    'dv' => array('Maldivian'),
+    'dz' => array('Bhutani'),
+    'ee' => array('Ewe', 'Ɛʋɛ'),
+    'el' => array('Greek', 'Ελληνικά'),
+    'en' => array('English'),
+    'en-gb' => array('English, British'),
+    'eo' => array('Esperanto'),
+    'es' => array('Spanish', 'Español'),
+    'et' => array('Estonian', 'Eesti'),
+    'eu' => array('Basque', 'Euskera'),
+    'fa' => array('Persian', /* Left-to-right marker "‭" */ 'فارسی', LANGUAGE_RTL),
+    'ff' => array('Fulah', 'Fulfulde'),
+    'fi' => array('Finnish', 'Suomi'),
+    'fil' => array('Filipino'),
+    'fj' => array('Fiji'),
+    'fo' => array('Faeroese'),
+    'fr' => array('French', 'Français'),
+    'fy' => array('Frisian', 'Frysk'),
+    'ga' => array('Irish', 'Gaeilge'),
+    'gd' => array('Scots Gaelic'),
+    'gl' => array('Galician', 'Galego'),
+    'gn' => array('Guarani'),
+    'gsw-berne' => array('Swiss German'),
+    'gu' => array('Gujarati'),
+    'gv' => array('Manx'),
+    'ha' => array('Hausa'),
+    'he' => array('Hebrew', /* Left-to-right marker "‭" */ 'עברית', LANGUAGE_RTL),
+    'hi' => array('Hindi', 'हिन्दी'),
+    'ho' => array('Hiri Motu'),
+    'hr' => array('Croatian', 'Hrvatski'),
+    'ht' => array('Haitian Creole'),
+    'hu' => array('Hungarian', 'Magyar'),
+    'hy' => array('Armenian', 'Հայերեն'),
+    'hz' => array('Herero'),
+    'ia' => array('Interlingua'),
+    'id' => array('Indonesian', 'Bahasa Indonesia'),
+    'ie' => array('Interlingue'),
+    'ig' => array('Igbo'),
+    'ik' => array('Inupiak'),
+    'is' => array('Icelandic', 'Íslenska'),
+    'it' => array('Italian', 'Italiano'),
+    'iu' => array('Inuktitut'),
+    'ja' => array('Japanese', '日本語'),
+    'jv' => array('Javanese'),
+    'ka' => array('Georgian'),
+    'kg' => array('Kongo'),
+    'ki' => array('Kikuyu'),
+    'kj' => array('Kwanyama'),
+    'kk' => array('Kazakh', 'Қазақ'),
+    'kl' => array('Greenlandic'),
+    'km' => array('Cambodian'),
+    'kn' => array('Kannada', 'ಕನ್ನಡ'),
+    'ko' => array('Korean', '한국어'),
+    'kr' => array('Kanuri'),
+    'ks' => array('Kashmiri'),
+    'ku' => array('Kurdish', 'Kurdî'),
+    'kv' => array('Komi'),
+    'kw' => array('Cornish'),
+    'ky' => array('Kyrgyz', 'Кыргызча'),
+    'la' => array('Latin', 'Latina'),
+    'lb' => array('Luxembourgish'),
+    'lg' => array('Luganda'),
+    'ln' => array('Lingala'),
+    'lo' => array('Laothian'),
+    'lt' => array('Lithuanian', 'Lietuvių'),
+    'lv' => array('Latvian', 'Latviešu'),
+    'mg' => array('Malagasy'),
+    'mh' => array('Marshallese'),
+    'mi' => array('Māori'),
+    'mk' => array('Macedonian', 'Македонски'),
+    'ml' => array('Malayalam', 'മലയാളം'),
+    'mn' => array('Mongolian'),
+    'mo' => array('Moldavian'),
+    'mr' => array('Marathi'),
+    'ms' => array('Malay', 'Bahasa Melayu'),
+    'mt' => array('Maltese', 'Malti'),
+    'my' => array('Burmese'),
+    'na' => array('Nauru'),
+    'nd' => array('North Ndebele'),
+    'ne' => array('Nepali'),
+    'ng' => array('Ndonga'),
+    'nl' => array('Dutch', 'Nederlands'),
+    'nb' => array('Norwegian Bokmål', 'Bokmål'),
+    'nn' => array('Norwegian Nynorsk', 'Nynorsk'),
+    'nr' => array('South Ndebele'),
+    'nv' => array('Navajo'),
+    'ny' => array('Chichewa'),
+    'oc' => array('Occitan'),
+    'om' => array('Oromo'),
+    'or' => array('Oriya'),
+    'os' => array('Ossetian'),
+    'pa' => array('Punjabi'),
+    'pi' => array('Pali'),
+    'pl' => array('Polish', 'Polski'),
+    'ps' => array('Pashto', /* Left-to-right marker "‭" */ 'پښتو', LANGUAGE_RTL),
+    'pt' => array('Portuguese, International'),
+    'pt-pt' => array('Portuguese, Portugal', 'Português'),
+    'pt-br' => array('Portuguese, Brazil', 'Português'),
+    'qu' => array('Quechua'),
+    'rm' => array('Rhaeto-Romance'),
+    'rn' => array('Kirundi'),
+    'ro' => array('Romanian', 'Română'),
+    'ru' => array('Russian', 'Русский'),
+    'rw' => array('Kinyarwanda'),
+    'sa' => array('Sanskrit'),
+    'sc' => array('Sardinian'),
+    'sco' => array('Scots'),
+    'sd' => array('Sindhi'),
+    'se' => array('Northern Sami'),
+    'sg' => array('Sango'),
+    'sh' => array('Serbo-Croatian'),
+    'si' => array('Sinhala', 'සිංහල'),
+    'sk' => array('Slovak', 'Slovenčina'),
+    'sl' => array('Slovenian', 'Slovenščina'),
+    'sm' => array('Samoan'),
+    'sn' => array('Shona'),
+    'so' => array('Somali'),
+    'sq' => array('Albanian', 'Shqip'),
+    'sr' => array('Serbian', 'Српски'),
+    'ss' => array('Siswati'),
+    'st' => array('Sesotho'),
+    'su' => array('Sudanese'),
+    'sv' => array('Swedish', 'Svenska'),
+    'sw' => array('Swahili', 'Kiswahili'),
+    'ta' => array('Tamil', 'தமிழ்'),
+    'te' => array('Telugu', 'తెలుగు'),
+    'tg' => array('Tajik'),
+    'th' => array('Thai', 'ภาษาไทย'),
+    'ti' => array('Tigrinya'),
+    'tk' => array('Turkmen'),
+    'tl' => array('Tagalog'),
+    'tn' => array('Setswana'),
+    'to' => array('Tonga'),
+    'tr' => array('Turkish', 'Türkçe'),
+    'ts' => array('Tsonga'),
+    'tt' => array('Tatar', 'Tatarça'),
+    'tw' => array('Twi'),
+    'ty' => array('Tahitian'),
+    'ug' => array('Uyghur'),
+    'uk' => array('Ukrainian', 'Українська'),
+    'ur' => array('Urdu', /* Left-to-right marker "‭" */ 'اردو', LANGUAGE_RTL),
+    'uz' => array('Uzbek', "o'zbek"),
+    've' => array('Venda'),
+    'vi' => array('Vietnamese', 'Tiếng Việt'),
+    'wo' => array('Wolof'),
+    'xh' => array('Xhosa', 'isiXhosa'),
+    'xx-lolspeak' => array('Lolspeak'),
+    'yi' => array('Yiddish'),
+    'yo' => array('Yoruba', 'Yorùbá'),
+    'za' => array('Zhuang'),
+    'zh-hans' => array('Chinese, Simplified', '简体中文'),
+    'zh-hant' => array('Chinese, Traditional', '繁體中文'),
+    'zu' => array('Zulu', 'isiZulu'),
+  );
+}
+/**
+ * @} End of "locale-api-languages-predefined"
+ */

+ 102 - 0
includes/json-encode.inc

@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @file
+ * Provides a helper to properly encode HTML-safe JSON prior to PHP 5.3.0.
+ */
+
+/**
+ * Encodes a PHP variable to HTML-safe JSON for PHP versions below 5.3.0.
+ *
+ * @see drupal_json_encode()
+ */
+function drupal_json_encode_helper($var) {
+  switch (gettype($var)) {
+    case 'boolean':
+      return $var ? 'true' : 'false'; // Lowercase necessary!
+
+    case 'integer':
+    case 'double':
+      return $var;
+
+    case 'resource':
+    case 'string':
+      // Always use Unicode escape sequences (\u0022) over JSON escape
+      // sequences (\") to prevent browsers interpreting these as
+      // special characters.
+      $replace_pairs = array(
+        // ", \ and U+0000 - U+001F must be escaped according to RFC 4627.
+        '\\' => '\u005C',
+        '"' => '\u0022',
+        "\x00" => '\u0000',
+        "\x01" => '\u0001',
+        "\x02" => '\u0002',
+        "\x03" => '\u0003',
+        "\x04" => '\u0004',
+        "\x05" => '\u0005',
+        "\x06" => '\u0006',
+        "\x07" => '\u0007',
+        "\x08" => '\u0008',
+        "\x09" => '\u0009',
+        "\x0a" => '\u000A',
+        "\x0b" => '\u000B',
+        "\x0c" => '\u000C',
+        "\x0d" => '\u000D',
+        "\x0e" => '\u000E',
+        "\x0f" => '\u000F',
+        "\x10" => '\u0010',
+        "\x11" => '\u0011',
+        "\x12" => '\u0012',
+        "\x13" => '\u0013',
+        "\x14" => '\u0014',
+        "\x15" => '\u0015',
+        "\x16" => '\u0016',
+        "\x17" => '\u0017',
+        "\x18" => '\u0018',
+        "\x19" => '\u0019',
+        "\x1a" => '\u001A',
+        "\x1b" => '\u001B',
+        "\x1c" => '\u001C',
+        "\x1d" => '\u001D',
+        "\x1e" => '\u001E',
+        "\x1f" => '\u001F',
+        // Prevent browsers from interpreting these as as special.
+        "'" => '\u0027',
+        '<' => '\u003C',
+        '>' => '\u003E',
+        '&' => '\u0026',
+        // Prevent browsers from interpreting the solidus as special and
+        // non-compliant JSON parsers from interpreting // as a comment.
+        '/' => '\u002F',
+        // While these are allowed unescaped according to ECMA-262, section
+        // 15.12.2, they cause problems in some JSON parsers.
+        "\xe2\x80\xa8" => '\u2028', // U+2028, Line Separator.
+        "\xe2\x80\xa9" => '\u2029', // U+2029, Paragraph Separator.
+      );
+
+      return '"' . strtr($var, $replace_pairs) . '"';
+
+    case 'array':
+      // Arrays in JSON can't be associative. If the array is empty or if it
+      // has sequential whole number keys starting with 0, it's not associative
+      // so we can go ahead and convert it as an array.
+      if (empty($var) || array_keys($var) === range(0, sizeof($var) - 1)) {
+        $output = array();
+        foreach ($var as $v) {
+          $output[] = drupal_json_encode_helper($v);
+        }
+        return '[ ' . implode(', ', $output) . ' ]';
+      }
+      // Otherwise, fall through to convert the array as an object.
+
+    case 'object':
+      $output = array();
+      foreach ($var as $k => $v) {
+        $output[] = drupal_json_encode_helper(strval($k)) . ':' . drupal_json_encode_helper($v);
+      }
+      return '{' . implode(', ', $output) . '}';
+
+    default:
+      return 'null';
+  }
+}

+ 581 - 0
includes/language.inc

@@ -0,0 +1,581 @@
+<?php
+
+/**
+ * @file
+ * Language Negotiation API.
+ *
+ * @see http://drupal.org/node/1497272
+ */
+
+/**
+ * No language negotiation. The default language is used.
+ */
+define('LANGUAGE_NEGOTIATION_DEFAULT', 'language-default');
+
+/**
+ * @defgroup language_negotiation Language Negotiation API functionality
+ * @{
+ * Functions to customize the language types and the negotiation process.
+ *
+ * The language negotiation API is based on two major concepts:
+ * - Language types: types of translatable data (the types of data that a user
+ *   can view or request).
+ * - Language negotiation providers: functions for determining which language to
+ *   use to present a particular piece of data to the user.
+ * Both language types and language negotiation providers are customizable.
+ *
+ * Drupal defines three built-in language types:
+ * - Interface language: The page's main language, used to present translated
+ *   user interface elements such as titles, labels, help text, and messages.
+ * - Content language: The language used to present content that is available
+ *   in more than one language (see
+ *   @link field_language Field Language API @endlink for details).
+ * - URL language: The language associated with URLs. When generating a URL,
+ *   this value will be used by url() as a default if no explicit preference is
+ *   provided.
+ * Modules can define additional language types through
+ * hook_language_types_info(), and alter existing language type definitions
+ * through hook_language_types_info_alter().
+ *
+ * Language types may be configurable or fixed. The language negotiation
+ * providers associated with a configurable language type can be explicitly
+ * set through the user interface. A fixed language type has predetermined
+ * (module-defined) language negotiation settings and, thus, does not appear in
+ * the configuration page. Here is a code snippet that makes the content
+ * language (which by default inherits the interface language's values)
+ * configurable:
+ * @code
+ * function mymodule_language_types_info_alter(&$language_types) {
+ *   unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']);
+ * }
+ * @endcode
+ *
+ * Every language type can have a different set of language negotiation
+ * providers assigned to it. Different language types often share the same
+ * language negotiation settings, but they can have independent settings if
+ * needed. If two language types are configured the same way, their language
+ * switcher configuration will be functionally identical and the same settings
+ * will act on both language types.
+ *
+ * Drupal defines the following built-in language negotiation providers:
+ * - URL: Determine the language from the URL (path prefix or domain).
+ * - Session: Determine the language from a request/session parameter.
+ * - User: Follow the user's language preference.
+ * - Browser: Determine the language from the browser's language settings.
+ * - Default language: Use the default site language.
+ * Language negotiation providers are simple callback functions that implement a
+ * particular logic to return a language code. For instance, the URL provider
+ * searches for a valid path prefix or domain name in the current request URL.
+ * If a language negotiation provider does not return a valid language code, the
+ * next provider associated to the language type (based on provider weight) is
+ * invoked.
+ *
+ * Modules can define additional language negotiation providers through
+ * hook_language_negotiation_info(), and alter existing providers through
+ * hook_language_negotiation_info_alter(). Here is an example snippet that lets
+ * path prefixes be ignored for administrative paths:
+ * @code
+ * function mymodule_language_negotiation_info_alter(&$negotiation_info) {
+ *   // Replace the core function with our own function.
+ *   module_load_include('language', 'inc', 'language.negotiation');
+ *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['callbacks']['negotiation'] = 'mymodule_from_url';
+ *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['file'] = drupal_get_path('module', 'mymodule') . '/mymodule.module';
+ * }
+ *
+ * function mymodule_from_url($languages) {
+ *   // Use the core URL language negotiation provider to get a valid language
+ *   // code.
+ *   module_load_include('language', 'inc', 'language.negotiation');
+ *   $langcode = language_from_url($languages);
+ *
+ *   // If we are on an administrative path, override with the default language.
+ *   if (isset($_GET['q']) && strtok($_GET['q'], '/') == 'admin') {
+ *     return language_default()->langcode;
+ *   }
+ *   return $langcode;
+ * }
+ * ?>
+ * @endcode
+ *
+ * For more information, see
+ * @link http://drupal.org/node/1497272 Language Negotiation API @endlink
+ */
+
+/**
+ * Returns all the defined language types.
+ *
+ * @return
+ *   An array of language type names. The name will be used as the global
+ *   variable name the language value will be stored in.
+ */
+function language_types_info() {
+  $language_types = &drupal_static(__FUNCTION__);
+
+  if (!isset($language_types)) {
+    $language_types = module_invoke_all('language_types_info');
+    // Let other modules alter the list of language types.
+    drupal_alter('language_types_info', $language_types);
+  }
+
+  return $language_types;
+}
+
+/**
+ * Returns only the configurable language types.
+ *
+ * A language type maybe configurable or fixed. A fixed language type is a type
+ * whose language negotiation providers are module-defined and not altered
+ * through the user interface.
+ *
+ * @param $stored
+ *   Optional. By default retrieves values from the 'language_types' variable to
+ *   avoid unnecessary hook invocations.
+ *   If set to FALSE retrieves values from the actual language type definitions.
+ *   This allows to react to alterations performed on the definitions by modules
+ *   installed after the 'language_types' variable is set.
+ *
+ * @return
+ *   An array of language type names.
+ */
+function language_types_configurable($stored = TRUE) {
+  $configurable = &drupal_static(__FUNCTION__);
+
+  if ($stored && !isset($configurable)) {
+    $types = variable_get('language_types', drupal_language_types());
+    $configurable = array_keys(array_filter($types));
+  }
+
+  if (!$stored) {
+    $result = array();
+    foreach (language_types_info() as $type => $info) {
+      if (!isset($info['fixed'])) {
+        $result[] = $type;
+      }
+    }
+    return $result;
+  }
+
+  return $configurable;
+}
+
+/**
+ * Disables the given language types.
+ *
+ * @param $types
+ *   An array of language types.
+ */
+function language_types_disable($types) {
+  $enabled_types = variable_get('language_types', drupal_language_types());
+
+  foreach ($types as $type) {
+    unset($enabled_types[$type]);
+  }
+
+  variable_set('language_types', $enabled_types);
+}
+
+/**
+ * Updates the language type configuration.
+ */
+function language_types_set() {
+  // Ensure that we are getting the defined language negotiation information. An
+  // invocation of module_enable() or module_disable() could outdate the cached
+  // information.
+  drupal_static_reset('language_types_info');
+  drupal_static_reset('language_negotiation_info');
+
+  // Determine which language types are configurable and which not by checking
+  // whether the 'fixed' key is defined. Non-configurable (fixed) language types
+  // have their language negotiation settings stored there.
+  $defined_providers = language_negotiation_info();
+  foreach (language_types_info() as $type => $info) {
+    if (isset($info['fixed'])) {
+      $language_types[$type] = FALSE;
+      $negotiation = array();
+      foreach ($info['fixed'] as $weight => $id) {
+        if (isset($defined_providers[$id])) {
+          $negotiation[$id] = $weight;
+        }
+      }
+      language_negotiation_set($type, $negotiation);
+    }
+    else {
+      $language_types[$type] = TRUE;
+    }
+  }
+
+  // Save language types.
+  variable_set('language_types', $language_types);
+
+  // Ensure that subsequent calls of language_types_configurable() return the
+  // updated language type information.
+  drupal_static_reset('language_types_configurable');
+}
+
+/**
+ * Checks whether a language negotiation provider is enabled for a language type.
+ *
+ * This has two possible behaviors:
+ *  - If $provider_id is given return its ID if enabled, FALSE otherwise.
+ *  - If no ID is passed the first enabled language negotiation provider is
+ *    returned.
+ *
+ * @param $type
+ *   The language negotiation provider type.
+ * @param $provider_id
+ *   The language negotiation provider ID.
+ *
+ * @return
+ *   The provider ID if it is enabled, FALSE otherwise.
+ */
+function language_negotiation_get($type, $provider_id = NULL) {
+  $negotiation = variable_get("language_negotiation_$type", array());
+
+  if (empty($negotiation)) {
+    return empty($provider_id) ? LANGUAGE_NEGOTIATION_DEFAULT : FALSE;
+  }
+
+  if (empty($provider_id)) {
+    return key($negotiation);
+  }
+
+  if (isset($negotiation[$provider_id])) {
+    return $provider_id;
+  }
+
+  return FALSE;
+}
+
+/**
+ * Checks if the language negotiation provider is enabled for any language type.
+ *
+ * @param $provider_id
+ *   The language negotiation provider ID.
+ *
+ * @return
+ *   TRUE if there is at least one language type for which the given language
+ *   provider is enabled, FALSE otherwise.
+ */
+function language_negotiation_get_any($provider_id) {
+  foreach (language_types_configurable() as $type) {
+    if (language_negotiation_get($type, $provider_id)) {
+      return TRUE;
+    }
+  }
+
+  return FALSE;
+}
+
+/**
+ * Returns the language switch links for the given language.
+ *
+ * @param $type
+ *   The language negotiation type.
+ * @param $path
+ *   The internal path the switch links will be relative to.
+ *
+ * @return
+ *   A keyed array of links ready to be themed.
+ */
+function language_negotiation_get_switch_links($type, $path) {
+  $links = FALSE;
+  $negotiation = variable_get("language_negotiation_$type", array());
+
+  // Only get the languages if we have more than one.
+  if (count(language_list()) >= 2) {
+    $language = language_initialize($type);
+  }
+
+  foreach ($negotiation as $id => $provider) {
+    if (isset($provider['callbacks']['switcher'])) {
+      if (isset($provider['file'])) {
+        require_once DRUPAL_ROOT . '/' . $provider['file'];
+      }
+
+      $callback = $provider['callbacks']['switcher'];
+      $result = $callback($type, $path);
+
+      // Add support for WCAG 2.0's Language of Parts to add language identifiers.
+      // http://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html
+      foreach ($result as $langcode => $link) {
+        $result[$langcode]['attributes']['lang'] = $langcode;
+      }
+
+      if (!empty($result)) {
+        // Allow modules to provide translations for specific links.
+        drupal_alter('language_switch_links', $result, $type, $path);
+        $links = (object) array('links' => $result, 'provider' => $id);
+        break;
+      }
+    }
+  }
+
+  return $links;
+}
+
+/**
+ * Removes any unused language negotation providers from the configuration.
+ */
+function language_negotiation_purge() {
+  // Ensure that we are getting the defined language negotiation information. An
+  // invocation of module_enable() or module_disable() could outdate the cached
+  // information.
+  drupal_static_reset('language_negotiation_info');
+  drupal_static_reset('language_types_info');
+
+  $defined_providers = language_negotiation_info();
+  foreach (language_types_info() as $type => $type_info) {
+    $weight = 0;
+    $negotiation = array();
+    foreach (variable_get("language_negotiation_$type", array()) as $id => $provider) {
+      if (isset($defined_providers[$id])) {
+        $negotiation[$id] = $weight++;
+      }
+    }
+    language_negotiation_set($type, $negotiation);
+  }
+}
+
+/**
+ * Saves a list of language negotiation providers.
+ *
+ * @param $type
+ *   The language negotiation type.
+ * @param $language_providers
+ *   An array of language negotiation provider weights keyed by provider ID.
+ *   @see language_provider_weight()
+ */
+function language_negotiation_set($type, $language_providers) {
+  // Save only the necessary fields.
+  $provider_fields = array('callbacks', 'file', 'cache');
+
+  $negotiation = array();
+  $providers_weight = array();
+  $defined_providers = language_negotiation_info();
+  $default_types = language_types_configurable(FALSE);
+
+  // Initialize the providers weight list.
+  foreach ($language_providers as $id => $provider) {
+    $providers_weight[$id] = language_provider_weight($provider);
+  }
+
+  // Order providers list by weight.
+  asort($providers_weight);
+
+  foreach ($providers_weight as $id => $weight) {
+    if (isset($defined_providers[$id])) {
+      $provider = $defined_providers[$id];
+      // If the provider does not express any preference about types, make it
+      // available for any configurable type.
+      $types = array_flip(isset($provider['types']) ? $provider['types'] : $default_types);
+      // Check whether the provider is defined and has the right type.
+      if (isset($types[$type])) {
+        $provider_data = array();
+        foreach ($provider_fields as $field) {
+          if (isset($provider[$field])) {
+            $provider_data[$field] = $provider[$field];
+          }
+        }
+        $negotiation[$id] = $provider_data;
+      }
+    }
+  }
+
+  variable_set("language_negotiation_$type", $negotiation);
+}
+
+/**
+ * Returns all the defined language negotiation providers.
+ *
+ * @return
+ *   An array of language negotiation providers.
+ */
+function language_negotiation_info() {
+  $language_providers = &drupal_static(__FUNCTION__);
+
+  if (!isset($language_providers)) {
+    // Collect all the module-defined language negotiation providers.
+    $language_providers = module_invoke_all('language_negotiation_info');
+
+    // Add the default language negotiation provider.
+    $language_providers[LANGUAGE_NEGOTIATION_DEFAULT] = array(
+      'callbacks' => array('language' => 'language_from_default'),
+      'weight' => 10,
+      'name' => t('Default'),
+      'description' => t('Use the default site language (@language_name).', array('@language_name' => language_default()->native)),
+    );
+
+    // Let other modules alter the list of language negotiation providers.
+    drupal_alter('language_negotiation_info', $language_providers);
+  }
+
+  return $language_providers;
+}
+
+/**
+ * Helper function used to cache the language negotiation providers results.
+ *
+ * @param $provider_id
+ *   The language negotiation provider's identifier.
+ * @param $provider
+ *   (optional) An associative array of information about the provider to be
+ *   invoked (see hook_language_negotiation_info() for details). If not passed
+ *   in, it will be loaded through language_negotiation_info().
+ *
+ * @return
+ *   A language object representing the language chosen by the provider.
+ */
+function language_provider_invoke($provider_id, $provider = NULL) {
+  $results = &drupal_static(__FUNCTION__);
+
+  if (!isset($results[$provider_id])) {
+    global $user;
+
+    // Get languages grouped by status and select only the enabled ones.
+    $languages = language_list('enabled');
+    $languages = $languages[1];
+
+    if (!isset($provider)) {
+      $providers = language_negotiation_info();
+      $provider = $providers[$provider_id];
+    }
+
+    if (isset($provider['file'])) {
+      require_once DRUPAL_ROOT . '/' . $provider['file'];
+    }
+
+    // If the language negotiation provider has no cache preference or this is
+    // satisfied we can execute the callback.
+    $cache = !isset($provider['cache']) || $user->uid || $provider['cache'] == variable_get('cache', 0);
+    $callback = isset($provider['callbacks']['language']) ? $provider['callbacks']['language'] : FALSE;
+    $langcode = $cache && function_exists($callback) ? $callback($languages) : FALSE;
+    $results[$provider_id] = isset($languages[$langcode]) ? $languages[$langcode] : FALSE;
+  }
+
+  // Since objects are resources, we need to return a clone to prevent the
+  // language negotiation provider cache from being unintentionally altered. The
+  // same providers might be used with different language types based on
+  // configuration.
+  return !empty($results[$provider_id]) ? clone($results[$provider_id]) : $results[$provider_id];
+}
+
+/**
+ * Returns the passed language negotiation provider weight or a default value.
+ *
+ * @param $provider
+ *   A language negotiation provider data structure.
+ *
+ * @return
+ *   A numeric weight.
+ */
+function language_provider_weight($provider) {
+  $default = is_numeric($provider) ? $provider : 0;
+  return isset($provider['weight']) && is_numeric($provider['weight']) ? $provider['weight'] : $default;
+}
+
+/**
+ * Chooses a language based on language negotiation provider settings.
+ *
+ * @param $type
+ *   The language type key to find the language for.
+ *
+ * @return
+ *   The negotiated language object.
+ */
+function language_initialize($type) {
+  // Execute the language negotiation providers in the order they were set up and return the
+  // first valid language found.
+  $negotiation = variable_get("language_negotiation_$type", array());
+
+  foreach ($negotiation as $provider_id => $provider) {
+    $language = language_provider_invoke($provider_id, $provider);
+    if ($language) {
+      $language->provider = $provider_id;
+      return $language;
+    }
+  }
+
+  // If no other language was found use the default one.
+  $language = language_default();
+  $language->provider = LANGUAGE_NEGOTIATION_DEFAULT;
+  return $language;
+}
+
+/**
+ * Returns the default language negotiation provider.
+ *
+ * @return
+ *   The default language code.
+ */
+function language_from_default() {
+  return language_default()->language;
+}
+
+/**
+ * Splits the given path into prefix and actual path.
+ *
+ * Parse the given path and return the language object identified by the prefix
+ * and the actual path.
+ *
+ * @param $path
+ *   The path to split.
+ * @param $languages
+ *   An array of valid languages.
+ *
+ * @return
+ *   An array composed of:
+ *    - A language object corresponding to the identified prefix on success,
+ *      FALSE otherwise.
+ *    - The path without the prefix on success, the given path otherwise.
+ */
+function language_url_split_prefix($path, $languages) {
+  $args = empty($path) ? array() : explode('/', $path);
+  $prefix = array_shift($args);
+
+  // Search prefix within enabled languages.
+  foreach ($languages as $language) {
+    if (!empty($language->prefix) && $language->prefix == $prefix) {
+      // Rebuild $path with the language removed.
+      return array($language, implode('/', $args));
+    }
+  }
+
+  return array(FALSE, $path);
+}
+
+/**
+ * Returns the possible fallback languages ordered by language weight.
+ *
+ * @param
+ *   (optional) The language type. Defaults to LANGUAGE_TYPE_CONTENT.
+ *
+ * @return
+ *   An array of language codes.
+ */
+function language_fallback_get_candidates($type = LANGUAGE_TYPE_CONTENT) {
+  $fallback_candidates = &drupal_static(__FUNCTION__);
+
+  if (!isset($fallback_candidates)) {
+    $fallback_candidates = array();
+
+    // Get languages ordered by weight.
+    // Use array keys to avoid duplicated entries.
+    foreach (language_list('weight') as $languages) {
+      foreach ($languages as $language) {
+        $fallback_candidates[$language->language] = NULL;
+      }
+    }
+
+    $fallback_candidates = array_keys($fallback_candidates);
+    $fallback_candidates[] = LANGUAGE_NONE;
+
+    // Let other modules hook in and add/change candidates.
+    drupal_alter('language_fallback_candidates', $fallback_candidates);
+  }
+
+  return $fallback_candidates;
+}
+
+/**
+ * @} End of "language_negotiation"
+ */

+ 2442 - 0
includes/locale.inc

@@ -0,0 +1,2442 @@
+<?php
+
+/**
+ * @file
+ * Administration functions for locale.module.
+ */
+
+/**
+ * The language is determined using a URL language indicator:
+ * path prefix or domain according to the configuration.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_URL', 'locale-url');
+
+/**
+ * The language is set based on the browser language settings.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_BROWSER', 'locale-browser');
+
+/**
+ * The language is determined using the current interface language.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_INTERFACE', 'locale-interface');
+
+/**
+ * If no URL language is available language is determined using an already
+ * detected one.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK', 'locale-url-fallback');
+
+/**
+ * The language is set based on the user language settings.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_USER', 'locale-user');
+
+/**
+ * The language is set based on the request/session parameters.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_SESSION', 'locale-session');
+
+/**
+ * Regular expression pattern used to localize JavaScript strings.
+ */
+define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+');
+
+/**
+ * Regular expression pattern used to match simple JS object literal.
+ *
+ * This pattern matches a basic JS object, but will fail on an object with
+ * nested objects. Used in JS file parsing for string arg processing.
+ */
+define('LOCALE_JS_OBJECT', '\{.*?\}');
+
+/**
+ * Regular expression to match an object containing a key 'context'.
+ *
+ * Pattern to match a JS object containing a 'context key' with a string value,
+ * which is captured. Will fail if there are nested objects.
+ */
+define('LOCALE_JS_OBJECT_CONTEXT', '
+  \{              # match object literal start
+  .*?             # match anything, non-greedy
+  (?:             # match a form of "context"
+    \'context\'
+    |
+    "context"
+    |
+    context
+  )
+  \s*:\s*         # match key-value separator ":"
+  (' . LOCALE_JS_STRING . ')  # match context string
+  .*?             # match anything, non-greedy
+  \}              # match end of object literal
+');
+
+/**
+ * Translation import mode overwriting all existing translations
+ * if new translated version available.
+ */
+define('LOCALE_IMPORT_OVERWRITE', 0);
+
+/**
+ * Translation import mode keeping existing translations and only
+ * inserting new strings.
+ */
+define('LOCALE_IMPORT_KEEP', 1);
+
+/**
+ * URL language negotiation: use the path prefix as URL language
+ * indicator.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX', 0);
+
+/**
+ * URL language negotiation: use the domain as URL language
+ * indicator.
+ */
+define('LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN', 1);
+
+/**
+ * @defgroup locale-languages-negotiation Language negotiation options
+ * @{
+ * Functions for language negotiation.
+ *
+ * There are functions that provide the ability to identify the
+ * language. This behavior can be controlled by various options.
+ */
+
+/**
+ * Identifies the language from the current interface language.
+ *
+ * @return
+ *   The current interface language code.
+ */
+function locale_language_from_interface() {
+  global $language;
+  return isset($language->language) ? $language->language : FALSE;
+}
+
+/**
+ * Identify language from the Accept-language HTTP header we got.
+ *
+ * We perform browser accept-language parsing only if page cache is disabled,
+ * otherwise we would cache a user-specific preference.
+ *
+ * @param $languages
+ *   An array of language objects for enabled languages ordered by weight.
+ *
+ * @return
+ *   A valid language code on success, FALSE otherwise.
+ */
+function locale_language_from_browser($languages) {
+  if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+    return FALSE;
+  }
+
+  // The Accept-Language header contains information about the language
+  // preferences configured in the user's browser / operating system.
+  // RFC 2616 (section 14.4) defines the Accept-Language header as follows:
+  //   Accept-Language = "Accept-Language" ":"
+  //                  1#( language-range [ ";" "q" "=" qvalue ] )
+  //   language-range  = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
+  // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
+  $browser_langcodes = array();
+  if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
+    foreach ($matches as $match) {
+      // We can safely use strtolower() here, tags are ASCII.
+      // RFC2616 mandates that the decimal part is no more than three digits,
+      // so we multiply the qvalue by 1000 to avoid floating point comparisons.
+      $langcode = strtolower($match[1]);
+      $qvalue = isset($match[2]) ? (float) $match[2] : 1;
+      $browser_langcodes[$langcode] = (int) ($qvalue * 1000);
+    }
+  }
+
+  // We should take pristine values from the HTTP headers, but Internet Explorer
+  // from version 7 sends only specific language tags (eg. fr-CA) without the
+  // corresponding generic tag (fr) unless explicitly configured. In that case,
+  // we assume that the lowest value of the specific tags is the value of the
+  // generic language to be as close to the HTTP 1.1 spec as possible.
+  // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 and
+  // http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
+  asort($browser_langcodes);
+  foreach ($browser_langcodes as $langcode => $qvalue) {
+    $generic_tag = strtok($langcode, '-');
+    if (!isset($browser_langcodes[$generic_tag])) {
+      $browser_langcodes[$generic_tag] = $qvalue;
+    }
+  }
+
+  // Find the enabled language with the greatest qvalue, following the rules
+  // of RFC 2616 (section 14.4). If several languages have the same qvalue,
+  // prefer the one with the greatest weight.
+  $best_match_langcode = FALSE;
+  $max_qvalue = 0;
+  foreach ($languages as $langcode => $language) {
+    // Language tags are case insensitive (RFC2616, sec 3.10).
+    $langcode = strtolower($langcode);
+
+    // If nothing matches below, the default qvalue is the one of the wildcard
+    // language, if set, or is 0 (which will never match).
+    $qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0;
+
+    // Find the longest possible prefix of the browser-supplied language
+    // ('the language-range') that matches this site language ('the language tag').
+    $prefix = $langcode;
+    do {
+      if (isset($browser_langcodes[$prefix])) {
+        $qvalue = $browser_langcodes[$prefix];
+        break;
+      }
+    }
+    while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));
+
+    // Find the best match.
+    if ($qvalue > $max_qvalue) {
+      $best_match_langcode = $language->language;
+      $max_qvalue = $qvalue;
+    }
+  }
+
+  return $best_match_langcode;
+}
+
+/**
+ * Identify language from the user preferences.
+ *
+ * @param $languages
+ *   An array of valid language objects.
+ *
+ * @return
+ *   A valid language code on success, FALSE otherwise.
+ */
+function locale_language_from_user($languages) {
+  // User preference (only for logged users).
+  global $user;
+
+  if ($user->uid) {
+    return $user->language;
+  }
+
+  // No language preference from the user.
+  return FALSE;
+}
+
+/**
+ * Identify language from a request/session parameter.
+ *
+ * @param $languages
+ *   An array of valid language objects.
+ *
+ * @return
+ *   A valid language code on success, FALSE otherwise.
+ */
+function locale_language_from_session($languages) {
+  $param = variable_get('locale_language_negotiation_session_param', 'language');
+
+  // Request parameter: we need to update the session parameter only if we have
+  // an authenticated user.
+  if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
+    global $user;
+    if ($user->uid) {
+      $_SESSION[$param] = $langcode;
+    }
+    return $langcode;
+  }
+
+  // Session parameter.
+  if (isset($_SESSION[$param])) {
+    return $_SESSION[$param];
+  }
+
+  return FALSE;
+}
+
+/**
+ * Identify language via URL prefix or domain.
+ *
+ * @param $languages
+ *   An array of valid language objects.
+ *
+ * @return
+ *   A valid language code on success, FALSE otherwise.
+ */
+function locale_language_from_url($languages) {
+  $language_url = FALSE;
+
+  if (!language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) {
+    return $language_url;
+  }
+
+  switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
+    case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
+      // $_GET['q'] might not be available at this time, because
+      // path initialization runs after the language bootstrap phase.
+      list($language, $_GET['q']) = language_url_split_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages);
+      if ($language !== FALSE) {
+        $language_url = $language->language;
+      }
+      break;
+
+    case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
+      // Get only the host, not the port.
+      $http_host= $_SERVER['HTTP_HOST'];
+      if (strpos($http_host, ':') !== FALSE) {
+        $http_host_tmp = explode(':', $http_host);
+        $http_host = current($http_host_tmp);
+      }
+      foreach ($languages as $language) {
+        // Skip check if the language doesn't have a domain.
+        if ($language->domain) {
+          // Only compare the domains not the protocols or ports.
+          // Remove protocol and add http:// so parse_url works
+          $host = 'http://' . str_replace(array('http://', 'https://'), '', $language->domain);
+          $host = parse_url($host, PHP_URL_HOST);
+          if ($http_host == $host) {
+            $language_url = $language->language;
+            break;
+          }
+        }
+      }
+      break;
+  }
+
+  return $language_url;
+}
+
+/**
+ * Determines the language to be assigned to URLs when none is detected.
+ *
+ * The language negotiation process has a fallback chain that ends with the
+ * default language provider. Each built-in language type has a separate
+ * initialization:
+ * - Interface language, which is the only configurable one, always gets a valid
+ *   value. If no request-specific language is detected, the default language
+ *   will be used.
+ * - Content language merely inherits the interface language by default.
+ * - URL language is detected from the requested URL and will be used to rewrite
+ *   URLs appearing in the page being rendered. If no language can be detected,
+ *   there are two possibilities:
+ *   - If the default language has no configured path prefix or domain, then the
+ *     default language is used. This guarantees that (missing) URL prefixes are
+ *     preserved when navigating through the site.
+ *   - If the default language has a configured path prefix or domain, a
+ *     requested URL having an empty prefix or domain is an anomaly that must be
+ *     fixed. This is done by introducing a prefix or domain in the rendered
+ *     page matching the detected interface language.
+ *
+ * @param $languages
+ *   (optional) An array of valid language objects. This is passed by
+ *   language_provider_invoke() to every language provider callback, but it is
+ *   not actually needed here. Defaults to NULL.
+ * @param $language_type
+ *   (optional) The language type to fall back to. Defaults to the interface
+ *   language.
+ *
+ * @return
+ *   A valid language code.
+ */
+function locale_language_url_fallback($language = NULL, $language_type = LANGUAGE_TYPE_INTERFACE) {
+  $default = language_default();
+  $prefix = (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX);
+
+  // If the default language is not configured to convey language information,
+  // a missing URL language information indicates that URL language should be
+  // the default one, otherwise we fall back to an already detected language.
+  if (($prefix && empty($default->prefix)) || (!$prefix && empty($default->domain))) {
+    return $default->language;
+  }
+  else {
+    return $GLOBALS[$language_type]->language;
+  }
+}
+
+/**
+ * Return the URL language switcher block. Translation links may be provided by
+ * other modules.
+ */
+function locale_language_switcher_url($type, $path) {
+  $languages = language_list('enabled');
+  $links = array();
+
+  foreach ($languages[1] as $language) {
+    $links[$language->language] = array(
+      'href'       => $path,
+      'title'      => $language->native,
+      'language'   => $language,
+      'attributes' => array('class' => array('language-link')),
+    );
+  }
+
+  return $links;
+}
+
+/**
+ * Return the session language switcher block.
+ */
+function locale_language_switcher_session($type, $path) {
+  drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
+
+  $param = variable_get('locale_language_negotiation_session_param', 'language');
+  $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->language;
+
+  $languages = language_list('enabled');
+  $links = array();
+
+  $query = $_GET;
+  unset($query['q']);
+
+  foreach ($languages[1] as $language) {
+    $langcode = $language->language;
+    $links[$langcode] = array(
+      'href'       => $path,
+      'title'      => $language->native,
+      'attributes' => array('class' => array('language-link')),
+      'query'      => $query,
+    );
+    if ($language_query != $langcode) {
+      $links[$langcode]['query'][$param] = $langcode;
+    }
+    else {
+      $links[$langcode]['attributes']['class'][] = ' session-active';
+    }
+  }
+
+  return $links;
+}
+
+/**
+ * Rewrite URLs for the URL language provider.
+ */
+function locale_language_url_rewrite_url(&$path, &$options) {
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['languages'] = &drupal_static(__FUNCTION__);
+  }
+  $languages = &$drupal_static_fast['languages'];
+
+  if (!isset($languages)) {
+    $languages = language_list('enabled');
+    $languages = array_flip(array_keys($languages[1]));
+  }
+
+  // Language can be passed as an option, or we go for current URL language.
+  if (!isset($options['language'])) {
+    global $language_url;
+    $options['language'] = $language_url;
+  }
+  // We allow only enabled languages here.
+  elseif (!isset($languages[$options['language']->language])) {
+    unset($options['language']);
+    return;
+  }
+
+  if (isset($options['language'])) {
+    switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
+      case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
+        if ($options['language']->domain) {
+          // Ask for an absolute URL with our modified base_url.
+          global $is_https;
+          $url_scheme = ($is_https) ? 'https://' : 'http://';
+          $options['absolute'] = TRUE;
+
+          // Take the domain without ports or protocols so we can apply the
+          // protocol needed. The setting might include a protocol.
+          // This is changed in Drupal 8 but we need to keep backwards
+          // compatibility for Drupal 7.
+          $host = 'http://' . str_replace(array('http://', 'https://'), '', $options['language']->domain);
+          $host = parse_url($host, PHP_URL_HOST);
+
+          // Apply the appropriate protocol to the URL.
+          $options['base_url'] = $url_scheme . $host;
+          if (isset($options['https']) && variable_get('https', FALSE)) {
+            if ($options['https'] === TRUE) {
+              $options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
+            }
+            elseif ($options['https'] === FALSE) {
+              $options['base_url'] = str_replace('https://', 'http://', $options['base_url']);
+            }
+          }
+        }
+        break;
+
+      case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
+        if (!empty($options['language']->prefix)) {
+          $options['prefix'] = $options['language']->prefix . '/';
+        }
+        break;
+    }
+  }
+}
+
+/**
+ * Rewrite URLs for the Session language provider.
+ */
+function locale_language_url_rewrite_session(&$path, &$options) {
+  static $query_rewrite, $query_param, $query_value;
+
+  // The following values are not supposed to change during a single page
+  // request processing.
+  if (!isset($query_rewrite)) {
+    global $user;
+    if (!$user->uid) {
+      $languages = language_list('enabled');
+      $languages = $languages[1];
+      $query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language'));
+      $query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL;
+      $query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION);
+    }
+    else {
+      $query_rewrite = FALSE;
+    }
+  }
+
+  // If the user is anonymous, the user language provider is enabled, and the
+  // corresponding option has been set, we must preserve any explicit user
+  // language preference even with cookies disabled.
+  if ($query_rewrite) {
+    if (is_string($options['query'])) {
+      $options['query'] = drupal_get_query_array($options['query']);
+    }
+    if (!isset($options['query'][$query_param])) {
+      $options['query'][$query_param] = $query_value;
+    }
+  }
+}
+
+/**
+ * @} End of "locale-languages-negotiation"
+ */
+
+/**
+ * Check that a string is safe to be added or imported as a translation.
+ *
+ * This test can be used to detect possibly bad translation strings. It should
+ * not have any false positives. But it is only a test, not a transformation,
+ * as it destroys valid HTML. We cannot reliably filter translation strings
+ * on import because some strings are irreversibly corrupted. For example,
+ * a &amp; in the translation would get encoded to &amp;amp; by filter_xss()
+ * before being put in the database, and thus would be displayed incorrectly.
+ *
+ * The allowed tag list is like filter_xss_admin(), but omitting div and img as
+ * not needed for translation and likely to cause layout issues (div) or a
+ * possible attack vector (img).
+ */
+function locale_string_is_safe($string) {
+  return decode_entities($string) == decode_entities(filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var')));
+}
+
+/**
+ * @defgroup locale-api-add Language addition API
+ * @{
+ * Add a language.
+ *
+ * The language addition API is used to create languages and store them.
+ */
+
+/**
+ * API function to add a language.
+ *
+ * @param $langcode
+ *   Language code.
+ * @param $name
+ *   English name of the language
+ * @param $native
+ *   Native name of the language
+ * @param $direction
+ *   LANGUAGE_LTR or LANGUAGE_RTL
+ * @param $domain
+ *   Optional custom domain name with protocol, without
+ *   trailing slash (eg. http://de.example.com).
+ * @param $prefix
+ *   Optional path prefix for the language. Defaults to the
+ *   language code if omitted.
+ * @param $enabled
+ *   Optionally TRUE to enable the language when created or FALSE to disable.
+ * @param $default
+ *   Optionally set this language to be the default.
+ */
+function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
+  // Default prefix on language code.
+  if (empty($prefix)) {
+    $prefix = $langcode;
+  }
+
+  // If name was not set, we add a predefined language.
+  if (!isset($name)) {
+    include_once DRUPAL_ROOT . '/includes/iso.inc';
+    $predefined = _locale_get_predefined_list();
+    $name = $predefined[$langcode][0];
+    $native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0];
+    $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
+  }
+
+  db_insert('languages')
+    ->fields(array(
+      'language' => $langcode,
+      'name' => $name,
+      'native' => $native,
+      'direction' => $direction,
+      'domain' => $domain,
+      'prefix' => $prefix,
+      'enabled' => $enabled,
+    ))
+    ->execute();
+
+  // Only set it as default if enabled.
+  if ($enabled && $default) {
+    variable_set('language_default', (object) array('language' => $langcode, 'name' => $name, 'native' => $native, 'direction' => $direction, 'enabled' => (int) $enabled, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => $prefix, 'weight' => 0, 'javascript' => ''));
+  }
+
+  if ($enabled) {
+    // Increment enabled language count if we are adding an enabled language.
+    variable_set('language_count', variable_get('language_count', 1) + 1);
+  }
+
+  // Kill the static cache in language_list().
+  drupal_static_reset('language_list');
+
+  // Force JavaScript translation file creation for the newly added language.
+  _locale_invalidate_js($langcode);
+
+  watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
+
+  module_invoke_all('multilingual_settings_changed');
+}
+/**
+ * @} End of "locale-api-add"
+ */
+
+/**
+ * @defgroup locale-api-import-export Translation import/export API.
+ * @{
+ * Functions to import and export translations.
+ *
+ * These functions provide the ability to import translations from
+ * external files and to export translations and translation templates.
+ */
+
+/**
+ * Parses Gettext Portable Object file information and inserts into database
+ *
+ * @param $file
+ *   Drupal file object corresponding to the PO file to import.
+ * @param $langcode
+ *   Language code.
+ * @param $mode
+ *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
+ *   LOCALE_IMPORT_OVERWRITE.
+ * @param $group
+ *   Text group to import PO file into (eg. 'default' for interface
+ *   translations).
+ */
+function _locale_import_po($file, $langcode, $mode, $group = NULL) {
+  // Try to allocate enough time to parse and import the data.
+  drupal_set_time_limit(240);
+
+  // Check if we have the language already in the database.
+  if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) {
+    drupal_set_message(t('The language selected for import is not supported.'), 'error');
+    return FALSE;
+  }
+
+  // Get strings from file (returns on failure after a partial import, or on success)
+  $status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group);
+  if ($status === FALSE) {
+    // Error messages are set in _locale_import_read_po().
+    return FALSE;
+  }
+
+  // Get status information on import process.
+  list($header_done, $additions, $updates, $deletes, $skips) = _locale_import_one_string('db-report');
+
+  if (!$header_done) {
+    drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
+  }
+
+  // Clear cache and force refresh of JavaScript translations.
+  _locale_invalidate_js($langcode);
+  cache_clear_all('locale:', 'cache', TRUE);
+
+  // Rebuild the menu, strings may have changed.
+  menu_rebuild();
+
+  drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
+  watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
+  if ($skips) {
+    $skip_message = format_plural($skips, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.');
+    drupal_set_message($skip_message);
+    watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
+  }
+  return TRUE;
+}
+
+/**
+ * Parses Gettext Portable Object file into an array
+ *
+ * @param $op
+ *   Storage operation type: db-store or mem-store.
+ * @param $file
+ *   Drupal file object corresponding to the PO file to import.
+ * @param $mode
+ *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
+ *   LOCALE_IMPORT_OVERWRITE.
+ * @param $lang
+ *   Language code.
+ * @param $group
+ *   Text group to import PO file into (eg. 'default' for interface
+ *   translations).
+ */
+function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
+
+  // The file will get closed by PHP on returning from this function.
+  $fd = fopen($file->uri, 'rb');
+  if (!$fd) {
+    _locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
+    return FALSE;
+  }
+
+  /*
+   * The parser context. Can be:
+   *  - 'COMMENT' (#)
+   *  - 'MSGID' (msgid)
+   *  - 'MSGID_PLURAL' (msgid_plural)
+   *  - 'MSGCTXT' (msgctxt)
+   *  - 'MSGSTR' (msgstr or msgstr[])
+   *  - 'MSGSTR_ARR' (msgstr_arg)
+   */
+  $context = 'COMMENT';
+
+  // Current entry being read.
+  $current = array();
+
+  // Current plurality for 'msgstr[]'.
+  $plural = 0;
+
+  // Current line.
+  $lineno = 0;
+
+  while (!feof($fd)) {
+    // A line should not be longer than 10 * 1024.
+    $line = fgets($fd, 10 * 1024);
+
+    if ($lineno == 0) {
+      // The first line might come with a UTF-8 BOM, which should be removed.
+      $line = str_replace("\xEF\xBB\xBF", '', $line);
+    }
+
+    $lineno++;
+
+    // Trim away the linefeed.
+    $line = trim(strtr($line, array("\\\n" => "")));
+
+    if (!strncmp('#', $line, 1)) {
+      // Lines starting with '#' are comments.
+
+      if ($context == 'COMMENT') {
+        // Already in comment token, insert the comment.
+        $current['#'][] = substr($line, 1);
+      }
+      elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
+        // We are currently in string token, close it out.
+        _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
+
+        // Start a new entry for the comment.
+        $current         = array();
+        $current['#'][]  = substr($line, 1);
+
+        $context = 'COMMENT';
+      }
+      else {
+        // A comment following any other token is a syntax error.
+        _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
+        return FALSE;
+      }
+    }
+    elseif (!strncmp('msgid_plural', $line, 12)) {
+      // A plural form for the current message.
+
+      if ($context != 'MSGID') {
+        // A plural form cannot be added to anything else but the id directly.
+        _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Remove 'msgid_plural' and trim away whitespace.
+      $line = trim(substr($line, 12));
+      // At this point, $line should now contain only the plural form.
+
+      $quoted = _locale_import_parse_quoted($line);
+      if ($quoted === FALSE) {
+        // The plural form must be wrapped in quotes.
+        _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Append the plural form to the current entry.
+      $current['msgid'] .= "\0" . $quoted;
+
+      $context = 'MSGID_PLURAL';
+    }
+    elseif (!strncmp('msgid', $line, 5)) {
+      // Starting a new message.
+
+      if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
+        // We are currently in a message string, close it out.
+        _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
+
+        // Start a new context for the id.
+        $current = array();
+      }
+      elseif ($context == 'MSGID') {
+        // We are currently already in the context, meaning we passed an id with no data.
+        _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Remove 'msgid' and trim away whitespace.
+      $line = trim(substr($line, 5));
+      // At this point, $line should now contain only the message id.
+
+      $quoted = _locale_import_parse_quoted($line);
+      if ($quoted === FALSE) {
+        // The message id must be wrapped in quotes.
+        _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      $current['msgid'] = $quoted;
+      $context = 'MSGID';
+    }
+    elseif (!strncmp('msgctxt', $line, 7)) {
+      // Starting a new context.
+
+      if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
+        // We are currently in a message, start a new one.
+        _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
+        $current = array();
+      }
+      elseif (!empty($current['msgctxt'])) {
+        // A context cannot apply to another context.
+        _locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Remove 'msgctxt' and trim away whitespaces.
+      $line = trim(substr($line, 7));
+      // At this point, $line should now contain the context.
+
+      $quoted = _locale_import_parse_quoted($line);
+      if ($quoted === FALSE) {
+        // The context string must be quoted.
+        _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      $current['msgctxt'] = $quoted;
+
+      $context = 'MSGCTXT';
+    }
+    elseif (!strncmp('msgstr[', $line, 7)) {
+      // A message string for a specific plurality.
+
+      if (($context != 'MSGID') && ($context != 'MSGCTXT') && ($context != 'MSGID_PLURAL') && ($context != 'MSGSTR_ARR')) {
+        // Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries.
+        _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Ensure the plurality is terminated.
+      if (strpos($line, ']') === FALSE) {
+        _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Extract the plurality.
+      $frombracket = strstr($line, '[');
+      $plural = substr($frombracket, 1, strpos($frombracket, ']') - 1);
+
+      // Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data.
+      $line = trim(strstr($line, " "));
+
+      $quoted = _locale_import_parse_quoted($line);
+      if ($quoted === FALSE) {
+        // The string must be quoted.
+        _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      $current['msgstr'][$plural] = $quoted;
+
+      $context = 'MSGSTR_ARR';
+    }
+    elseif (!strncmp("msgstr", $line, 6)) {
+      // A string for the an id or context.
+
+      if (($context != 'MSGID') && ($context != 'MSGCTXT')) {
+        // Strings are only valid within an id or context scope.
+        _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Remove 'msgstr' and trim away away whitespaces.
+      $line = trim(substr($line, 6));
+      // At this point, $line should now contain the message.
+
+      $quoted = _locale_import_parse_quoted($line);
+      if ($quoted === FALSE) {
+        // The string must be quoted.
+        _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      $current['msgstr'] = $quoted;
+
+      $context = 'MSGSTR';
+    }
+    elseif ($line != '') {
+      // Anything that is not a token may be a continuation of a previous token.
+
+      $quoted = _locale_import_parse_quoted($line);
+      if ($quoted === FALSE) {
+        // The string must be quoted.
+        _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
+        return FALSE;
+      }
+
+      // Append the string to the current context.
+      if (($context == 'MSGID') || ($context == 'MSGID_PLURAL')) {
+        $current['msgid'] .= $quoted;
+      }
+      elseif ($context == 'MSGCTXT') {
+        $current['msgctxt'] .= $quoted;
+      }
+      elseif ($context == 'MSGSTR') {
+        $current['msgstr'] .= $quoted;
+      }
+      elseif ($context == 'MSGSTR_ARR') {
+        $current['msgstr'][$plural] .= $quoted;
+      }
+      else {
+        // No valid context to append to.
+        _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
+        return FALSE;
+      }
+    }
+  }
+
+  // End of PO file, closed out the last entry.
+  if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
+    _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
+  }
+  elseif ($context != 'COMMENT') {
+    _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
+    return FALSE;
+  }
+}
+
+/**
+ * Sets an error message occurred during locale file parsing.
+ *
+ * @param $message
+ *   The message to be translated.
+ * @param $file
+ *   Drupal file object corresponding to the PO file to import.
+ * @param $lineno
+ *   An optional line number argument.
+ */
+function _locale_import_message($message, $file, $lineno = NULL) {
+  $vars = array('%filename' => $file->filename);
+  if (isset($lineno)) {
+    $vars['%line'] = $lineno;
+  }
+  $t = get_t();
+  drupal_set_message($t($message, $vars), 'error');
+}
+
+/**
+ * Imports a string into the database
+ *
+ * @param $op
+ *   Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'.
+ * @param $value
+ *   Details of the string stored.
+ * @param $mode
+ *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
+ *   LOCALE_IMPORT_OVERWRITE.
+ * @param $lang
+ *   Language to store the string in.
+ * @param $file
+ *   Object representation of file being imported, only required when op is
+ *   'db-store'.
+ * @param $group
+ *   Text group to import PO file into (eg. 'default' for interface
+ *   translations).
+ */
+function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
+  $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
+  $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
+  $strings = &drupal_static(__FUNCTION__ . ':strings', array());
+
+  switch ($op) {
+    // Return stored strings
+    case 'mem-report':
+      return $strings;
+
+    // Store string in memory (only supports single strings)
+    case 'mem-store':
+      $strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr'];
+      return;
+
+    // Called at end of import to inform the user
+    case 'db-report':
+      return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']);
+
+    // Store the string we got in the database.
+    case 'db-store':
+      // We got header information.
+      if ($value['msgid'] == '') {
+        $languages = language_list();
+        if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) {
+          // Since we only need to parse the header if we ought to update the
+          // plural formula, only run this if we don't need to keep existing
+          // data untouched or if we don't have an existing plural formula.
+          $header = _locale_import_parse_header($value['msgstr']);
+
+          // Get and store the plural formula if available.
+          if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) {
+            list($nplurals, $plural) = $p;
+            db_update('languages')
+              ->fields(array(
+                'plurals' => $nplurals,
+                'formula' => $plural,
+              ))
+              ->condition('language', $lang)
+              ->execute();
+          }
+        }
+        $header_done = TRUE;
+      }
+
+      else {
+        // Some real string to import.
+        $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
+
+        if (strpos($value['msgid'], "\0")) {
+          // This string has plural versions.
+          $english = explode("\0", $value['msgid'], 2);
+          $entries = array_keys($value['msgstr']);
+          for ($i = 3; $i <= count($entries); $i++) {
+            $english[] = $english[1];
+          }
+          $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
+          $english = array_map('_locale_import_append_plural', $english, $entries);
+          foreach ($translation as $key => $trans) {
+            if ($key == 0) {
+              $plid = 0;
+            }
+            $plid = _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, $plid, $key);
+          }
+        }
+
+        else {
+          // A simple string to import.
+          $english = $value['msgid'];
+          $translation = $value['msgstr'];
+          _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode);
+        }
+      }
+  } // end of db-store operation
+}
+
+/**
+ * Import one string into the database.
+ *
+ * @param $report
+ *   Report array summarizing the number of changes done in the form:
+ *   array(inserts, updates, deletes).
+ * @param $langcode
+ *   Language code to import string into.
+ * @param $context
+ *   The context of this string.
+ * @param $source
+ *   Source string.
+ * @param $translation
+ *   Translation to language specified in $langcode.
+ * @param $textgroup
+ *   Name of textgroup to store translation in.
+ * @param $location
+ *   Location value to save with source string.
+ * @param $mode
+ *   Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
+ * @param $plid
+ *   Optional plural ID to use.
+ * @param $plural
+ *   Optional plural value to use.
+ *
+ * @return
+ *   The string ID of the existing string modified or the new string added.
+ */
+function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $plid = 0, $plural = 0) {
+  $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField();
+
+  if (!empty($translation)) {
+    // Skip this string unless it passes a check for dangerous code.
+    // Text groups other than default still can contain HTML tags
+    // (i.e. translatable blocks).
+    if ($textgroup == "default" && !locale_string_is_safe($translation)) {
+      $report['skips']++;
+      $lid = 0;
+    }
+    elseif ($lid) {
+      // We have this source string saved already.
+      db_update('locales_source')
+        ->fields(array(
+          'location' => $location,
+        ))
+        ->condition('lid', $lid)
+        ->execute();
+
+      $exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField();
+
+      if (!$exists) {
+        // No translation in this language.
+        db_insert('locales_target')
+          ->fields(array(
+            'lid' => $lid,
+            'language' => $langcode,
+            'translation' => $translation,
+            'plid' => $plid,
+            'plural' => $plural,
+          ))
+          ->execute();
+
+        $report['additions']++;
+      }
+      elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
+        // Translation exists, only overwrite if instructed.
+        db_update('locales_target')
+          ->fields(array(
+            'translation' => $translation,
+            'plid' => $plid,
+            'plural' => $plural,
+          ))
+          ->condition('language', $langcode)
+          ->condition('lid', $lid)
+          ->execute();
+
+        $report['updates']++;
+      }
+    }
+    else {
+      // No such source string in the database yet.
+      $lid = db_insert('locales_source')
+        ->fields(array(
+          'location' => $location,
+          'source' => $source,
+          'context' => (string) $context,
+          'textgroup' => $textgroup,
+        ))
+        ->execute();
+
+      db_insert('locales_target')
+        ->fields(array(
+           'lid' => $lid,
+           'language' => $langcode,
+           'translation' => $translation,
+           'plid' => $plid,
+           'plural' => $plural
+        ))
+        ->execute();
+
+      $report['additions']++;
+    }
+  }
+  elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
+    // Empty translation, remove existing if instructed.
+    db_delete('locales_target')
+      ->condition('language', $langcode)
+      ->condition('lid', $lid)
+      ->condition('plid', $plid)
+      ->condition('plural', $plural)
+      ->execute();
+
+    $report['deletes']++;
+  }
+
+  return $lid;
+}
+
+/**
+ * Parses a Gettext Portable Object file header
+ *
+ * @param $header
+ *   A string containing the complete header.
+ *
+ * @return
+ *   An associative array of key-value pairs.
+ */
+function _locale_import_parse_header($header) {
+  $header_parsed = array();
+  $lines = array_map('trim', explode("\n", $header));
+  foreach ($lines as $line) {
+    if ($line) {
+      list($tag, $contents) = explode(":", $line, 2);
+      $header_parsed[trim($tag)] = trim($contents);
+    }
+  }
+  return $header_parsed;
+}
+
+/**
+ * Parses a Plural-Forms entry from a Gettext Portable Object file header
+ *
+ * @param $pluralforms
+ *   A string containing the Plural-Forms entry.
+ * @param $filepath
+ *   A string containing the filepath.
+ *
+ * @return
+ *   An array containing the number of plurals and a
+ *   formula in PHP for computing the plural form.
+ */
+function _locale_import_parse_plural_forms($pluralforms, $filepath) {
+  // First, delete all whitespace
+  $pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
+
+  // Select the parts that define nplurals and plural
+  $nplurals = strstr($pluralforms, "nplurals=");
+  if (strpos($nplurals, ";")) {
+    $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
+  }
+  else {
+    return FALSE;
+  }
+  $plural = strstr($pluralforms, "plural=");
+  if (strpos($plural, ";")) {
+    $plural = substr($plural, 7, strpos($plural, ";") - 7);
+  }
+  else {
+    return FALSE;
+  }
+
+  // Get PHP version of the plural formula
+  $plural = _locale_import_parse_arithmetic($plural);
+
+  if ($plural !== FALSE) {
+    return array($nplurals, $plural);
+  }
+  else {
+    drupal_set_message(t('The translation file %filepath contains an error: the plural formula could not be parsed.', array('%filepath' => $filepath)), 'error');
+    return FALSE;
+  }
+}
+
+/**
+ * Parses and sanitizes an arithmetic formula into a PHP expression
+ *
+ * While parsing, we ensure, that the operators have the right
+ * precedence and associativity.
+ *
+ * @param $string
+ *   A string containing the arithmetic formula.
+ *
+ * @return
+ *   The PHP version of the formula.
+ */
+function _locale_import_parse_arithmetic($string) {
+  // Operator precedence table
+  $precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8);
+  // Right associativity
+  $right_associativity = array("?" => 1, ":" => 1);
+
+  $tokens = _locale_import_tokenize_formula($string);
+
+  // Parse by converting into infix notation then back into postfix
+  // Operator stack - holds math operators and symbols
+  $operator_stack = array();
+  // Element Stack - holds data to be operated on
+  $element_stack = array();
+
+  foreach ($tokens as $token) {
+    $current_token = $token;
+
+    // Numbers and the $n variable are simply pushed into $element_stack
+    if (is_numeric($token)) {
+      $element_stack[] = $current_token;
+    }
+    elseif ($current_token == "n") {
+      $element_stack[] = '$n';
+    }
+    elseif ($current_token == "(") {
+      $operator_stack[] = $current_token;
+    }
+    elseif ($current_token == ")") {
+      $topop = array_pop($operator_stack);
+      while (isset($topop) && ($topop != "(")) {
+        $element_stack[] = $topop;
+        $topop = array_pop($operator_stack);
+      }
+    }
+    elseif (!empty($precedence[$current_token])) {
+      // If it's an operator, then pop from $operator_stack into $element_stack until the
+      // precedence in $operator_stack is less than current, then push into $operator_stack
+      $topop = array_pop($operator_stack);
+      while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
+        $element_stack[] = $topop;
+        $topop = array_pop($operator_stack);
+      }
+      if ($topop) {
+        $operator_stack[] = $topop;   // Return element to top
+      }
+      $operator_stack[] = $current_token;      // Parentheses are not needed
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  // Flush operator stack
+  $topop = array_pop($operator_stack);
+  while ($topop != NULL) {
+    $element_stack[] = $topop;
+    $topop = array_pop($operator_stack);
+  }
+
+  // Now extract formula from stack
+  $previous_size = count($element_stack) + 1;
+  while (count($element_stack) < $previous_size) {
+    $previous_size = count($element_stack);
+    for ($i = 2; $i < count($element_stack); $i++) {
+      $op = $element_stack[$i];
+      if (!empty($precedence[$op])) {
+        $f = "";
+        if ($op == ":") {
+          $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")";
+        }
+        elseif ($op == "?") {
+          $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1];
+        }
+        else {
+          $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")";
+        }
+        array_splice($element_stack, $i - 2, 3, $f);
+        break;
+      }
+    }
+  }
+
+  // If only one element is left, the number of operators is appropriate
+  if (count($element_stack) == 1) {
+    return $element_stack[0];
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Backward compatible implementation of token_get_all() for formula parsing
+ *
+ * @param $string
+ *   A string containing the arithmetic formula.
+ *
+ * @return
+ *   The PHP version of the formula.
+ */
+function _locale_import_tokenize_formula($formula) {
+  $formula = str_replace(" ", "", $formula);
+  $tokens = array();
+  for ($i = 0; $i < strlen($formula); $i++) {
+    if (is_numeric($formula[$i])) {
+      $num = $formula[$i];
+      $j = $i + 1;
+      while ($j < strlen($formula) && is_numeric($formula[$j])) {
+        $num .= $formula[$j];
+        $j++;
+      }
+      $i = $j - 1;
+      $tokens[] = $num;
+    }
+    elseif ($pos = strpos(" =<>!&|", $formula[$i])) { // We won't have a space
+      $next = $formula[$i + 1];
+      switch ($pos) {
+        case 1:
+        case 2:
+        case 3:
+        case 4:
+          if ($next == '=') {
+            $tokens[] = $formula[$i] . '=';
+            $i++;
+          }
+          else {
+            $tokens[] = $formula[$i];
+          }
+          break;
+        case 5:
+          if ($next == '&') {
+            $tokens[] = '&&';
+            $i++;
+          }
+          else {
+            $tokens[] = $formula[$i];
+          }
+          break;
+        case 6:
+          if ($next == '|') {
+            $tokens[] = '||';
+            $i++;
+          }
+          else {
+            $tokens[] = $formula[$i];
+          }
+          break;
+      }
+    }
+    else {
+      $tokens[] = $formula[$i];
+    }
+  }
+  return $tokens;
+}
+
+/**
+ * Modify a string to contain proper count indices
+ *
+ * This is a callback function used via array_map()
+ *
+ * @param $entry
+ *   An array element.
+ * @param $key
+ *   Index of the array element.
+ */
+function _locale_import_append_plural($entry, $key) {
+  // No modifications for 0, 1
+  if ($key == 0 || $key == 1) {
+    return $entry;
+  }
+
+  // First remove any possibly false indices, then add new ones
+  $entry = preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
+  return preg_replace('/(@count)/', "\\1[$key]", $entry);
+}
+
+/**
+ * Generate a short, one string version of the passed comment array
+ *
+ * @param $comment
+ *   An array of strings containing a comment.
+ *
+ * @return
+ *   Short one string version of the comment.
+ */
+function _locale_import_shorten_comments($comment) {
+  $comm = '';
+  while (count($comment)) {
+    $test = $comm . substr(array_shift($comment), 1) . ', ';
+    if (strlen($comm) < 130) {
+      $comm = $test;
+    }
+    else {
+      break;
+    }
+  }
+  return trim(substr($comm, 0, -2));
+}
+
+/**
+ * Parses a string in quotes
+ *
+ * @param $string
+ *   A string specified with enclosing quotes.
+ *
+ * @return
+ *   The string parsed from inside the quotes.
+ */
+function _locale_import_parse_quoted($string) {
+  if (substr($string, 0, 1) != substr($string, -1, 1)) {
+    return FALSE;   // Start and end quotes must be the same
+  }
+  $quote = substr($string, 0, 1);
+  $string = substr($string, 1, -1);
+  if ($quote == '"') {        // Double quotes: strip slashes
+    return stripcslashes($string);
+  }
+  elseif ($quote == "'") {  // Simple quote: return as-is
+    return $string;
+  }
+  else {
+    return FALSE;             // Unrecognized quote
+  }
+}
+/**
+ * @} End of "locale-api-import-export"
+ */
+
+/**
+ * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and
+ * Drupal.formatPlural() and inserts them into the database.
+ */
+function _locale_parse_js_file($filepath) {
+  global $language;
+
+  // The file path might contain a query string, so make sure we only use the
+  // actual file.
+  $parsed_url = drupal_parse_url($filepath);
+  $filepath = $parsed_url['path'];
+  // Load the JavaScript file.
+  $file = file_get_contents($filepath);
+
+  // Match all calls to Drupal.t() in an array.
+  // Note: \s also matches newlines with the 's' modifier.
+  preg_match_all('~
+    [^\w]Drupal\s*\.\s*t\s*                       # match "Drupal.t" with whitespace
+    \(\s*                                         # match "(" argument list start
+    (' . LOCALE_JS_STRING . ')\s*                 # capture string argument
+    (?:,\s*' . LOCALE_JS_OBJECT . '\s*            # optionally capture str args
+      (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*) # optionally capture context
+    ?)?                                           # close optional args
+    [,\)]                                         # match ")" or "," to finish
+    ~sx', $file, $t_matches);
+
+  // Match all Drupal.formatPlural() calls in another array.
+  preg_match_all('~
+    [^\w]Drupal\s*\.\s*formatPlural\s*  # match "Drupal.formatPlural" with whitespace
+    \(                                  # match "(" argument list start
+    \s*.+?\s*,\s*                       # match count argument
+    (' . LOCALE_JS_STRING . ')\s*,\s*   # match singular string argument
+    (                             # capture plural string argument
+      (?:                         # non-capturing group to repeat string pieces
+        (?:
+          \'                      # match start of single-quoted string
+          (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
+          @count                  # match "@count"
+          (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
+          \'                      # match end of single-quoted string
+          |
+          "                       # match start of double-quoted string
+          (?:\\\\"|[^"])*         # match any character except unescaped double-quote
+          @count                  # match "@count"
+          (?:\\\\"|[^"])*         # match any character except unescaped double-quote
+          "                       # match end of double-quoted string
+        )
+        (?:\s*\+\s*)?             # match "+" with possible whitespace, for str concat
+      )+                          # match multiple because we supports concatenating strs
+    )\s*                          # end capturing of plural string argument
+    (?:,\s*' . LOCALE_JS_OBJECT . '\s*          # optionally capture string args
+      (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*)?  # optionally capture context
+    )?
+    [,\)]
+    ~sx', $file, $plural_matches);
+
+  $matches = array();
+
+  // Add strings from Drupal.t().
+  foreach ($t_matches[1] as $key => $string) {
+    $matches[] = array(
+      'string'  => $string,
+      'context' => $t_matches[2][$key],
+    );
+  }
+
+  // Add string from Drupal.formatPlural().
+  foreach ($plural_matches[1] as $key => $string) {
+    $matches[] = array(
+      'string'  => $string,
+      'context' => $plural_matches[3][$key],
+    );
+
+    // If there is also a plural version of this string, add it to the strings array.
+    if (isset($plural_matches[2][$key])) {
+      $matches[] = array(
+        'string'  => $plural_matches[2][$key],
+        'context' => $plural_matches[3][$key],
+      );
+    }
+  }
+
+  foreach ($matches as $key => $match) {
+    // Remove the quotes and string concatenations from the string.
+    $string = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['string'], 1, -1)));
+    $context = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['context'], 1, -1)));
+
+    $source = db_query("SELECT lid, location FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = 'default'", array(':source' => $string, ':context' => $context))->fetchObject();
+    if ($source) {
+      // We already have this source string and now have to add the location
+      // to the location column, if this file is not yet present in there.
+      $locations = preg_split('~\s*;\s*~', $source->location);
+
+      if (!in_array($filepath, $locations)) {
+        $locations[] = $filepath;
+        $locations = implode('; ', $locations);
+
+        // Save the new locations string to the database.
+        db_update('locales_source')
+          ->fields(array(
+            'location' => $locations,
+          ))
+          ->condition('lid', $source->lid)
+          ->execute();
+      }
+    }
+    else {
+      // We don't have the source string yet, thus we insert it into the database.
+      db_insert('locales_source')
+        ->fields(array(
+          'location' => $filepath,
+          'source' => $string,
+          'context' => $context,
+          'textgroup' => 'default',
+        ))
+        ->execute();
+    }
+  }
+}
+
+/**
+ * @addtogroup locale-api-import-export
+ * @{
+ */
+
+/**
+ * Generates a structured array of all strings with translations in
+ * $language, if given. This array can be used to generate an export
+ * of the string in the database.
+ *
+ * @param $language
+ *   Language object to generate the output for, or NULL if generating
+ *   translation template.
+ * @param $group
+ *   Text group to export PO file from (eg. 'default' for interface
+ *   translations).
+ */
+function _locale_export_get_strings($language = NULL, $group = 'default') {
+  if (isset($language)) {
+    $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':language' => $language->language, ':textgroup' => $group));
+  }
+  else {
+    $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':textgroup' => $group));
+  }
+  $strings = array();
+  foreach ($result as $child) {
+    $string = array(
+      'comment'     => $child->location,
+      'source'      => $child->source,
+      'context'     => $child->context,
+      'translation' => isset($child->translation) ? $child->translation : '',
+    );
+    if ($child->plid) {
+      // Has a parent lid. Since we process in the order of plids,
+      // we already have the parent in the array, so we can add the
+      // lid to the next plural version to it. This builds a linked
+      // list of plurals.
+      $string['child'] = TRUE;
+      $strings[$child->plid]['plural'] = $child->lid;
+    }
+    $strings[$child->lid] = $string;
+  }
+  return $strings;
+}
+
+/**
+ * Generates the PO(T) file contents for given strings.
+ *
+ * @param $language
+ *   Language object to generate the output for, or NULL if generating
+ *   translation template.
+ * @param $strings
+ *   Array of strings to export. See _locale_export_get_strings()
+ *   on how it should be formatted.
+ * @param $header
+ *   The header portion to use for the output file. Defaults
+ *   are provided for PO and POT files.
+ */
+function _locale_export_po_generate($language = NULL, $strings = array(), $header = NULL) {
+  global $user;
+
+  if (!isset($header)) {
+    if (isset($language)) {
+      $header = '# ' . $language->name . ' translation of ' . variable_get('site_name', 'Drupal') . "\n";
+      $header .= '# Generated by ' . $user->name . ' <' . $user->mail . ">\n";
+      $header .= "#\n";
+      $header .= "msgid \"\"\n";
+      $header .= "msgstr \"\"\n";
+      $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
+      $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
+      $header .= "\"PO-Revision-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
+      $header .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
+      $header .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
+      $header .= "\"MIME-Version: 1.0\\n\"\n";
+      $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
+      $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
+      if ($language->formula && $language->plurals) {
+        $header .= "\"Plural-Forms: nplurals=" . $language->plurals . "; plural=" . strtr($language->formula, array('$' => '')) . ";\\n\"\n";
+      }
+    }
+    else {
+      $header = "# LANGUAGE translation of PROJECT\n";
+      $header .= "# Copyright (c) YEAR NAME <EMAIL@ADDRESS>\n";
+      $header .= "#\n";
+      $header .= "msgid \"\"\n";
+      $header .= "msgstr \"\"\n";
+      $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
+      $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
+      $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n";
+      $header .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
+      $header .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
+      $header .= "\"MIME-Version: 1.0\\n\"\n";
+      $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
+      $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
+      $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n";
+    }
+  }
+
+  $output = $header . "\n";
+
+  foreach ($strings as $lid => $string) {
+    // Only process non-children, children are output below their parent.
+    if (!isset($string['child'])) {
+      if ($string['comment']) {
+        $output .= '#: ' . $string['comment'] . "\n";
+      }
+      if (!empty($string['context'])) {
+        $output .= 'msgctxt ' . _locale_export_string($string['context']);
+      }
+      $output .= 'msgid ' . _locale_export_string($string['source']);
+      if (!empty($string['plural'])) {
+        $plural = $string['plural'];
+        $output .= 'msgid_plural ' . _locale_export_string($strings[$plural]['source']);
+        if (isset($language)) {
+          $translation = $string['translation'];
+          for ($i = 0; $i < $language->plurals; $i++) {
+            $output .= 'msgstr[' . $i . '] ' . _locale_export_string($translation);
+            if ($plural) {
+              $translation = _locale_export_remove_plural($strings[$plural]['translation']);
+              $plural = isset($strings[$plural]['plural']) ? $strings[$plural]['plural'] : 0;
+            }
+            else {
+              $translation = '';
+            }
+          }
+        }
+        else {
+          $output .= 'msgstr[0] ""' . "\n";
+          $output .= 'msgstr[1] ""' . "\n";
+        }
+      }
+      else {
+        $output .= 'msgstr ' . _locale_export_string($string['translation']);
+      }
+      $output .= "\n";
+    }
+  }
+  return $output;
+}
+
+/**
+ * Write a generated PO or POT file to the output.
+ *
+ * @param $language
+ *   Language object to generate the output for, or NULL if generating
+ *   translation template.
+ * @param $output
+ *   The PO(T) file to output as a string. See _locale_export_generate_po()
+ *   on how it can be generated.
+ */
+function _locale_export_po($language = NULL, $output = NULL) {
+  // Log the export event.
+  if (isset($language)) {
+    $filename = $language->language . '.po';
+    watchdog('locale', 'Exported %locale translation file: %filename.', array('%locale' => $language->name, '%filename' => $filename));
+  }
+  else {
+    $filename = 'drupal.pot';
+    watchdog('locale', 'Exported translation file: %filename.', array('%filename' => $filename));
+  }
+  // Download the file for the client.
+  header("Content-Disposition: attachment; filename=$filename");
+  header("Content-Type: text/plain; charset=utf-8");
+  print $output;
+  drupal_exit();
+}
+
+/**
+ * Print out a string on multiple lines
+ */
+function _locale_export_string($str) {
+  $stri = addcslashes($str, "\0..\37\\\"");
+  $parts = array();
+
+  // Cut text into several lines
+  while ($stri != "") {
+    $i = strpos($stri, "\\n");
+    if ($i === FALSE) {
+      $curstr = $stri;
+      $stri = "";
+    }
+    else {
+      $curstr = substr($stri, 0, $i + 2);
+      $stri = substr($stri, $i + 2);
+    }
+    $curparts = explode("\n", _locale_export_wrap($curstr, 70));
+    $parts = array_merge($parts, $curparts);
+  }
+
+  // Multiline string
+  if (count($parts) > 1) {
+    return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n";
+  }
+  // Single line string
+  elseif (count($parts) == 1) {
+    return "\"$parts[0]\"\n";
+  }
+  // No translation
+  else {
+    return "\"\"\n";
+  }
+}
+
+/**
+ * Custom word wrapping for Portable Object (Template) files.
+ */
+function _locale_export_wrap($str, $len) {
+  $words = explode(' ', $str);
+  $return = array();
+
+  $cur = "";
+  $nstr = 1;
+  while (count($words)) {
+    $word = array_shift($words);
+    if ($nstr) {
+      $cur = $word;
+      $nstr = 0;
+    }
+    elseif (strlen("$cur $word") > $len) {
+      $return[] = $cur . " ";
+      $cur = $word;
+    }
+    else {
+      $cur = "$cur $word";
+    }
+  }
+  $return[] = $cur;
+
+  return implode("\n", $return);
+}
+
+/**
+ * Removes plural index information from a string
+ */
+function _locale_export_remove_plural($entry) {
+  return preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
+}
+/**
+ * @} End of "locale-api-import-export"
+ */
+
+/**
+ * @defgroup locale-api-seek Translation search API
+ * @{
+ * Functions to search in translation files.
+ *
+ * These functions provide the functionality to search for specific
+ * translations.
+ */
+
+/**
+ * Perform a string search and display results in a table
+ */
+function _locale_translate_seek() {
+  $output = '';
+
+  // We have at least one criterion to match
+  if (!($query = _locale_translate_seek_query())) {
+    $query = array(
+      'translation' => 'all',
+      'group' => 'all',
+      'language' => 'all',
+      'string' => '',
+    );
+  }
+
+  $sql_query = db_select('locales_source', 's');
+
+  $limit_language = NULL;
+  if ($query['language'] != 'en' && $query['language'] != 'all') {
+    $sql_query->leftJoin('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(':langcode' => $query['language']));
+    $limit_language = $query['language'];
+  }
+  else {
+    $sql_query->leftJoin('locales_target', 't', 't.lid = s.lid');
+  }
+
+  $sql_query->fields('s', array('source', 'location', 'context', 'lid', 'textgroup'));
+  $sql_query->fields('t', array('translation', 'language'));
+
+  // Compute LIKE section.
+  switch ($query['translation']) {
+    case 'translated':
+      $sql_query->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE');
+      $sql_query->orderBy('t.translation', 'DESC');
+      break;
+    case 'untranslated':
+      $sql_query->condition(db_and()
+        ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE')
+        ->isNull('t.translation')
+      );
+      $sql_query->orderBy('s.source');
+      break;
+    case 'all' :
+    default:
+      $condition = db_or()
+        ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE');
+      if ($query['language'] != 'en') {
+        // Only search in translations if the language is not forced to English.
+        $condition->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE');
+      }
+      $sql_query->condition($condition);
+      break;
+  }
+
+  // Add a condition on the text group.
+  if (!empty($query['group']) && $query['group'] != 'all') {
+    $sql_query->condition('s.textgroup', $query['group']);
+  }
+
+  $sql_query = $sql_query->extend('PagerDefault')->limit(50);
+  $locales = $sql_query->execute();
+
+  $groups = module_invoke_all('locale', 'groups');
+  $header = array(t('Text group'), t('String'), t('Context'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2'));
+
+  $strings = array();
+  foreach ($locales as $locale) {
+    if (!isset($strings[$locale->lid])) {
+      $strings[$locale->lid] = array(
+        'group' => $locale->textgroup,
+        'languages' => array(),
+        'location' => $locale->location,
+        'source' => $locale->source,
+        'context' => $locale->context,
+      );
+    }
+    if (isset($locale->language)) {
+      $strings[$locale->lid]['languages'][$locale->language] = $locale->translation;
+    }
+  }
+
+  $rows = array();
+  foreach ($strings as $lid => $string) {
+    $rows[] = array(
+      $groups[$string['group']],
+      array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
+      $string['context'],
+      array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'),
+      array('data' => l(t('edit'), "admin/config/regional/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')),
+      array('data' => l(t('delete'), "admin/config/regional/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')),
+    );
+  }
+
+  $output .= theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('No strings available.')));
+  $output .= theme('pager');
+
+  return $output;
+}
+
+/**
+ * Build array out of search criteria specified in request variables
+ */
+function _locale_translate_seek_query() {
+  $query = &drupal_static(__FUNCTION__);
+  if (!isset($query)) {
+    $query = array();
+    $fields = array('string', 'language', 'translation', 'group');
+    foreach ($fields as $field) {
+      if (isset($_SESSION['locale_translation_filter'][$field])) {
+        $query[$field] = $_SESSION['locale_translation_filter'][$field];
+      }
+    }
+  }
+  return $query;
+}
+
+/**
+ * Force the JavaScript translation file(s) to be refreshed.
+ *
+ * This function sets a refresh flag for a specified language, or all
+ * languages except English, if none specified. JavaScript translation
+ * files are rebuilt (with locale_update_js_files()) the next time a
+ * request is served in that language.
+ *
+ * @param $langcode
+ *   The language code for which the file needs to be refreshed.
+ *
+ * @return
+ *   New content of the 'javascript_parsed' variable.
+ */
+function _locale_invalidate_js($langcode = NULL) {
+  $parsed = variable_get('javascript_parsed', array());
+
+  if (empty($langcode)) {
+    // Invalidate all languages.
+    $languages = language_list();
+    unset($languages['en']);
+    foreach ($languages as $lcode => $data) {
+      $parsed['refresh:' . $lcode] = 'waiting';
+    }
+  }
+  else {
+    // Invalidate single language.
+    $parsed['refresh:' . $langcode] = 'waiting';
+  }
+
+  variable_set('javascript_parsed', $parsed);
+  return $parsed;
+}
+
+/**
+ * (Re-)Creates the JavaScript translation file for a language.
+ *
+ * @param $language
+ *   The language, the translation file should be (re)created for.
+ */
+function _locale_rebuild_js($langcode = NULL) {
+  if (!isset($langcode)) {
+    global $language;
+  }
+  else {
+    // Get information about the locale.
+    $languages = language_list();
+    $language = $languages[$langcode];
+  }
+
+  // Construct the array for JavaScript translations.
+  // Only add strings with a translation to the translations array.
+  $result = db_query("SELECT s.lid, s.source, s.context, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%' AND s.textgroup = :textgroup", array(':language' => $language->language, ':textgroup' => 'default'));
+
+  $translations = array();
+  foreach ($result as $data) {
+    $translations[$data->context][$data->source] = $data->translation;
+  }
+
+  // Construct the JavaScript file, if there are translations.
+  $data_hash = NULL;
+  $data = $status = '';
+  if (!empty($translations)) {
+
+    $data = "Drupal.locale = { ";
+
+    if (!empty($language->formula)) {
+      $data .= "'pluralFormula': function (\$n) { return Number({$language->formula}); }, ";
+    }
+
+    $data .= "'strings': " . drupal_json_encode($translations) . " };";
+    $data_hash = drupal_hash_base64($data);
+  }
+
+  // Construct the filepath where JS translation files are stored.
+  // There is (on purpose) no front end to edit that variable.
+  $dir = 'public://' . variable_get('locale_js_directory', 'languages');
+
+  // Delete old file, if we have no translations anymore, or a different file to be saved.
+  $changed_hash = $language->javascript != $data_hash;
+  if (!empty($language->javascript) && (!$data || $changed_hash)) {
+    file_unmanaged_delete($dir . '/' . $language->language . '_' . $language->javascript . '.js');
+    $language->javascript = '';
+    $status = 'deleted';
+  }
+
+  // Only create a new file if the content has changed or the original file got
+  // lost.
+  $dest = $dir . '/' . $language->language . '_' . $data_hash . '.js';
+  if ($data && ($changed_hash || !file_exists($dest))) {
+    // Ensure that the directory exists and is writable, if possible.
+    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
+
+    // Save the file.
+    if (file_unmanaged_save_data($data, $dest)) {
+      $language->javascript = $data_hash;
+      // If we deleted a previous version of the file and we replace it with a
+      // new one we have an update.
+      if ($status == 'deleted') {
+        $status = 'updated';
+      }
+      // If the file did not exist previously and the data has changed we have
+      // a fresh creation.
+      elseif ($changed_hash) {
+        $status = 'created';
+      }
+      // If the data hash is unchanged the translation was lost and has to be
+      // rebuilt.
+      else {
+        $status = 'rebuilt';
+      }
+    }
+    else {
+      $language->javascript = '';
+      $status = 'error';
+    }
+  }
+
+  // Save the new JavaScript hash (or an empty value if the file just got
+  // deleted). Act only if some operation was executed that changed the hash
+  // code.
+  if ($status && $changed_hash) {
+    db_update('languages')
+      ->fields(array(
+        'javascript' => $language->javascript,
+      ))
+      ->condition('language', $language->language)
+      ->execute();
+
+    // Update the default language variable if the default language has been altered.
+    // This is necessary to keep the variable consistent with the database
+    // version of the language and to prevent checking against an outdated hash.
+    $default_langcode = language_default('language');
+    if ($default_langcode == $language->language) {
+      $default = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $default_langcode))->fetchObject();
+      variable_set('language_default', $default);
+    }
+  }
+
+  // Log the operation and return success flag.
+  switch ($status) {
+    case 'updated':
+      watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => t($language->name)));
+      return TRUE;
+    case 'rebuilt':
+      watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $language->javascript), WATCHDOG_WARNING);
+      // Proceed to the 'created' case as the JavaScript translation file has
+      // been created again.
+    case 'created':
+      watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => t($language->name)));
+      return TRUE;
+    case 'deleted':
+      watchdog('locale', 'Removed JavaScript translation file for the language %language, because no translations currently exist for that language.', array('%language' => t($language->name)));
+      return TRUE;
+    case 'error':
+      watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => t($language->name)), WATCHDOG_ERROR);
+      return FALSE;
+    default:
+      // No operation needed.
+      return TRUE;
+  }
+}
+
+/**
+ * List languages in search result table
+ */
+function _locale_translate_language_list($translation, $limit_language) {
+  // Add CSS.
+  drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
+
+  $languages = language_list();
+  unset($languages['en']);
+  $output = '';
+  foreach ($languages as $langcode => $language) {
+    if (!$limit_language || $limit_language == $langcode) {
+      $output .= (!empty($translation[$langcode])) ? $langcode . ' ' : "<em class=\"locale-untranslated\">$langcode</em> ";
+    }
+  }
+
+  return $output;
+}
+/**
+ * @} End of "locale-api-seek"
+ */
+
+/**
+ * @defgroup locale-api-predefined List of predefined languages
+ * @{
+ * API to provide a list of predefined languages.
+ */
+
+/**
+ * Prepares the language code list for a select form item with only the unsupported ones
+ */
+function _locale_prepare_predefined_list() {
+  include_once DRUPAL_ROOT . '/includes/iso.inc';
+  $languages = language_list();
+  $predefined = _locale_get_predefined_list();
+  foreach ($predefined as $key => $value) {
+    if (isset($languages[$key])) {
+      unset($predefined[$key]);
+      continue;
+    }
+    // Include native name in output, if possible
+    if (count($value) > 1) {
+      $tname = t($value[0]);
+      $predefined[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])";
+    }
+    else {
+      $predefined[$key] = t($value[0]);
+    }
+  }
+  asort($predefined);
+  return $predefined;
+}
+
+/**
+ * @} End of "locale-api-languages-predefined"
+ */
+
+/**
+ * @defgroup locale-autoimport Automatic interface translation import
+ * @{
+ * Functions to create batches for importing translations.
+ *
+ * These functions can be used to import translations for installed
+ * modules.
+ */
+
+/**
+ * Prepare a batch to import translations for all enabled
+ * modules in a given language.
+ *
+ * @param $langcode
+ *   Language code to import translations for.
+ * @param $finished
+ *   Optional finished callback for the batch.
+ * @param $skip
+ *   Array of component names to skip. Used in the installer for the
+ *   second pass import, when most components are already imported.
+ *
+ * @return
+ *   A batch structure or FALSE if no files found.
+ */
+function locale_batch_by_language($langcode, $finished = NULL, $skip = array()) {
+  // Collect all files to import for all enabled modules and themes.
+  $files = array();
+  $components = array();
+  $query = db_select('system', 's');
+  $query->fields('s', array('name', 'filename'));
+  $query->condition('s.status', 1);
+  if (count($skip)) {
+    $query->condition('name', $skip, 'NOT IN');
+  }
+  $result = $query->execute();
+  foreach ($result as $component) {
+    // Collect all files for all components, names as $langcode.po or
+    // with names ending with $langcode.po. This allows for filenames
+    // like node-module.de.po to let translators use small files and
+    // be able to import in smaller chunks.
+    $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)' . $langcode . '\.po$/', array('recurse' => FALSE)));
+    $components[] = $component->name;
+  }
+
+  return _locale_batch_build($files, $finished, $components);
+}
+
+/**
+ * Prepare a batch to run when installing modules or enabling themes.
+ *
+ * This batch will import translations for the newly added components
+ * in all the languages already set up on the site.
+ *
+ * @param $components
+ *   An array of component (theme and/or module) names to import
+ *   translations for.
+ * @param $finished
+ *   Optional finished callback for the batch.
+ */
+function locale_batch_by_component($components, $finished = '_locale_batch_system_finished') {
+  $files = array();
+  $languages = language_list('enabled');
+  unset($languages[1]['en']);
+  if (count($languages[1])) {
+    $language_list = join('|', array_keys($languages[1]));
+    // Collect all files to import for all $components.
+    $result = db_query("SELECT name, filename FROM {system} WHERE status = 1");
+    foreach ($result as $component) {
+      if (in_array($component->name, $components)) {
+        // Collect all files for this component in all enabled languages, named
+        // as $langcode.po or with names ending with $langcode.po. This allows
+        // for filenames like node-module.de.po to let translators use small
+        // files and be able to import in smaller chunks.
+        $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)(' . $language_list . ')\.po$/', array('recurse' => FALSE)));
+      }
+    }
+    return _locale_batch_build($files, $finished);
+  }
+  return FALSE;
+}
+
+/**
+ * Build a locale batch from an array of files.
+ *
+ * @param $files
+ *   Array of files to import.
+ * @param $finished
+ *   Optional finished callback for the batch.
+ * @param $components
+ *   Optional list of component names the batch covers. Used in the installer.
+ *
+ * @return
+ *   A batch structure.
+ */
+function _locale_batch_build($files, $finished = NULL, $components = array()) {
+  $t = get_t();
+  if (count($files)) {
+    $operations = array();
+    foreach ($files as $file) {
+      // We call _locale_batch_import for every batch operation.
+      $operations[] = array('_locale_batch_import', array($file->uri));
+    }
+    $batch = array(
+      'operations'    => $operations,
+      'title'         => $t('Importing interface translations'),
+      'init_message'  => $t('Starting import'),
+      'error_message' => $t('Error importing interface translations'),
+      'file'          => 'includes/locale.inc',
+      // This is not a batch API construct, but data passed along to the
+      // installer, so we know what did we import already.
+      '#components'   => $components,
+    );
+    if (isset($finished)) {
+      $batch['finished'] = $finished;
+    }
+    return $batch;
+  }
+  return FALSE;
+}
+
+/**
+ * Perform interface translation import as a batch step.
+ *
+ * @param $filepath
+ *   Path to a file to import.
+ * @param $results
+ *   Contains a list of files imported.
+ */
+function _locale_batch_import($filepath, &$context) {
+  // The filename is either {langcode}.po or {prefix}.{langcode}.po, so
+  // we can extract the language code to use for the import from the end.
+  if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) {
+    $file = (object) array('filename' => drupal_basename($filepath), 'uri' => $filepath);
+    _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]);
+    $context['results'][] = $filepath;
+  }
+}
+
+/**
+ * Finished callback of system page locale import batch.
+ * Inform the user of translation files imported.
+ */
+function _locale_batch_system_finished($success, $results) {
+  if ($success) {
+    drupal_set_message(format_plural(count($results), 'One translation file imported for the newly installed modules.', '@count translation files imported for the newly installed modules.'));
+  }
+}
+
+/**
+ * Finished callback of language addition locale import batch.
+ * Inform the user of translation files imported.
+ */
+function _locale_batch_language_finished($success, $results) {
+  if ($success) {
+    drupal_set_message(format_plural(count($results), 'One translation file imported for the enabled modules.', '@count translation files imported for the enabled modules.'));
+  }
+}
+
+/**
+ * @} End of "locale-autoimport"
+ */
+
+/**
+ * Get list of all predefined and custom countries.
+ *
+ * @return
+ *   An array of all country code => country name pairs.
+ */
+function country_get_list() {
+  include_once DRUPAL_ROOT . '/includes/iso.inc';
+  $countries = _country_get_predefined_list();
+  // Allow other modules to modify the country list.
+  drupal_alter('countries', $countries);
+  return $countries;
+}
+
+/**
+ * Save locale specific date formats to the database.
+ *
+ * @param $langcode
+ *   Language code, can be 2 characters, e.g. 'en' or 5 characters, e.g.
+ *   'en-CA'.
+ * @param $type
+ *   Date format type, e.g. 'short', 'medium'.
+ * @param $format
+ *   The date format string.
+ */
+function locale_date_format_save($langcode, $type, $format) {
+  $locale_format = array();
+  $locale_format['language'] = $langcode;
+  $locale_format['type'] = $type;
+  $locale_format['format'] = $format;
+
+  $is_existing = (bool) db_query_range('SELECT 1 FROM {date_format_locale} WHERE language = :langcode AND type = :type', 0, 1, array(':langcode' => $langcode, ':type' => $type))->fetchField();
+  if ($is_existing) {
+    $keys = array('type', 'language');
+    drupal_write_record('date_format_locale', $locale_format, $keys);
+  }
+  else {
+    drupal_write_record('date_format_locale', $locale_format);
+  }
+}
+
+/**
+ * Select locale date format details from database.
+ *
+ * @param $languages
+ *   An array of language codes.
+ *
+ * @return
+ *   An array of date formats.
+ */
+function locale_get_localized_date_format($languages) {
+  $formats = array();
+
+  // Get list of different format types.
+  $format_types = system_get_date_types();
+  $short_default = variable_get('date_format_short', 'm/d/Y - H:i');
+
+  // Loop through each language until we find one with some date formats
+  // configured.
+  foreach ($languages as $language) {
+    $date_formats = system_date_format_locale($language);
+    if (!empty($date_formats)) {
+      // We have locale-specific date formats, so check for their types. If
+      // we're missing a type, use the default setting instead.
+      foreach ($format_types as $type => $type_info) {
+        // If format exists for this language, use it.
+        if (!empty($date_formats[$type])) {
+          $formats['date_format_' . $type] = $date_formats[$type];
+        }
+        // Otherwise get default variable setting. If this is not set, default
+        // to the short format.
+        else {
+          $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
+        }
+      }
+
+      // Return on the first match.
+      return $formats;
+    }
+  }
+
+  // No locale specific formats found, so use defaults.
+  $system_types = array('short', 'medium', 'long');
+  // Handle system types separately as they have defaults if no variable exists.
+  $formats['date_format_short'] = $short_default;
+  $formats['date_format_medium'] = variable_get('date_format_medium', 'D, m/d/Y - H:i');
+  $formats['date_format_long'] = variable_get('date_format_long', 'l, F j, Y - H:i');
+
+  // For non-system types, get the default setting, otherwise use the short
+  // format.
+  foreach ($format_types as $type => $type_info) {
+    if (!in_array($type, $system_types)) {
+      $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
+    }
+  }
+
+  return $formats;
+}

+ 274 - 0
includes/lock.inc

@@ -0,0 +1,274 @@
+<?php
+
+/**
+ * @file
+ * A database-mediated implementation of a locking mechanism.
+ */
+
+/**
+ * @defgroup lock Locking mechanisms
+ * @{
+ * Functions to coordinate long-running operations across requests.
+ *
+ * In most environments, multiple Drupal page requests (a.k.a. threads or
+ * processes) will execute in parallel. This leads to potential conflicts or
+ * race conditions when two requests execute the same code at the same time. A
+ * common example of this is a rebuild like menu_rebuild() where we invoke many
+ * hook implementations to get and process data from all active modules, and
+ * then delete the current data in the database to insert the new afterwards.
+ *
+ * This is a cooperative, advisory lock system. Any long-running operation
+ * that could potentially be attempted in parallel by multiple requests should
+ * try to acquire a lock before proceeding. By obtaining a lock, one request
+ * notifies any other requests that a specific operation is in progress which
+ * must not be executed in parallel.
+ *
+ * To use this API, pick a unique name for the lock. A sensible choice is the
+ * name of the function performing the operation. A very simple example use of
+ * this API:
+ * @code
+ * function mymodule_long_operation() {
+ *   if (lock_acquire('mymodule_long_operation')) {
+ *     // Do the long operation here.
+ *     // ...
+ *     lock_release('mymodule_long_operation');
+ *   }
+ * }
+ * @endcode
+ *
+ * If a function acquires a lock it should always release it when the
+ * operation is complete by calling lock_release(), as in the example.
+ *
+ * A function that has acquired a lock may attempt to renew a lock (extend the
+ * duration of the lock) by calling lock_acquire() again during the operation.
+ * Failure to renew a lock is indicative that another request has acquired
+ * the lock, and that the current operation may need to be aborted.
+ *
+ * If a function fails to acquire a lock it may either immediately return, or
+ * it may call lock_wait() if the rest of the current page request requires
+ * that the operation in question be complete. After lock_wait() returns,
+ * the function may again attempt to acquire the lock, or may simply allow the
+ * page request to proceed on the assumption that a parallel request completed
+ * the operation.
+ *
+ * lock_acquire() and lock_wait() will automatically break (delete) a lock
+ * whose duration has exceeded the timeout specified when it was acquired.
+ *
+ * Alternative implementations of this API (such as APC) may be substituted
+ * by setting the 'lock_inc' variable to an alternate include filepath. Since
+ * this is an API intended to support alternative implementations, code using
+ * this API should never rely upon specific implementation details (for example
+ * no code should look for or directly modify a lock in the {semaphore} table).
+ */
+
+/**
+ * Initialize the locking system.
+ */
+function lock_initialize() {
+  global $locks;
+
+  $locks = array();
+}
+
+/**
+ * Helper function to get this request's unique id.
+ */
+function _lock_id() {
+  // Do not use drupal_static(). This identifier refers to the current
+  // client request, and must not be changed under any circumstances
+  // else the shutdown handler may fail to release our locks.
+  static $lock_id;
+
+  if (!isset($lock_id)) {
+    // Assign a unique id.
+    $lock_id = uniqid(mt_rand(), TRUE);
+    // We only register a shutdown function if a lock is used.
+    drupal_register_shutdown_function('lock_release_all', $lock_id);
+  }
+  return $lock_id;
+}
+
+/**
+ * Acquire (or renew) a lock, but do not block if it fails.
+ *
+ * @param $name
+ *   The name of the lock.
+ * @param $timeout
+ *   A number of seconds (float) before the lock expires (minimum of 0.001).
+ *
+ * @return
+ *   TRUE if the lock was acquired, FALSE if it failed.
+ */
+function lock_acquire($name, $timeout = 30.0) {
+  global $locks;
+
+  // Insure that the timeout is at least 1 ms.
+  $timeout = max($timeout, 0.001);
+  $expire = microtime(TRUE) + $timeout;
+  if (isset($locks[$name])) {
+    // Try to extend the expiration of a lock we already acquired.
+    $success = (bool) db_update('semaphore')
+      ->fields(array('expire' => $expire))
+      ->condition('name', $name)
+      ->condition('value', _lock_id())
+      ->execute();
+    if (!$success) {
+      // The lock was broken.
+      unset($locks[$name]);
+    }
+    return $success;
+  }
+  else {
+    // Optimistically try to acquire the lock, then retry once if it fails.
+    // The first time through the loop cannot be a retry.
+    $retry = FALSE;
+    // We always want to do this code at least once.
+    do {
+      try {
+        db_insert('semaphore')
+          ->fields(array(
+            'name' => $name,
+            'value' => _lock_id(),
+            'expire' => $expire,
+          ))
+          ->execute();
+        // We track all acquired locks in the global variable.
+        $locks[$name] = TRUE;
+        // We never need to try again.
+        $retry = FALSE;
+      }
+      catch (PDOException $e) {
+        // Suppress the error. If this is our first pass through the loop,
+        // then $retry is FALSE. In this case, the insert must have failed
+        // meaning some other request acquired the lock but did not release it.
+        // We decide whether to retry by checking lock_may_be_available()
+        // Since this will break the lock in case it is expired.
+        $retry = $retry ? FALSE : lock_may_be_available($name);
+      }
+      // We only retry in case the first attempt failed, but we then broke
+      // an expired lock.
+    } while ($retry);
+  }
+  return isset($locks[$name]);
+}
+
+/**
+ * Check if lock acquired by a different process may be available.
+ *
+ * If an existing lock has expired, it is removed.
+ *
+ * @param $name
+ *   The name of the lock.
+ *
+ * @return
+ *   TRUE if there is no lock or it was removed, FALSE otherwise.
+ */
+function lock_may_be_available($name) {
+  $lock = db_query('SELECT expire, value FROM {semaphore} WHERE name = :name', array(':name' => $name))->fetchAssoc();
+  if (!$lock) {
+    return TRUE;
+  }
+  $expire = (float) $lock['expire'];
+  $now = microtime(TRUE);
+  if ($now > $expire) {
+    // We check two conditions to prevent a race condition where another
+    // request acquired the lock and set a new expire time. We add a small
+    // number to $expire to avoid errors with float to string conversion.
+    return (bool) db_delete('semaphore')
+      ->condition('name', $name)
+      ->condition('value', $lock['value'])
+      ->condition('expire', 0.0001 + $expire, '<=')
+      ->execute();
+  }
+  return FALSE;
+}
+
+/**
+ * Wait for a lock to be available.
+ *
+ * This function may be called in a request that fails to acquire a desired
+ * lock. This will block further execution until the lock is available or the
+ * specified delay in seconds is reached. This should not be used with locks
+ * that are acquired very frequently, since the lock is likely to be acquired
+ * again by a different request while waiting.
+ *
+ * @param $name
+ *   The name of the lock.
+ * @param $delay
+ *   The maximum number of seconds to wait, as an integer.
+ *
+ * @return
+ *   TRUE if the lock holds, FALSE if it is available.
+ */
+function lock_wait($name, $delay = 30) {
+  // Pause the process for short periods between calling
+  // lock_may_be_available(). This prevents hitting the database with constant
+  // database queries while waiting, which could lead to performance issues.
+  // However, if the wait period is too long, there is the potential for a
+  // large number of processes to be blocked waiting for a lock, especially
+  // if the item being rebuilt is commonly requested. To address both of these
+  // concerns, begin waiting for 25ms, then add 25ms to the wait period each
+  // time until it reaches 500ms. After this point polling will continue every
+  // 500ms until $delay is reached.
+
+  // $delay is passed in seconds, but we will be using usleep(), which takes
+  // microseconds as a parameter. Multiply it by 1 million so that all
+  // further numbers are equivalent.
+  $delay = (int) $delay * 1000000;
+
+  // Begin sleeping at 25ms.
+  $sleep = 25000;
+  while ($delay > 0) {
+    // This function should only be called by a request that failed to get a
+    // lock, so we sleep first to give the parallel request a chance to finish
+    // and release the lock.
+    usleep($sleep);
+    // After each sleep, increase the value of $sleep until it reaches
+    // 500ms, to reduce the potential for a lock stampede.
+    $delay = $delay - $sleep;
+    $sleep = min(500000, $sleep + 25000, $delay);
+    if (lock_may_be_available($name)) {
+      // No longer need to wait.
+      return FALSE;
+    }
+  }
+  // The caller must still wait longer to get the lock.
+  return TRUE;
+}
+
+/**
+ * Release a lock previously acquired by lock_acquire().
+ *
+ * This will release the named lock if it is still held by the current request.
+ *
+ * @param $name
+ *   The name of the lock.
+ */
+function lock_release($name) {
+  global $locks;
+
+  unset($locks[$name]);
+  db_delete('semaphore')
+    ->condition('name', $name)
+    ->condition('value', _lock_id())
+    ->execute();
+}
+
+/**
+ * Release all previously acquired locks.
+ */
+function lock_release_all($lock_id = NULL) {
+  global $locks;
+
+  $locks = array();
+  if (empty($lock_id)) {
+    $lock_id = _lock_id();
+  }
+  db_delete('semaphore')
+    ->condition('value', $lock_id)
+    ->execute();
+}
+
+/**
+ * @} End of "defgroup lock".
+ */

+ 622 - 0
includes/mail.inc

@@ -0,0 +1,622 @@
+<?php
+
+/**
+ * @file
+ * API functions for processing and sending e-mail.
+ */
+
+/**
+ * Auto-detect appropriate line endings for e-mails.
+ *
+ * $conf['mail_line_endings'] will override this setting.
+ */
+define('MAIL_LINE_ENDINGS', isset($_SERVER['WINDIR']) || strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE ? "\r\n" : "\n");
+
+/**
+ * Composes and optionally sends an e-mail message.
+ *
+ * Sending an e-mail works with defining an e-mail template (subject, text
+ * and possibly e-mail headers) and the replacement values to use in the
+ * appropriate places in the template. Processed e-mail templates are
+ * requested from hook_mail() from the module sending the e-mail. Any module
+ * can modify the composed e-mail message array using hook_mail_alter().
+ * Finally drupal_mail_system()->mail() sends the e-mail, which can
+ * be reused if the exact same composed e-mail is to be sent to multiple
+ * recipients.
+ *
+ * Finding out what language to send the e-mail with needs some consideration.
+ * If you send e-mail to a user, her preferred language should be fine, so
+ * use user_preferred_language(). If you send email based on form values
+ * filled on the page, there are two additional choices if you are not
+ * sending the e-mail to a user on the site. You can either use the language
+ * used to generate the page ($language global variable) or the site default
+ * language. See language_default(). The former is good if sending e-mail to
+ * the person filling the form, the later is good if you send e-mail to an
+ * address previously set up (like contact addresses in a contact form).
+ *
+ * Taking care of always using the proper language is even more important
+ * when sending e-mails in a row to multiple users. Hook_mail() abstracts
+ * whether the mail text comes from an administrator setting or is
+ * static in the source code. It should also deal with common mail tokens,
+ * only receiving $params which are unique to the actual e-mail at hand.
+ *
+ * An example:
+ *
+ * @code
+ *   function example_notify($accounts) {
+ *     foreach ($accounts as $account) {
+ *       $params['account'] = $account;
+ *       // example_mail() will be called based on the first drupal_mail() parameter.
+ *       drupal_mail('example', 'notice', $account->mail, user_preferred_language($account), $params);
+ *     }
+ *   }
+ *
+ *   function example_mail($key, &$message, $params) {
+ *     $data['user'] = $params['account'];
+ *     $options['language'] = $message['language'];
+ *     user_mail_tokens($variables, $data, $options);
+ *     switch($key) {
+ *       case 'notice':
+ *         // If the recipient can receive such notices by instant-message, do
+ *         // not send by email.
+ *         if (example_im_send($key, $message, $params)) {
+ *           $message['send'] = FALSE;
+ *           break;
+ *         }
+ *         $langcode = $message['language']->language;
+ *         $message['subject'] = t('Notification from !site', $variables, array('langcode' => $langcode));
+ *         $message['body'][] = t("Dear !username\n\nThere is new content available on the site.", $variables, array('langcode' => $langcode));
+ *         break;
+ *     }
+ *   }
+ * @endcode
+ *
+ * Another example, which uses drupal_mail() to format a message for sending
+ * later:
+ *
+ * @code
+ *   $params = array('current_conditions' => $data);
+ *   $to = 'user@example.com';
+ *   $message = drupal_mail('example', 'notice', $to, $language, $params, FALSE);
+ *   // Only add to the spool if sending was not canceled.
+ *   if ($message['send']) {
+ *     example_spool_message($message);
+ *   }
+ * @endcode
+ *
+ * @param $module
+ *   A module name to invoke hook_mail() on. The {$module}_mail() hook will be
+ *   called to complete the $message structure which will already contain common
+ *   defaults.
+ * @param $key
+ *   A key to identify the e-mail sent. The final e-mail id for e-mail altering
+ *   will be {$module}_{$key}.
+ * @param $to
+ *   The e-mail address or addresses where the message will be sent to. The
+ *   formatting of this string will be validated with the
+ *   @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink
+ *   Some examples are:
+ *   - user@example.com
+ *   - user@example.com, anotheruser@example.com
+ *   - User <user@example.com>
+ *   - User <user@example.com>, Another User <anotheruser@example.com>
+ * @param $language
+ *   Language object to use to compose the e-mail.
+ * @param $params
+ *   Optional parameters to build the e-mail.
+ * @param $from
+ *   Sets From to this value, if given.
+ * @param $send
+ *   If TRUE, drupal_mail() will call drupal_mail_system()->mail() to deliver
+ *   the message, and store the result in $message['result']. Modules
+ *   implementing hook_mail_alter() may cancel sending by setting
+ *   $message['send'] to FALSE.
+ *
+ * @return
+ *   The $message array structure containing all details of the
+ *   message. If already sent ($send = TRUE), then the 'result' element
+ *   will contain the success indicator of the e-mail, failure being already
+ *   written to the watchdog. (Success means nothing more than the message being
+ *   accepted at php-level, which still doesn't guarantee it to be delivered.)
+ */
+function drupal_mail($module, $key, $to, $language, $params = array(), $from = NULL, $send = TRUE) {
+  $default_from = variable_get('site_mail', ini_get('sendmail_from'));
+
+  // Bundle up the variables into a structured array for altering.
+  $message = array(
+    'id'       => $module . '_' . $key,
+    'module'   => $module,
+    'key'      => $key,
+    'to'       => $to,
+    'from'     => isset($from) ? $from : $default_from,
+    'language' => $language,
+    'params'   => $params,
+    'send'     => TRUE,
+    'subject'  => '',
+    'body'     => array()
+  );
+
+  // Build the default headers
+  $headers = array(
+    'MIME-Version'              => '1.0',
+    'Content-Type'              => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
+    'Content-Transfer-Encoding' => '8Bit',
+    'X-Mailer'                  => 'Drupal'
+  );
+  if ($default_from) {
+    // To prevent e-mail from looking like spam, the addresses in the Sender and
+    // Return-Path headers should have a domain authorized to use the originating
+    // SMTP server.
+    $headers['From'] = $headers['Sender'] = $headers['Return-Path'] = $default_from;
+  }
+  if ($from) {
+    $headers['From'] = $from;
+  }
+  $message['headers'] = $headers;
+
+  // Build the e-mail (get subject and body, allow additional headers) by
+  // invoking hook_mail() on this module. We cannot use module_invoke() as
+  // we need to have $message by reference in hook_mail().
+  if (function_exists($function = $module . '_mail')) {
+    $function($key, $message, $params);
+  }
+
+  // Invoke hook_mail_alter() to allow all modules to alter the resulting e-mail.
+  drupal_alter('mail', $message);
+
+  // Retrieve the responsible implementation for this message.
+  $system = drupal_mail_system($module, $key);
+
+  // Format the message body.
+  $message = $system->format($message);
+
+  // Optionally send e-mail.
+  if ($send) {
+    // The original caller requested sending. Sending was canceled by one or
+    // more hook_mail_alter() implementations. We set 'result' to NULL, because
+    // FALSE indicates an error in sending.
+    if (empty($message['send'])) {
+      $message['result'] = NULL;
+    }
+    // Sending was originally requested and was not canceled.
+    else {
+      $message['result'] = $system->mail($message);
+      // Log errors.
+      if (!$message['result']) {
+        watchdog('mail', 'Error sending e-mail (from %from to %to).', array('%from' => $message['from'], '%to' => $message['to']), WATCHDOG_ERROR);
+        drupal_set_message(t('Unable to send e-mail. Contact the site administrator if the problem persists.'), 'error');
+      }
+    }
+  }
+
+  return $message;
+}
+
+/**
+ * Returns an object that implements the MailSystemInterface interface.
+ *
+ * Allows for one or more custom mail backends to format and send mail messages
+ * composed using drupal_mail().
+ *
+ * An implementation needs to implement the following methods:
+ * - format: Allows to preprocess, format, and postprocess a mail
+ *   message before it is passed to the sending system. By default, all messages
+ *   may contain HTML and are converted to plain-text by the DefaultMailSystem
+ *   implementation. For example, an alternative implementation could override
+ *   the default implementation and additionally sanitize the HTML for usage in
+ *   a MIME-encoded e-mail, but still invoking the DefaultMailSystem
+ *   implementation to generate an alternate plain-text version for sending.
+ * - mail: Sends a message through a custom mail sending engine.
+ *   By default, all messages are sent via PHP's mail() function by the
+ *   DefaultMailSystem implementation.
+ *
+ * The selection of a particular implementation is controlled via the variable
+ * 'mail_system', which is a keyed array.  The default implementation
+ * is the class whose name is the value of 'default-system' key. A more specific
+ * match first to key and then to module will be used in preference to the
+ * default. To specify a different class for all mail sent by one module, set
+ * the class name as the value for the key corresponding to the module name. To
+ * specify a class for a particular message sent by one module, set the class
+ * name as the value for the array key that is the message id, which is
+ * "${module}_${key}".
+ *
+ * For example to debug all mail sent by the user module by logging it to a
+ * file, you might set the variable as something like:
+ *
+ * @code
+ * array(
+ *   'default-system' => 'DefaultMailSystem',
+ *   'user' => 'DevelMailLog',
+ * );
+ * @endcode
+ *
+ * Finally, a different system can be specified for a specific e-mail ID (see
+ * the $key param), such as one of the keys used by the contact module:
+ *
+ * @code
+ * array(
+ *   'default-system' => 'DefaultMailSystem',
+ *   'user' => 'DevelMailLog',
+ *   'contact_page_autoreply' => 'DrupalDevNullMailSend',
+ * );
+ * @endcode
+ *
+ * Other possible uses for system include a mail-sending class that actually
+ * sends (or duplicates) each message to SMS, Twitter, instant message, etc, or
+ * a class that queues up a large number of messages for more efficient bulk
+ * sending or for sending via a remote gateway so as to reduce the load
+ * on the local server.
+ *
+ * @param $module
+ *   The module name which was used by drupal_mail() to invoke hook_mail().
+ * @param $key
+ *   A key to identify the e-mail sent. The final e-mail ID for the e-mail
+ *   alter hook in drupal_mail() would have been {$module}_{$key}.
+ *
+ * @return MailSystemInterface
+ */
+function drupal_mail_system($module, $key) {
+  $instances = &drupal_static(__FUNCTION__, array());
+
+  $id = $module . '_' . $key;
+
+  $configuration = variable_get('mail_system', array('default-system' => 'DefaultMailSystem'));
+
+  // Look for overrides for the default class, starting from the most specific
+  // id, and falling back to the module name.
+  if (isset($configuration[$id])) {
+    $class = $configuration[$id];
+  }
+  elseif (isset($configuration[$module])) {
+    $class = $configuration[$module];
+  }
+  else {
+    $class = $configuration['default-system'];
+  }
+
+  if (empty($instances[$class])) {
+    $interfaces = class_implements($class);
+    if (isset($interfaces['MailSystemInterface'])) {
+      $instances[$class] = new $class();
+    }
+    else {
+      throw new Exception(t('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'MailSystemInterface')));
+    }
+  }
+  return $instances[$class];
+}
+
+/**
+ * An interface for pluggable mail back-ends.
+ */
+interface MailSystemInterface {
+  /**
+   * Format a message composed by drupal_mail() prior sending.
+   *
+   * @param $message
+   *   A message array, as described in hook_mail_alter().
+   *
+   * @return
+   *   The formatted $message.
+   */
+   public function format(array $message);
+
+  /**
+   * Send a message composed by drupal_mail().
+   *
+   * @param $message
+   *   Message array with at least the following elements:
+   *   - id: A unique identifier of the e-mail type. Examples: 'contact_user_copy',
+   *     'user_password_reset'.
+   *   - to: The mail address or addresses where the message will be sent to.
+   *     The formatting of this string will be validated with the
+   *     @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink
+   *     Some examples are:
+   *     - user@example.com
+   *     - user@example.com, anotheruser@example.com
+   *     - User <user@example.com>
+   *     - User <user@example.com>, Another User <anotheruser@example.com>
+   *   - subject: Subject of the e-mail to be sent. This must not contain any
+   *     newline characters, or the mail may not be sent properly.
+   *   - body: Message to be sent. Accepts both CRLF and LF line-endings.
+   *     E-mail bodies must be wrapped. You can use drupal_wrap_mail() for
+   *     smart plain text wrapping.
+   *   - headers: Associative array containing all additional mail headers not
+   *     defined by one of the other parameters.  PHP's mail() looks for Cc and
+   *     Bcc headers and sends the mail to addresses in these headers too.
+   *
+   * @return
+   *   TRUE if the mail was successfully accepted for delivery, otherwise FALSE.
+   */
+   public function mail(array $message);
+}
+
+/**
+ * Performs format=flowed soft wrapping for mail (RFC 3676).
+ *
+ * We use delsp=yes wrapping, but only break non-spaced languages when
+ * absolutely necessary to avoid compatibility issues.
+ *
+ * We deliberately use LF rather than CRLF, see drupal_mail().
+ *
+ * @param $text
+ *   The plain text to process.
+ * @param $indent (optional)
+ *   A string to indent the text with. Only '>' characters are repeated on
+ *   subsequent wrapped lines. Others are replaced by spaces.
+ *
+ * @return
+ *   The content of the email as a string with formatting applied.
+ */
+function drupal_wrap_mail($text, $indent = '') {
+  // Convert CRLF into LF.
+  $text = str_replace("\r", '', $text);
+  // See if soft-wrapping is allowed.
+  $clean_indent = _drupal_html_to_text_clean($indent);
+  $soft = strpos($clean_indent, ' ') === FALSE;
+  // Check if the string has line breaks.
+  if (strpos($text, "\n") !== FALSE) {
+    // Remove trailing spaces to make existing breaks hard.
+    $text = preg_replace('/ +\n/m', "\n", $text);
+    // Wrap each line at the needed width.
+    $lines = explode("\n", $text);
+    array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent)));
+    $text = implode("\n", $lines);
+  }
+  else {
+    // Wrap this line.
+    _drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' => strlen($indent)));
+  }
+  // Empty lines with nothing but spaces.
+  $text = preg_replace('/^ +\n/m', "\n", $text);
+  // Space-stuff special lines.
+  $text = preg_replace('/^(>| |From)/m', ' $1', $text);
+  // Apply indentation. We only include non-'>' indentation on the first line.
+  $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent));
+
+  return $text;
+}
+
+/**
+ * Transforms an HTML string into plain text, preserving its structure.
+ *
+ * The output will be suitable for use as 'format=flowed; delsp=yes' text
+ * (RFC 3676) and can be passed directly to drupal_mail() for sending.
+ *
+ * We deliberately use LF rather than CRLF, see drupal_mail().
+ *
+ * This function provides suitable alternatives for the following tags:
+ * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt>
+ * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr>
+ *
+ * @param $string
+ *   The string to be transformed.
+ * @param $allowed_tags (optional)
+ *   If supplied, a list of tags that will be transformed. If omitted, all
+ *   all supported tags are transformed.
+ *
+ * @return
+ *   The transformed string.
+ */
+function drupal_html_to_text($string, $allowed_tags = NULL) {
+  // Cache list of supported tags.
+  static $supported_tags;
+  if (empty($supported_tags)) {
+    $supported_tags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr');
+  }
+
+  // Make sure only supported tags are kept.
+  $allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags, $allowed_tags) : $supported_tags;
+
+  // Make sure tags, entities and attributes are well-formed and properly nested.
+  $string = _filter_htmlcorrector(filter_xss($string, $allowed_tags));
+
+  // Apply inline styles.
+  $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
+  $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string);
+
+  // Replace inline <a> tags with the text of link and a footnote.
+  // 'See <a href="http://drupal.org">the Drupal site</a>' becomes
+  // 'See the Drupal site [1]' with the URL included as a footnote.
+  _drupal_html_to_mail_urls(NULL, TRUE);
+  $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i';
+  $string = preg_replace_callback($pattern, '_drupal_html_to_mail_urls', $string);
+  $urls = _drupal_html_to_mail_urls();
+  $footnotes = '';
+  if (count($urls)) {
+    $footnotes .= "\n";
+    for ($i = 0, $max = count($urls); $i < $max; $i++) {
+      $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n";
+    }
+  }
+
+  // Split tags from text.
+  $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+  // Note: PHP ensures the array consists of alternating delimiters and literals
+  // and begins and ends with a literal (inserting $null as required).
+
+  $tag = FALSE; // Odd/even counter (tag or no tag)
+  $casing = NULL; // Case conversion function
+  $output = '';
+  $indent = array(); // All current indentation string chunks
+  $lists = array(); // Array of counters for opened lists
+  foreach ($split as $value) {
+    $chunk = NULL; // Holds a string ready to be formatted and output.
+
+    // Process HTML tags (but don't output any literally).
+    if ($tag) {
+      list($tagname) = explode(' ', strtolower($value), 2);
+      switch ($tagname) {
+        // List counters
+        case 'ul':
+          array_unshift($lists, '*');
+          break;
+        case 'ol':
+          array_unshift($lists, 1);
+          break;
+        case '/ul':
+        case '/ol':
+          array_shift($lists);
+          $chunk = ''; // Ensure blank new-line.
+          break;
+
+        // Quotation/list markers, non-fancy headers
+        case 'blockquote':
+          // Format=flowed indentation cannot be mixed with lists.
+          $indent[] = count($lists) ? ' "' : '>';
+          break;
+        case 'li':
+          $indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * ';
+          break;
+        case 'dd':
+          $indent[] = '    ';
+          break;
+        case 'h3':
+          $indent[] = '.... ';
+          break;
+        case 'h4':
+          $indent[] = '.. ';
+          break;
+        case '/blockquote':
+          if (count($lists)) {
+            // Append closing quote for inline quotes (immediately).
+            $output = rtrim($output, "> \n") . "\"\n";
+            $chunk = ''; // Ensure blank new-line.
+          }
+          // Fall-through
+        case '/li':
+        case '/dd':
+          array_pop($indent);
+          break;
+        case '/h3':
+        case '/h4':
+          array_pop($indent);
+        case '/h5':
+        case '/h6':
+          $chunk = ''; // Ensure blank new-line.
+          break;
+
+        // Fancy headers
+        case 'h1':
+          $indent[] = '======== ';
+          $casing = 'drupal_strtoupper';
+          break;
+        case 'h2':
+          $indent[] = '-------- ';
+          $casing = 'drupal_strtoupper';
+          break;
+        case '/h1':
+        case '/h2':
+          $casing = NULL;
+          // Pad the line with dashes.
+          $output = _drupal_html_to_text_pad($output, ($tagname == '/h1') ? '=' : '-', ' ');
+          array_pop($indent);
+          $chunk = ''; // Ensure blank new-line.
+          break;
+
+        // Horizontal rulers
+        case 'hr':
+          // Insert immediately.
+          $output .= drupal_wrap_mail('', implode('', $indent)) . "\n";
+          $output = _drupal_html_to_text_pad($output, '-');
+          break;
+
+        // Paragraphs and definition lists
+        case '/p':
+        case '/dl':
+          $chunk = ''; // Ensure blank new-line.
+          break;
+      }
+    }
+    // Process blocks of text.
+    else {
+      // Convert inline HTML text to plain text; not removing line-breaks or
+      // white-space, since that breaks newlines when sanitizing plain-text.
+      $value = trim(decode_entities($value));
+      if (drupal_strlen($value)) {
+        $chunk = $value;
+      }
+    }
+
+    // See if there is something waiting to be output.
+    if (isset($chunk)) {
+      // Apply any necessary case conversion.
+      if (isset($casing)) {
+        $chunk = $casing($chunk);
+      }
+      // Format it and apply the current indentation.
+      $output .= drupal_wrap_mail($chunk, implode('', $indent)) . MAIL_LINE_ENDINGS;
+      // Remove non-quotation markers from indentation.
+      $indent = array_map('_drupal_html_to_text_clean', $indent);
+    }
+
+    $tag = !$tag;
+  }
+
+  return $output . $footnotes;
+}
+
+/**
+ * Wraps words on a single line.
+ *
+ * Callback for array_walk() winthin drupal_wrap_mail().
+ */
+function _drupal_wrap_mail_line(&$line, $key, $values) {
+  // Use soft-breaks only for purely quoted or unindented text.
+  $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? "  \n" : "\n");
+  // Break really long words at the maximum width allowed.
+  $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n");
+}
+
+/**
+ * Keeps track of URLs and replaces them with placeholder tokens.
+ *
+ * Callback for preg_replace_callback() within drupal_html_to_text().
+ */
+function _drupal_html_to_mail_urls($match = NULL, $reset = FALSE) {
+  global $base_url, $base_path;
+  static $urls = array(), $regexp;
+
+  if ($reset) {
+    // Reset internal URL list.
+    $urls = array();
+  }
+  else {
+    if (empty($regexp)) {
+      $regexp = '@^' . preg_quote($base_path, '@') . '@';
+    }
+    if ($match) {
+      list(, , $url, $label) = $match;
+      // Ensure all URLs are absolute.
+      $urls[] = strpos($url, '://') ? $url : preg_replace($regexp, $base_url . '/', $url);
+      return $label . ' [' . count($urls) . ']';
+    }
+  }
+  return $urls;
+}
+
+/**
+ * Replaces non-quotation markers from a given piece of indentation with spaces.
+ *
+ * Callback for array_map() within drupal_html_to_text().
+ */
+function _drupal_html_to_text_clean($indent) {
+  return preg_replace('/[^>]/', ' ', $indent);
+}
+
+/**
+ * Pads the last line with the given character.
+ *
+ * @see drupal_html_to_text()
+ */
+function _drupal_html_to_text_pad($text, $pad, $prefix = '') {
+  // Remove last line break.
+  $text = substr($text, 0, -1);
+  // Calculate needed padding space and add it.
+  if (($p = strrpos($text, "\n")) === FALSE) {
+    $p = -1;
+  }
+  $n = max(0, 79 - (strlen($text) - $p) - strlen($prefix));
+  // Add prefix and padding, and restore linebreak.
+  return $text . $prefix . str_repeat($pad, $n) . "\n";
+}

+ 3883 - 0
includes/menu.inc

@@ -0,0 +1,3883 @@
+<?php
+
+/**
+ * @file
+ * API for the Drupal menu system.
+ */
+
+/**
+ * @defgroup menu Menu system
+ * @{
+ * Define the navigation menus, and route page requests to code based on URLs.
+ *
+ * The Drupal menu system drives both the navigation system from a user
+ * perspective and the callback system that Drupal uses to respond to URLs
+ * passed from the browser. For this reason, a good understanding of the
+ * menu system is fundamental to the creation of complex modules. As a note,
+ * this is related to, but separate from menu.module, which allows menus
+ * (which in this context are hierarchical lists of links) to be customized from
+ * the Drupal administrative interface.
+ *
+ * Drupal's menu system follows a simple hierarchy defined by paths.
+ * Implementations of hook_menu() define menu items and assign them to
+ * paths (which should be unique). The menu system aggregates these items
+ * and determines the menu hierarchy from the paths. For example, if the
+ * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
+ * would form the structure:
+ * - a
+ *   - a/b
+ *     - a/b/c/d
+ *     - a/b/h
+ * - e
+ * - f/g
+ * Note that the number of elements in the path does not necessarily
+ * determine the depth of the menu item in the tree.
+ *
+ * When responding to a page request, the menu system looks to see if the
+ * path requested by the browser is registered as a menu item with a
+ * callback. If not, the system searches up the menu tree for the most
+ * complete match with a callback it can find. If the path a/b/i is
+ * requested in the tree above, the callback for a/b would be used.
+ *
+ * The found callback function is called with any arguments specified
+ * in the "page arguments" attribute of its menu item. The
+ * attribute must be an array. After these arguments, any remaining
+ * components of the path are appended as further arguments. In this
+ * way, the callback for a/b above could respond to a request for
+ * a/b/i differently than a request for a/b/j.
+ *
+ * For an illustration of this process, see page_example.module.
+ *
+ * Access to the callback functions is also protected by the menu system.
+ * The "access callback" with an optional "access arguments" of each menu
+ * item is called before the page callback proceeds. If this returns TRUE,
+ * then access is granted; if FALSE, then access is denied. Default local task
+ * menu items (see next paragraph) may omit this attribute to use the value
+ * provided by the parent item.
+ *
+ * In the default Drupal interface, you will notice many links rendered as
+ * tabs. These are known in the menu system as "local tasks", and they are
+ * rendered as tabs by default, though other presentations are possible.
+ * Local tasks function just as other menu items in most respects. It is
+ * convention that the names of these tasks should be short verbs if
+ * possible. In addition, a "default" local task should be provided for
+ * each set. When visiting a local task's parent menu item, the default
+ * local task will be rendered as if it is selected; this provides for a
+ * normal tab user experience. This default task is special in that it
+ * links not to its provided path, but to its parent item's path instead.
+ * The default task's path is only used to place it appropriately in the
+ * menu hierarchy.
+ *
+ * Everything described so far is stored in the menu_router table. The
+ * menu_links table holds the visible menu links. By default these are
+ * derived from the same hook_menu definitions, however you are free to
+ * add more with menu_link_save().
+ */
+
+/**
+ * @defgroup menu_flags Menu flags
+ * @{
+ * Flags for use in the "type" attribute of menu items.
+ */
+
+/**
+ * Internal menu flag -- menu item is the root of the menu tree.
+ */
+define('MENU_IS_ROOT', 0x0001);
+
+/**
+ * Internal menu flag -- menu item is visible in the menu tree.
+ */
+define('MENU_VISIBLE_IN_TREE', 0x0002);
+
+/**
+ * Internal menu flag -- menu item is visible in the breadcrumb.
+ */
+define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
+
+/**
+ * Internal menu flag -- menu item links back to its parent.
+ */
+define('MENU_LINKS_TO_PARENT', 0x0008);
+
+/**
+ * Internal menu flag -- menu item can be modified by administrator.
+ */
+define('MENU_MODIFIED_BY_ADMIN', 0x0020);
+
+/**
+ * Internal menu flag -- menu item was created by administrator.
+ */
+define('MENU_CREATED_BY_ADMIN', 0x0040);
+
+/**
+ * Internal menu flag -- menu item is a local task.
+ */
+define('MENU_IS_LOCAL_TASK', 0x0080);
+
+/**
+ * Internal menu flag -- menu item is a local action.
+ */
+define('MENU_IS_LOCAL_ACTION', 0x0100);
+
+/**
+ * @} End of "Menu flags".
+ */
+
+/**
+ * @defgroup menu_item_types Menu item types
+ * @{
+ * Definitions for various menu item types.
+ *
+ * Menu item definitions provide one of these constants, which are shortcuts for
+ * combinations of @link menu_flags Menu flags @endlink.
+ */
+
+/**
+ * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs.
+ *
+ * Normal menu items show up in the menu tree and can be moved/hidden by
+ * the administrator. Use this for most menu items. It is the default value if
+ * no menu item type is specified.
+ */
+define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
+
+/**
+ * Menu type -- A hidden, internal callback, typically used for API calls.
+ *
+ * Callbacks simply register a path so that the correct function is fired
+ * when the URL is accessed. They do not appear in menus or breadcrumbs.
+ */
+define('MENU_CALLBACK', 0x0000);
+
+/**
+ * Menu type -- A normal menu item, hidden until enabled by an administrator.
+ *
+ * Modules may "suggest" menu items that the administrator may enable. They act
+ * just as callbacks do until enabled, at which time they act like normal items.
+ * Note for the value: 0x0010 was a flag which is no longer used, but this way
+ * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
+ */
+define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
+
+/**
+ * Menu type -- A task specific to the parent item, usually rendered as a tab.
+ *
+ * Local tasks are menu items that describe actions to be performed on their
+ * parent item. An example is the path "node/52/edit", which performs the
+ * "edit" task on "node/52".
+ */
+define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB);
+
+/**
+ * Menu type -- The "default" local task, which is initially active.
+ *
+ * Every set of local tasks should provide one "default" task, that links to the
+ * same path as its parent when clicked.
+ */
+define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT | MENU_VISIBLE_IN_BREADCRUMB);
+
+/**
+ * Menu type -- An action specific to the parent, usually rendered as a link.
+ *
+ * Local actions are menu items that describe actions on the parent item such
+ * as adding a new user, taxonomy term, etc.
+ */
+define('MENU_LOCAL_ACTION', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB);
+
+/**
+ * @} End of "Menu item types".
+ */
+
+/**
+ * @defgroup menu_context_types Menu context types
+ * @{
+ * Flags for use in the "context" attribute of menu router items.
+ */
+
+/**
+ * Internal menu flag: Invisible local task.
+ *
+ * This flag may be used for local tasks like "Delete", so custom modules and
+ * themes can alter the default context and expose the task by altering menu.
+ */
+define('MENU_CONTEXT_NONE', 0x0000);
+
+/**
+ * Internal menu flag: Local task should be displayed in page context.
+ */
+define('MENU_CONTEXT_PAGE', 0x0001);
+
+/**
+ * Internal menu flag: Local task should be displayed inline.
+ */
+define('MENU_CONTEXT_INLINE', 0x0002);
+
+/**
+ * @} End of "Menu context types".
+ */
+
+/**
+ * @defgroup menu_status_codes Menu status codes
+ * @{
+ * Status codes for menu callbacks.
+ */
+
+/**
+ * Internal menu status code -- Menu item was found.
+ */
+define('MENU_FOUND', 1);
+
+/**
+ * Internal menu status code -- Menu item was not found.
+ */
+define('MENU_NOT_FOUND', 2);
+
+/**
+ * Internal menu status code -- Menu item access is denied.
+ */
+define('MENU_ACCESS_DENIED', 3);
+
+/**
+ * Internal menu status code -- Menu item inaccessible because site is offline.
+ */
+define('MENU_SITE_OFFLINE', 4);
+
+/**
+ * Internal menu status code -- Everything is working fine.
+ */
+define('MENU_SITE_ONLINE', 5);
+
+/**
+ * @} End of "Menu status codes".
+ */
+
+/**
+ * @defgroup menu_tree_parameters Menu tree parameters
+ * @{
+ * Parameters for a menu tree.
+ */
+
+ /**
+ * The maximum number of path elements for a menu callback
+ */
+define('MENU_MAX_PARTS', 9);
+
+
+/**
+ * The maximum depth of a menu links tree - matches the number of p columns.
+ */
+define('MENU_MAX_DEPTH', 9);
+
+
+/**
+ * @} End of "Menu tree parameters".
+ */
+
+/**
+ * Reserved key to identify the most specific menu link for a given path.
+ *
+ * The value of this constant is a hash of the constant name. We use the hash
+ * so that the reserved key is over 32 characters in length and will not
+ * collide with allowed menu names:
+ * @code
+ * sha1('MENU_PREFERRED_LINK') = 1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91
+ * @endcode
+ *
+ * @see menu_link_get_preferred()
+ */
+define('MENU_PREFERRED_LINK', '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91');
+
+/**
+ * Returns the ancestors (and relevant placeholders) for any given path.
+ *
+ * For example, the ancestors of node/12345/edit are:
+ * - node/12345/edit
+ * - node/12345/%
+ * - node/%/edit
+ * - node/%/%
+ * - node/12345
+ * - node/%
+ * - node
+ *
+ * To generate these, we will use binary numbers. Each bit represents a
+ * part of the path. If the bit is 1, then it represents the original
+ * value while 0 means wildcard. If the path is node/12/edit/foo
+ * then the 1011 bitstring represents node/%/edit/foo where % means that
+ * any argument matches that part. We limit ourselves to using binary
+ * numbers that correspond the patterns of wildcards of router items that
+ * actually exists. This list of 'masks' is built in menu_rebuild().
+ *
+ * @param $parts
+ *   An array of path parts, for the above example
+ *   array('node', '12345', 'edit').
+ *
+ * @return
+ *   An array which contains the ancestors and placeholders. Placeholders
+ *   simply contain as many '%s' as the ancestors.
+ */
+function menu_get_ancestors($parts) {
+  $number_parts = count($parts);
+  $ancestors = array();
+  $length =  $number_parts - 1;
+  $end = (1 << $number_parts) - 1;
+  $masks = variable_get('menu_masks');
+  // If the optimized menu_masks array is not available use brute force to get
+  // the correct $ancestors and $placeholders returned. Do not use this as the
+  // default value of the menu_masks variable to avoid building such a big
+  // array.
+  if (!$masks) {
+    $masks = range(511, 1);
+  }
+  // Only examine patterns that actually exist as router items (the masks).
+  foreach ($masks as $i) {
+    if ($i > $end) {
+      // Only look at masks that are not longer than the path of interest.
+      continue;
+    }
+    elseif ($i < (1 << $length)) {
+      // We have exhausted the masks of a given length, so decrease the length.
+      --$length;
+    }
+    $current = '';
+    for ($j = $length; $j >= 0; $j--) {
+      // Check the bit on the $j offset.
+      if ($i & (1 << $j)) {
+        // Bit one means the original value.
+        $current .= $parts[$length - $j];
+      }
+      else {
+        // Bit zero means means wildcard.
+        $current .= '%';
+      }
+      // Unless we are at offset 0, add a slash.
+      if ($j) {
+        $current .= '/';
+      }
+    }
+    $ancestors[] = $current;
+  }
+  return $ancestors;
+}
+
+/**
+ * Unserializes menu data, using a map to replace path elements.
+ *
+ * The menu system stores various path-related information (such as the 'page
+ * arguments' and 'access arguments' components of a menu item) in the database
+ * using serialized arrays, where integer values in the arrays represent
+ * arguments to be replaced by values from the path. This function first
+ * unserializes such menu information arrays, and then does the path
+ * replacement.
+ *
+ * The path replacement acts on each integer-valued element of the unserialized
+ * menu data array ($data) using a map array ($map, which is typically an array
+ * of path arguments) as a list of replacements. For instance, if there is an
+ * element of $data whose value is the number 2, then it is replaced in $data
+ * with $map[2]; non-integer values in $data are left alone.
+ *
+ * As an example, an unserialized $data array with elements ('node_load', 1)
+ * represents instructions for calling the node_load() function. Specifically,
+ * this instruction says to use the path component at index 1 as the input
+ * parameter to node_load(). If the path is 'node/123', then $map will be the
+ * array ('node', 123), and the returned array from this function will have
+ * elements ('node_load', 123), since $map[1] is 123. This return value will
+ * indicate specifically that node_load(123) is to be called to load the node
+ * whose ID is 123 for this menu item.
+ *
+ * @param $data
+ *   A serialized array of menu data, as read from the database.
+ * @param $map
+ *   A path argument array, used to replace integer values in $data; an integer
+ *   value N in $data will be replaced by value $map[N]. Typically, the $map
+ *   array is generated from a call to the arg() function.
+ *
+ * @return
+ *   The unserialized $data array, with path arguments replaced.
+ */
+function menu_unserialize($data, $map) {
+  if ($data = unserialize($data)) {
+    foreach ($data as $k => $v) {
+      if (is_int($v)) {
+        $data[$k] = isset($map[$v]) ? $map[$v] : '';
+      }
+    }
+    return $data;
+  }
+  else {
+    return array();
+  }
+}
+
+
+
+/**
+ * Replaces the statically cached item for a given path.
+ *
+ * @param $path
+ *   The path.
+ * @param $router_item
+ *   The router item. Usually a router entry from menu_get_item() is either
+ *   modified or set to a different path. This allows the navigation block,
+ *   the page title, the breadcrumb, and the page help to be modified in one
+ *   call.
+ */
+function menu_set_item($path, $router_item) {
+  menu_get_item($path, $router_item);
+}
+
+/**
+ * Gets a router item.
+ *
+ * @param $path
+ *   The path, for example node/5. The function will find the corresponding
+ *   node/% item and return that.
+ * @param $router_item
+ *   Internal use only.
+ *
+ * @return
+ *   The router item or, if an error occurs in _menu_translate(), FALSE. A
+ *   router item is an associative array corresponding to one row in the
+ *   menu_router table. The value corresponding to the key 'map' holds the
+ *   loaded objects. The value corresponding to the key 'access' is TRUE if the
+ *   current user can access this page. The values corresponding to the keys
+ *   'title', 'page_arguments', 'access_arguments', and 'theme_arguments' will
+ *   be filled in based on the database values and the objects loaded.
+ */
+function menu_get_item($path = NULL, $router_item = NULL) {
+  $router_items = &drupal_static(__FUNCTION__);
+  if (!isset($path)) {
+    $path = $_GET['q'];
+  }
+  if (isset($router_item)) {
+    $router_items[$path] = $router_item;
+  }
+  if (!isset($router_items[$path])) {
+    // Rebuild if we know it's needed, or if the menu masks are missing which
+    // occurs rarely, likely due to a race condition of multiple rebuilds.
+    if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
+      menu_rebuild();
+    }
+    $original_map = arg(NULL, $path);
+
+    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
+    $ancestors = menu_get_ancestors($parts);
+    $router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc();
+
+    if ($router_item) {
+      // Allow modules to alter the router item before it is translated and
+      // checked for access.
+      drupal_alter('menu_get_item', $router_item, $path, $original_map);
+
+      $map = _menu_translate($router_item, $original_map);
+      $router_item['original_map'] = $original_map;
+      if ($map === FALSE) {
+        $router_items[$path] = FALSE;
+        return FALSE;
+      }
+      if ($router_item['access']) {
+        $router_item['map'] = $map;
+        $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
+        $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));
+      }
+    }
+    $router_items[$path] = $router_item;
+  }
+  return $router_items[$path];
+}
+
+/**
+ * Execute the page callback associated with the current path.
+ *
+ * @param $path
+ *   The drupal path whose handler is to be be executed. If set to NULL, then
+ *   the current path is used.
+ * @param $deliver
+ *   (optional) A boolean to indicate whether the content should be sent to the
+ *   browser using the appropriate delivery callback (TRUE) or whether to return
+ *   the result to the caller (FALSE).
+ */
+function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
+  // Check if site is offline.
+  $page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE;
+
+  // Allow other modules to change the site status but not the path because that
+  // would not change the global variable. hook_url_inbound_alter() can be used
+  // to change the path. Code later will not use the $read_only_path variable.
+  $read_only_path = !empty($path) ? $path : $_GET['q'];
+  drupal_alter('menu_site_status', $page_callback_result, $read_only_path);
+
+  // Only continue if the site status is not set.
+  if ($page_callback_result == MENU_SITE_ONLINE) {
+    if ($router_item = menu_get_item($path)) {
+      if ($router_item['access']) {
+        if ($router_item['include_file']) {
+          require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
+        }
+        $page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
+      }
+      else {
+        $page_callback_result = MENU_ACCESS_DENIED;
+      }
+    }
+    else {
+      $page_callback_result = MENU_NOT_FOUND;
+    }
+  }
+
+  // Deliver the result of the page callback to the browser, or if requested,
+  // return it raw, so calling code can do more processing.
+  if ($deliver) {
+    $default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;
+    drupal_deliver_page($page_callback_result, $default_delivery_callback);
+  }
+  else {
+    return $page_callback_result;
+  }
+}
+
+/**
+ * Loads objects into the map as defined in the $item['load_functions'].
+ *
+ * @param $item
+ *   A menu router or menu link item
+ * @param $map
+ *   An array of path arguments (ex: array('node', '5'))
+ *
+ * @return
+ *   Returns TRUE for success, FALSE if an object cannot be loaded.
+ *   Names of object loading functions are placed in $item['load_functions'].
+ *   Loaded objects are placed in $map[]; keys are the same as keys in the
+ *   $item['load_functions'] array.
+ *   $item['access'] is set to FALSE if an object cannot be loaded.
+ */
+function _menu_load_objects(&$item, &$map) {
+  if ($load_functions = $item['load_functions']) {
+    // If someone calls this function twice, then unserialize will fail.
+    if (!is_array($load_functions)) {
+      $load_functions = unserialize($load_functions);
+    }
+    $path_map = $map;
+    foreach ($load_functions as $index => $function) {
+      if ($function) {
+        $value = isset($path_map[$index]) ? $path_map[$index] : '';
+        if (is_array($function)) {
+          // Set up arguments for the load function. These were pulled from
+          // 'load arguments' in the hook_menu() entry, but they need
+          // some processing. In this case the $function is the key to the
+          // load_function array, and the value is the list of arguments.
+          list($function, $args) = each($function);
+          $load_functions[$index] = $function;
+
+          // Some arguments are placeholders for dynamic items to process.
+          foreach ($args as $i => $arg) {
+            if ($arg === '%index') {
+              // Pass on argument index to the load function, so multiple
+              // occurrences of the same placeholder can be identified.
+              $args[$i] = $index;
+            }
+            if ($arg === '%map') {
+              // Pass on menu map by reference. The accepting function must
+              // also declare this as a reference if it wants to modify
+              // the map.
+              $args[$i] = &$map;
+            }
+            if (is_int($arg)) {
+              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
+            }
+          }
+          array_unshift($args, $value);
+          $return = call_user_func_array($function, $args);
+        }
+        else {
+          $return = $function($value);
+        }
+        // If callback returned an error or there is no callback, trigger 404.
+        if ($return === FALSE) {
+          $item['access'] = FALSE;
+          $map = FALSE;
+          return FALSE;
+        }
+        $map[$index] = $return;
+      }
+    }
+    $item['load_functions'] = $load_functions;
+  }
+  return TRUE;
+}
+
+/**
+ * Checks access to a menu item using the access callback.
+ *
+ * @param $item
+ *   A menu router or menu link item
+ * @param $map
+ *   An array of path arguments (ex: array('node', '5'))
+ *
+ * @return
+ *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
+ */
+function _menu_check_access(&$item, $map) {
+  $item['access'] = FALSE;
+  // Determine access callback, which will decide whether or not the current
+  // user has access to this path.
+  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
+  // Check for a TRUE or FALSE value.
+  if (is_numeric($callback)) {
+    $item['access'] = (bool) $callback;
+  }
+  else {
+    $arguments = menu_unserialize($item['access_arguments'], $map);
+    // As call_user_func_array is quite slow and user_access is a very common
+    // callback, it is worth making a special case for it.
+    if ($callback == 'user_access') {
+      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
+    }
+    elseif (function_exists($callback)) {
+      $item['access'] = call_user_func_array($callback, $arguments);
+    }
+  }
+}
+
+/**
+ * Localizes the router item title using t() or another callback.
+ *
+ * Translate the title and description to allow storage of English title
+ * strings in the database, yet display of them in the language required
+ * by the current user.
+ *
+ * @param $item
+ *   A menu router item or a menu link item.
+ * @param $map
+ *   The path as an array with objects already replaced. E.g., for path
+ *   node/123 $map would be array('node', $node) where $node is the node
+ *   object for node 123.
+ * @param $link_translate
+ *   TRUE if we are translating a menu link item; FALSE if we are
+ *   translating a menu router item.
+ *
+ * @return
+ *   No return value.
+ *   $item['title'] is localized according to $item['title_callback'].
+ *   If an item's callback is check_plain(), $item['options']['html'] becomes
+ *   TRUE.
+ *   $item['description'] is translated using t().
+ *   When doing link translation and the $item['options']['attributes']['title']
+ *   (link title attribute) matches the description, it is translated as well.
+ */
+function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
+  $callback = $item['title_callback'];
+  $item['localized_options'] = $item['options'];
+  // All 'class' attributes are assumed to be an array during rendering, but
+  // links stored in the database may use an old string value.
+  // @todo In order to remove this code we need to implement a database update
+  //   including unserializing all existing link options and running this code
+  //   on them, as well as adding validation to menu_link_save().
+  if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) {
+    $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']);
+  }
+  // If we are translating the title of a menu link, and its title is the same
+  // as the corresponding router item, then we can use the title information
+  // from the router. If it's customized, then we need to use the link title
+  // itself; can't localize.
+  // If we are translating a router item (tabs, page, breadcrumb), then we
+  // can always use the information from the router item.
+  if (!$link_translate || ($item['title'] == $item['link_title'])) {
+    // t() is a special case. Since it is used very close to all the time,
+    // we handle it directly instead of using indirect, slower methods.
+    if ($callback == 't') {
+      if (empty($item['title_arguments'])) {
+        $item['title'] = t($item['title']);
+      }
+      else {
+        $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
+      }
+    }
+    elseif ($callback && function_exists($callback)) {
+      if (empty($item['title_arguments'])) {
+        $item['title'] = $callback($item['title']);
+      }
+      else {
+        $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
+      }
+      // Avoid calling check_plain again on l() function.
+      if ($callback == 'check_plain') {
+        $item['localized_options']['html'] = TRUE;
+      }
+    }
+  }
+  elseif ($link_translate) {
+    $item['title'] = $item['link_title'];
+  }
+
+  // Translate description, see the motivation above.
+  if (!empty($item['description'])) {
+    $original_description = $item['description'];
+    $item['description'] = t($item['description']);
+    if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
+      $item['localized_options']['attributes']['title'] = $item['description'];
+    }
+  }
+}
+
+/**
+ * Handles dynamic path translation and menu access control.
+ *
+ * When a user arrives on a page such as node/5, this function determines
+ * what "5" corresponds to, by inspecting the page's menu path definition,
+ * node/%node. This will call node_load(5) to load the corresponding node
+ * object.
+ *
+ * It also works in reverse, to allow the display of tabs and menu items which
+ * contain these dynamic arguments, translating node/%node to node/5.
+ *
+ * Translation of menu item titles and descriptions are done here to
+ * allow for storage of English strings in the database, and translation
+ * to the language required to generate the current page.
+ *
+ * @param $router_item
+ *   A menu router item
+ * @param $map
+ *   An array of path arguments (ex: array('node', '5'))
+ * @param $to_arg
+ *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
+ *   path from the menu table, for example tabs.
+ *
+ * @return
+ *   Returns the map with objects loaded as defined in the
+ *   $item['load_functions']. $item['access'] becomes TRUE if the item is
+ *   accessible, FALSE otherwise. $item['href'] is set according to the map.
+ *   If an error occurs during calling the load_functions (like trying to load
+ *   a non-existent node) then this function returns FALSE.
+ */
+function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
+  if ($to_arg && !empty($router_item['to_arg_functions'])) {
+    // Fill in missing path elements, such as the current uid.
+    _menu_link_map_translate($map, $router_item['to_arg_functions']);
+  }
+  // The $path_map saves the pieces of the path as strings, while elements in
+  // $map may be replaced with loaded objects.
+  $path_map = $map;
+  if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) {
+    // An error occurred loading an object.
+    $router_item['access'] = FALSE;
+    return FALSE;
+  }
+
+  // Generate the link path for the page request or local tasks.
+  $link_map = explode('/', $router_item['path']);
+  if (isset($router_item['tab_root'])) {
+    $tab_root_map = explode('/', $router_item['tab_root']);
+  }
+  if (isset($router_item['tab_parent'])) {
+    $tab_parent_map = explode('/', $router_item['tab_parent']);
+  }
+  for ($i = 0; $i < $router_item['number_parts']; $i++) {
+    if ($link_map[$i] == '%') {
+      $link_map[$i] = $path_map[$i];
+    }
+    if (isset($tab_root_map[$i]) && $tab_root_map[$i] == '%') {
+      $tab_root_map[$i] = $path_map[$i];
+    }
+    if (isset($tab_parent_map[$i]) && $tab_parent_map[$i] == '%') {
+      $tab_parent_map[$i] = $path_map[$i];
+    }
+  }
+  $router_item['href'] = implode('/', $link_map);
+  $router_item['tab_root_href'] = implode('/', $tab_root_map);
+  $router_item['tab_parent_href'] = implode('/', $tab_parent_map);
+  $router_item['options'] = array();
+  _menu_check_access($router_item, $map);
+
+  // For performance, don't localize an item the user can't access.
+  if ($router_item['access']) {
+    _menu_item_localize($router_item, $map);
+  }
+
+  return $map;
+}
+
+/**
+ * Translates the path elements in the map using any to_arg helper function.
+ *
+ * @param $map
+ *   An array of path arguments (ex: array('node', '5'))
+ * @param $to_arg_functions
+ *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
+ *
+ * @see hook_menu()
+ */
+function _menu_link_map_translate(&$map, $to_arg_functions) {
+  $to_arg_functions = unserialize($to_arg_functions);
+  foreach ($to_arg_functions as $index => $function) {
+    // Translate place-holders into real values.
+    $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
+    if (!empty($map[$index]) || isset($arg)) {
+      $map[$index] = $arg;
+    }
+    else {
+      unset($map[$index]);
+    }
+  }
+}
+
+/**
+ * Returns a string containing the path relative to the current index.
+ */
+function menu_tail_to_arg($arg, $map, $index) {
+  return implode('/', array_slice($map, $index));
+}
+
+/**
+ * Loads the path as one string relative to the current index.
+ *
+ * To use this load function, you must specify the load arguments
+ * in the router item as:
+ * @code
+ * $item['load arguments'] = array('%map', '%index');
+ * @endcode
+ *
+ * @see search_menu().
+ */
+function menu_tail_load($arg, &$map, $index) {
+  $arg = implode('/', array_slice($map, $index));
+  $map = array_slice($map, 0, $index);
+  return $arg;
+}
+
+/**
+ * Provides menu link access control, translation, and argument handling.
+ *
+ * This function is similar to _menu_translate(), but it also does
+ * link-specific preparation (such as always calling to_arg() functions).
+ *
+ * @param $item
+ *   A menu link.
+ * @param $translate
+ *   (optional) Whether to try to translate a link containing dynamic path
+ *   argument placeholders (%) based on the menu router item of the current
+ *   path. Defaults to FALSE. Internally used for breadcrumbs.
+ *
+ * @return
+ *   Returns the map of path arguments with objects loaded as defined in the
+ *   $item['load_functions'].
+ *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
+ *   $item['href'] is generated from link_path, possibly by to_arg functions.
+ *   $item['title'] is generated from link_title, and may be localized.
+ *   $item['options'] is unserialized; it is also changed within the call here
+ *   to $item['localized_options'] by _menu_item_localize().
+ */
+function _menu_link_translate(&$item, $translate = FALSE) {
+  if (!is_array($item['options'])) {
+    $item['options'] = unserialize($item['options']);
+  }
+  if ($item['external']) {
+    $item['access'] = 1;
+    $map = array();
+    $item['href'] = $item['link_path'];
+    $item['title'] = $item['link_title'];
+    $item['localized_options'] = $item['options'];
+  }
+  else {
+    // Complete the path of the menu link with elements from the current path,
+    // if it contains dynamic placeholders (%).
+    $map = explode('/', $item['link_path']);
+    if (strpos($item['link_path'], '%') !== FALSE) {
+      // Invoke registered to_arg callbacks.
+      if (!empty($item['to_arg_functions'])) {
+        _menu_link_map_translate($map, $item['to_arg_functions']);
+      }
+      // Or try to derive the path argument map from the current router item,
+      // if this $item's path is within the router item's path. This means
+      // that if we are on the current path 'foo/%/bar/%/baz', then
+      // menu_get_item() will have translated the menu router item for the
+      // current path, and we can take over the argument map for a link like
+      // 'foo/%/bar'. This inheritance is only valid for breadcrumb links.
+      // @see _menu_tree_check_access()
+      // @see menu_get_active_breadcrumb()
+      elseif ($translate && ($current_router_item = menu_get_item())) {
+        // If $translate is TRUE, then this link is in the active trail.
+        // Only translate paths within the current path.
+        if (strpos($current_router_item['path'], $item['link_path']) === 0) {
+          $count = count($map);
+          $map = array_slice($current_router_item['original_map'], 0, $count);
+          $item['original_map'] = $map;
+          if (isset($current_router_item['map'])) {
+            $item['map'] = array_slice($current_router_item['map'], 0, $count);
+          }
+          // Reset access to check it (for the first time).
+          unset($item['access']);
+        }
+      }
+    }
+    $item['href'] = implode('/', $map);
+
+    // Skip links containing untranslated arguments.
+    if (strpos($item['href'], '%') !== FALSE) {
+      $item['access'] = FALSE;
+      return FALSE;
+    }
+    // menu_tree_check_access() may set this ahead of time for links to nodes.
+    if (!isset($item['access'])) {
+      if (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) {
+        // An error occurred loading an object.
+        $item['access'] = FALSE;
+        return FALSE;
+      }
+      _menu_check_access($item, $map);
+    }
+    // For performance, don't localize a link the user can't access.
+    if ($item['access']) {
+      _menu_item_localize($item, $map, TRUE);
+    }
+  }
+
+  // Allow other customizations - e.g. adding a page-specific query string to the
+  // options array. For performance reasons we only invoke this hook if the link
+  // has the 'alter' flag set in the options array.
+  if (!empty($item['options']['alter'])) {
+    drupal_alter('translated_menu_link', $item, $map);
+  }
+
+  return $map;
+}
+
+/**
+ * Gets a loaded object from a router item.
+ *
+ * menu_get_object() provides access to objects loaded by the current router
+ * item. For example, on the page node/%node, the router loads the %node object,
+ * and calling menu_get_object() will return that. Normally, it is necessary to
+ * specify the type of object referenced, however node is the default.
+ * The following example tests to see whether the node being displayed is of the
+ * "story" content type:
+ * @code
+ * $node = menu_get_object();
+ * $story = $node->type == 'story';
+ * @endcode
+ *
+ * @param $type
+ *   Type of the object. These appear in hook_menu definitions as %type. Core
+ *   provides aggregator_feed, aggregator_category, contact, filter_format,
+ *   forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
+ *   relevant {$type}_load function for more on each. Defaults to node.
+ * @param $position
+ *   The position of the object in the path, where the first path segment is 0.
+ *   For node/%node, the position of %node is 1, but for comment/reply/%node,
+ *   it's 2. Defaults to 1.
+ * @param $path
+ *   See menu_get_item() for more on this. Defaults to the current path.
+ */
+function menu_get_object($type = 'node', $position = 1, $path = NULL) {
+  $router_item = menu_get_item($path);
+  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
+    return $router_item['map'][$position];
+  }
+}
+
+/**
+ * Renders a menu tree based on the current path.
+ *
+ * The tree is expanded based on the current path and dynamic paths are also
+ * changed according to the defined to_arg functions (for example the 'My
+ * account' link is changed from user/% to a link with the current user's uid).
+ *
+ * @param $menu_name
+ *   The name of the menu.
+ *
+ * @return
+ *   A structured array representing the specified menu on the current page, to
+ *   be rendered by drupal_render().
+ */
+function menu_tree($menu_name) {
+  $menu_output = &drupal_static(__FUNCTION__, array());
+
+  if (!isset($menu_output[$menu_name])) {
+    $tree = menu_tree_page_data($menu_name);
+    $menu_output[$menu_name] = menu_tree_output($tree);
+  }
+  return $menu_output[$menu_name];
+}
+
+/**
+ * Returns a rendered menu tree.
+ *
+ * The menu item's LI element is given one of the following classes:
+ * - expanded: The menu item is showing its submenu.
+ * - collapsed: The menu item has a submenu which is not shown.
+ * - leaf: The menu item has no submenu.
+ *
+ * @param $tree
+ *   A data structure representing the tree as returned from menu_tree_data.
+ *
+ * @return
+ *   A structured array to be rendered by drupal_render().
+ */
+function menu_tree_output($tree) {
+  $build = array();
+  $items = array();
+
+  // Pull out just the menu links we are going to render so that we
+  // get an accurate count for the first/last classes.
+  foreach ($tree as $data) {
+    if ($data['link']['access'] && !$data['link']['hidden']) {
+      $items[] = $data;
+    }
+  }
+
+  $router_item = menu_get_item();
+  $num_items = count($items);
+  foreach ($items as $i => $data) {
+    $class = array();
+    if ($i == 0) {
+      $class[] = 'first';
+    }
+    if ($i == $num_items - 1) {
+      $class[] = 'last';
+    }
+    // Set a class for the <li>-tag. Since $data['below'] may contain local
+    // tasks, only set 'expanded' class if the link also has children within
+    // the current menu.
+    if ($data['link']['has_children'] && $data['below']) {
+      $class[] = 'expanded';
+    }
+    elseif ($data['link']['has_children']) {
+      $class[] = 'collapsed';
+    }
+    else {
+      $class[] = 'leaf';
+    }
+    // Set a class if the link is in the active trail.
+    if ($data['link']['in_active_trail']) {
+      $class[] = 'active-trail';
+      $data['link']['localized_options']['attributes']['class'][] = 'active-trail';
+    }
+    // Normally, l() compares the href of every link with $_GET['q'] and sets
+    // the active class accordingly. But local tasks do not appear in menu
+    // trees, so if the current path is a local task, and this link is its
+    // tab root, then we have to set the class manually.
+    if ($data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != $_GET['q']) {
+      $data['link']['localized_options']['attributes']['class'][] = 'active';
+    }
+
+    // Allow menu-specific theme overrides.
+    $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
+    $element['#attributes']['class'] = $class;
+    $element['#title'] = $data['link']['title'];
+    $element['#href'] = $data['link']['href'];
+    $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
+    $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below'];
+    $element['#original_link'] = $data['link'];
+    // Index using the link's unique mlid.
+    $build[$data['link']['mlid']] = $element;
+  }
+  if ($build) {
+    // Make sure drupal_render() does not re-order the links.
+    $build['#sorted'] = TRUE;
+    // Add the theme wrapper for outer markup.
+    // Allow menu-specific theme overrides.
+    $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_');
+  }
+
+  return $build;
+}
+
+/**
+ * Gets the data structure representing a named menu tree.
+ *
+ * Since this can be the full tree including hidden items, the data returned
+ * may be used for generating an an admin interface or a select.
+ *
+ * @param $menu_name
+ *   The named menu links to return
+ * @param $link
+ *   A fully loaded menu link, or NULL. If a link is supplied, only the
+ *   path to root will be included in the returned tree - as if this link
+ *   represented the current page in a visible menu.
+ * @param $max_depth
+ *   Optional maximum depth of links to retrieve. Typically useful if only one
+ *   or two levels of a sub tree are needed in conjunction with a non-NULL
+ *   $link, in which case $max_depth should be greater than $link['depth'].
+ *
+ * @return
+ *   An tree of menu links in an array, in the order they should be rendered.
+ */
+function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) {
+  $tree = &drupal_static(__FUNCTION__, array());
+
+  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
+  $mlid = isset($link['mlid']) ? $link['mlid'] : 0;
+  // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth.
+  $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $GLOBALS['language']->language . ':' . (int) $max_depth;
+
+  if (!isset($tree[$cid])) {
+    // If the static variable doesn't have the data, check {cache_menu}.
+    $cache = cache_get($cid, 'cache_menu');
+    if ($cache && isset($cache->data)) {
+      // If the cache entry exists, it contains the parameters for
+      // menu_build_tree().
+      $tree_parameters = $cache->data;
+    }
+    // If the tree data was not in the cache, build $tree_parameters.
+    if (!isset($tree_parameters)) {
+      $tree_parameters = array(
+        'min_depth' => 1,
+        'max_depth' => $max_depth,
+      );
+      if ($mlid) {
+        // The tree is for a single item, so we need to match the values in its
+        // p columns and 0 (the top level) with the plid values of other links.
+        $parents = array(0);
+        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
+          if (!empty($link["p$i"])) {
+            $parents[] = $link["p$i"];
+          }
+        }
+        $tree_parameters['expanded'] = $parents;
+        $tree_parameters['active_trail'] = $parents;
+        $tree_parameters['active_trail'][] = $mlid;
+      }
+
+      // Cache the tree building parameters using the page-specific cid.
+      cache_set($cid, $tree_parameters, 'cache_menu');
+    }
+
+    // Build the tree using the parameters; the resulting tree will be cached
+    // by _menu_build_tree()).
+    $tree[$cid] = menu_build_tree($menu_name, $tree_parameters);
+  }
+
+  return $tree[$cid];
+}
+
+/**
+ * Sets the path for determining the active trail of the specified menu tree.
+ *
+ * This path will also affect the breadcrumbs under some circumstances.
+ * Breadcrumbs are built using the preferred link returned by
+ * menu_link_get_preferred(). If the preferred link is inside one of the menus
+ * specified in calls to menu_tree_set_path(), the preferred link will be
+ * overridden by the corresponding path returned by menu_tree_get_path().
+ *
+ * Setting this path does not affect the main content; for that use
+ * menu_set_active_item() instead.
+ *
+ * @param $menu_name
+ *   The name of the affected menu tree.
+ * @param $path
+ *   The path to use when finding the active trail.
+ */
+function menu_tree_set_path($menu_name, $path = NULL) {
+  $paths = &drupal_static(__FUNCTION__);
+  if (isset($path)) {
+    $paths[$menu_name] = $path;
+  }
+  return isset($paths[$menu_name]) ? $paths[$menu_name] : NULL;
+}
+
+/**
+ * Gets the path for determining the active trail of the specified menu tree.
+ *
+ * @param $menu_name
+ *   The menu name of the requested tree.
+ *
+ * @return
+ *   A string containing the path. If no path has been specified with
+ *   menu_tree_set_path(), NULL is returned.
+ */
+function menu_tree_get_path($menu_name) {
+  return menu_tree_set_path($menu_name);
+}
+
+/**
+ * Gets the data structure for a named menu tree, based on the current page.
+ *
+ * The tree order is maintained by storing each parent in an individual
+ * field, see http://drupal.org/node/141866 for more.
+ *
+ * @param $menu_name
+ *   The named menu links to return.
+ * @param $max_depth
+ *   (optional) The maximum depth of links to retrieve.
+ * @param $only_active_trail
+ *   (optional) Whether to only return the links in the active trail (TRUE)
+ *   instead of all links on every level of the menu link tree (FALSE). Defaults
+ *   to FALSE. Internally used for breadcrumbs only.
+ *
+ * @return
+ *   An array of menu links, in the order they should be rendered. The array
+ *   is a list of associative arrays -- these have two keys, link and below.
+ *   link is a menu item, ready for theming as a link. Below represents the
+ *   submenu below the link if there is one, and it is a subtree that has the
+ *   same structure described for the top-level array.
+ */
+function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
+  $tree = &drupal_static(__FUNCTION__, array());
+
+  // Check if the active trail has been overridden for this menu tree.
+  $active_path = menu_tree_get_path($menu_name);
+  // Load the menu item corresponding to the current page.
+  if ($item = menu_get_item($active_path)) {
+    if (isset($max_depth)) {
+      $max_depth = min($max_depth, MENU_MAX_DEPTH);
+    }
+    // Generate a cache ID (cid) specific for this page.
+    $cid = 'links:' . $menu_name . ':page:' . $item['href'] . ':' . $GLOBALS['language']->language . ':' . (int) $item['access'] . ':' . (int) $max_depth;
+    // If we are asked for the active trail only, and $menu_name has not been
+    // built and cached for this page yet, then this likely means that it
+    // won't be built anymore, as this function is invoked from
+    // template_process_page(). So in order to not build a giant menu tree
+    // that needs to be checked for access on all levels, we simply check
+    // whether we have the menu already in cache, or otherwise, build a minimum
+    // tree containing the breadcrumb/active trail only.
+    // @see menu_set_active_trail()
+    if (!isset($tree[$cid]) && $only_active_trail) {
+      $cid .= ':trail';
+    }
+
+    if (!isset($tree[$cid])) {
+      // If the static variable doesn't have the data, check {cache_menu}.
+      $cache = cache_get($cid, 'cache_menu');
+      if ($cache && isset($cache->data)) {
+        // If the cache entry exists, it contains the parameters for
+        // menu_build_tree().
+        $tree_parameters = $cache->data;
+      }
+      // If the tree data was not in the cache, build $tree_parameters.
+      if (!isset($tree_parameters)) {
+        $tree_parameters = array(
+          'min_depth' => 1,
+          'max_depth' => $max_depth,
+        );
+        // Parent mlids; used both as key and value to ensure uniqueness.
+        // We always want all the top-level links with plid == 0.
+        $active_trail = array(0 => 0);
+
+        // If the item for the current page is accessible, build the tree
+        // parameters accordingly.
+        if ($item['access']) {
+          // Find a menu link corresponding to the current path. If $active_path
+          // is NULL, let menu_link_get_preferred() determine the path.
+          if ($active_link = menu_link_get_preferred($active_path, $menu_name)) {
+            // The active link may only be taken into account to build the
+            // active trail, if it resides in the requested menu. Otherwise,
+            // we'd needlessly re-run _menu_build_tree() queries for every menu
+            // on every page.
+            if ($active_link['menu_name'] == $menu_name) {
+              // Use all the coordinates, except the last one because there
+              // can be no child beyond the last column.
+              for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
+                if ($active_link['p' . $i]) {
+                  $active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
+                }
+              }
+              // If we are asked to build links for the active trail only, skip
+              // the entire 'expanded' handling.
+              if ($only_active_trail) {
+                $tree_parameters['only_active_trail'] = TRUE;
+              }
+            }
+          }
+          $parents = $active_trail;
+
+          $expanded = variable_get('menu_expanded', array());
+          // Check whether the current menu has any links set to be expanded.
+          if (!$only_active_trail && in_array($menu_name, $expanded)) {
+            // Collect all the links set to be expanded, and then add all of
+            // their children to the list as well.
+            do {
+              $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
+                ->fields('menu_links', array('mlid'))
+                ->condition('menu_name', $menu_name)
+                ->condition('expanded', 1)
+                ->condition('has_children', 1)
+                ->condition('plid', $parents, 'IN')
+                ->condition('mlid', $parents, 'NOT IN')
+                ->execute();
+              $num_rows = FALSE;
+              foreach ($result as $item) {
+                $parents[$item['mlid']] = $item['mlid'];
+                $num_rows = TRUE;
+              }
+            } while ($num_rows);
+          }
+          $tree_parameters['expanded'] = $parents;
+          $tree_parameters['active_trail'] = $active_trail;
+        }
+        // If access is denied, we only show top-level links in menus.
+        else {
+          $tree_parameters['expanded'] = $active_trail;
+          $tree_parameters['active_trail'] = $active_trail;
+        }
+        // Cache the tree building parameters using the page-specific cid.
+        cache_set($cid, $tree_parameters, 'cache_menu');
+      }
+
+      // Build the tree using the parameters; the resulting tree will be cached
+      // by _menu_build_tree().
+      $tree[$cid] = menu_build_tree($menu_name, $tree_parameters);
+    }
+    return $tree[$cid];
+  }
+
+  return array();
+}
+
+/**
+ * Builds a menu tree, translates links, and checks access.
+ *
+ * @param $menu_name
+ *   The name of the menu.
+ * @param $parameters
+ *   (optional) An associative array of build parameters. Possible keys:
+ *   - expanded: An array of parent link ids to return only menu links that are
+ *     children of one of the plids in this list. If empty, the whole menu tree
+ *     is built, unless 'only_active_trail' is TRUE.
+ *   - active_trail: An array of mlids, representing the coordinates of the
+ *     currently active menu link.
+ *   - only_active_trail: Whether to only return links that are in the active
+ *     trail. This option is ignored, if 'expanded' is non-empty. Internally
+ *     used for breadcrumbs.
+ *   - min_depth: The minimum depth of menu links in the resulting tree.
+ *     Defaults to 1, which is the default to build a whole tree for a menu
+ *     (excluding menu container itself).
+ *   - max_depth: The maximum depth of menu links in the resulting tree.
+ *   - conditions: An associative array of custom database select query
+ *     condition key/value pairs; see _menu_build_tree() for the actual query.
+ *
+ * @return
+ *   A fully built menu tree.
+ */
+function menu_build_tree($menu_name, array $parameters = array()) {
+  // Build the menu tree.
+  $data = _menu_build_tree($menu_name, $parameters);
+  // Check access for the current user to each item in the tree.
+  menu_tree_check_access($data['tree'], $data['node_links']);
+  return $data['tree'];
+}
+
+/**
+ * Builds a menu tree.
+ *
+ * This function may be used build the data for a menu tree only, for example
+ * to further massage the data manually before further processing happens.
+ * menu_tree_check_access() needs to be invoked afterwards.
+ *
+ * @see menu_build_tree()
+ */
+function _menu_build_tree($menu_name, array $parameters = array()) {
+  // Static cache of already built menu trees.
+  $trees = &drupal_static(__FUNCTION__, array());
+
+  // Build the cache id; sort parents to prevent duplicate storage and remove
+  // default parameter values.
+  if (isset($parameters['expanded'])) {
+    sort($parameters['expanded']);
+  }
+  $tree_cid = 'links:' . $menu_name . ':tree-data:' . $GLOBALS['language']->language . ':' . hash('sha256', serialize($parameters));
+
+  // If we do not have this tree in the static cache, check {cache_menu}.
+  if (!isset($trees[$tree_cid])) {
+    $cache = cache_get($tree_cid, 'cache_menu');
+    if ($cache && isset($cache->data)) {
+      $trees[$tree_cid] = $cache->data;
+    }
+  }
+
+  if (!isset($trees[$tree_cid])) {
+    // Select the links from the table, and recursively build the tree. We
+    // LEFT JOIN since there is no match in {menu_router} for an external
+    // link.
+    $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
+    $query->addTag('translatable');
+    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
+    $query->fields('ml');
+    $query->fields('m', array(
+      'load_functions',
+      'to_arg_functions',
+      'access_callback',
+      'access_arguments',
+      'page_callback',
+      'page_arguments',
+      'delivery_callback',
+      'tab_parent',
+      'tab_root',
+      'title',
+      'title_callback',
+      'title_arguments',
+      'theme_callback',
+      'theme_arguments',
+      'type',
+      'description',
+    ));
+    for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
+      $query->orderBy('p' . $i, 'ASC');
+    }
+    $query->condition('ml.menu_name', $menu_name);
+    if (!empty($parameters['expanded'])) {
+      $query->condition('ml.plid', $parameters['expanded'], 'IN');
+    }
+    elseif (!empty($parameters['only_active_trail'])) {
+      $query->condition('ml.mlid', $parameters['active_trail'], 'IN');
+    }
+    $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
+    if ($min_depth != 1) {
+      $query->condition('ml.depth', $min_depth, '>=');
+    }
+    if (isset($parameters['max_depth'])) {
+      $query->condition('ml.depth', $parameters['max_depth'], '<=');
+    }
+    // Add custom query conditions, if any were passed.
+    if (isset($parameters['conditions'])) {
+      foreach ($parameters['conditions'] as $column => $value) {
+        $query->condition($column, $value);
+      }
+    }
+
+    // Build an ordered array of links using the query result object.
+    $links = array();
+    foreach ($query->execute() as $item) {
+      $links[] = $item;
+    }
+    $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
+    $data['tree'] = menu_tree_data($links, $active_trail, $min_depth);
+    $data['node_links'] = array();
+    menu_tree_collect_node_links($data['tree'], $data['node_links']);
+
+    // Cache the data, if it is not already in the cache.
+    cache_set($tree_cid, $data, 'cache_menu');
+    $trees[$tree_cid] = $data;
+  }
+
+  return $trees[$tree_cid];
+}
+
+/**
+ * Collects node links from a given menu tree recursively.
+ *
+ * @param $tree
+ *   The menu tree you wish to collect node links from.
+ * @param $node_links
+ *   An array in which to store the collected node links.
+ */
+function menu_tree_collect_node_links(&$tree, &$node_links) {
+  foreach ($tree as $key => $v) {
+    if ($tree[$key]['link']['router_path'] == 'node/%') {
+      $nid = substr($tree[$key]['link']['link_path'], 5);
+      if (is_numeric($nid)) {
+        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
+        $tree[$key]['link']['access'] = FALSE;
+      }
+    }
+    if ($tree[$key]['below']) {
+      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
+    }
+  }
+}
+
+/**
+ * Checks access and performs dynamic operations for each link in the tree.
+ *
+ * @param $tree
+ *   The menu tree you wish to operate on.
+ * @param $node_links
+ *   A collection of node link references generated from $tree by
+ *   menu_tree_collect_node_links().
+ */
+function menu_tree_check_access(&$tree, $node_links = array()) {
+  if ($node_links) {
+    $nids = array_keys($node_links);
+    $select = db_select('node', 'n');
+    $select->addField('n', 'nid');
+    $select->condition('n.status', 1);
+    $select->condition('n.nid', $nids, 'IN');
+    $select->addTag('node_access');
+    $nids = $select->execute()->fetchCol();
+    foreach ($nids as $nid) {
+      foreach ($node_links[$nid] as $mlid => $link) {
+        $node_links[$nid][$mlid]['access'] = TRUE;
+      }
+    }
+  }
+  _menu_tree_check_access($tree);
+}
+
+/**
+ * Sorts the menu tree and recursively checks access for each item.
+ */
+function _menu_tree_check_access(&$tree) {
+  $new_tree = array();
+  foreach ($tree as $key => $v) {
+    $item = &$tree[$key]['link'];
+    _menu_link_translate($item);
+    if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) {
+      if ($tree[$key]['below']) {
+        _menu_tree_check_access($tree[$key]['below']);
+      }
+      // The weights are made a uniform 5 digits by adding 50000 as an offset.
+      // After _menu_link_translate(), $item['title'] has the localized link title.
+      // Adding the mlid to the end of the index insures that it is unique.
+      $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key];
+    }
+  }
+  // Sort siblings in the tree based on the weights and localized titles.
+  ksort($new_tree);
+  $tree = $new_tree;
+}
+
+/**
+ * Sorts and returns the built data representing a menu tree.
+ *
+ * @param $links
+ *   A flat array of menu links that are part of the menu. Each array element
+ *   is an associative array of information about the menu link, containing the
+ *   fields from the {menu_links} table, and optionally additional information
+ *   from the {menu_router} table, if the menu item appears in both tables.
+ *   This array must be ordered depth-first. See _menu_build_tree() for a sample
+ *   query.
+ * @param $parents
+ *   An array of the menu link ID values that are in the path from the current
+ *   page to the root of the menu tree.
+ * @param $depth
+ *   The minimum depth to include in the returned menu tree.
+ *
+ * @return
+ *   An array of menu links in the form of a tree. Each item in the tree is an
+ *   associative array containing:
+ *   - link: The menu link item from $links, with additional element
+ *     'in_active_trail' (TRUE if the link ID was in $parents).
+ *   - below: An array containing the sub-tree of this item, where each element
+ *     is a tree item array with 'link' and 'below' elements. This array will be
+ *     empty if the menu item has no items in its sub-tree having a depth
+ *     greater than or equal to $depth.
+ */
+function menu_tree_data(array $links, array $parents = array(), $depth = 1) {
+  // Reverse the array so we can use the more efficient array_pop() function.
+  $links = array_reverse($links);
+  return _menu_tree_data($links, $parents, $depth);
+}
+
+/**
+ * Builds the data representing a menu tree.
+ *
+ * The function is a bit complex because the rendering of a link depends on
+ * the next menu link.
+ */
+function _menu_tree_data(&$links, $parents, $depth) {
+  $tree = array();
+  while ($item = array_pop($links)) {
+    // We need to determine if we're on the path to root so we can later build
+    // the correct active trail and breadcrumb.
+    $item['in_active_trail'] = in_array($item['mlid'], $parents);
+    // Add the current link to the tree.
+    $tree[$item['mlid']] = array(
+      'link' => $item,
+      'below' => array(),
+    );
+    // Look ahead to the next link, but leave it on the array so it's available
+    // to other recursive function calls if we return or build a sub-tree.
+    $next = end($links);
+    // Check whether the next link is the first in a new sub-tree.
+    if ($next && $next['depth'] > $depth) {
+      // Recursively call _menu_tree_data to build the sub-tree.
+      $tree[$item['mlid']]['below'] = _menu_tree_data($links, $parents, $next['depth']);
+      // Fetch next link after filling the sub-tree.
+      $next = end($links);
+    }
+    // Determine if we should exit the loop and return.
+    if (!$next || $next['depth'] < $depth) {
+      break;
+    }
+  }
+  return $tree;
+}
+
+/**
+ * Implements template_preprocess_HOOK() for theme_menu_tree().
+ */
+function template_preprocess_menu_tree(&$variables) {
+  $variables['tree'] = $variables['tree']['#children'];
+}
+
+/**
+ * Returns HTML for a wrapper for a menu sub-tree.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - tree: An HTML string containing the tree's items.
+ *
+ * @see template_preprocess_menu_tree()
+ * @ingroup themeable
+ */
+function theme_menu_tree($variables) {
+  return '<ul class="menu">' . $variables['tree'] . '</ul>';
+}
+
+/**
+ * Returns HTML for a menu link and submenu.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: Structured array data for a menu link.
+ *
+ * @ingroup themeable
+ */
+function theme_menu_link(array $variables) {
+  $element = $variables['element'];
+  $sub_menu = '';
+
+  if ($element['#below']) {
+    $sub_menu = drupal_render($element['#below']);
+  }
+  $output = l($element['#title'], $element['#href'], $element['#localized_options']);
+  return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
+}
+
+/**
+ * Returns HTML for a single local task link.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element containing:
+ *     - #link: A menu link array with 'title', 'href', and 'localized_options'
+ *       keys.
+ *     - #active: A boolean indicating whether the local task is active.
+ *
+ * @ingroup themeable
+ */
+function theme_menu_local_task($variables) {
+  $link = $variables['element']['#link'];
+  $link_text = $link['title'];
+
+  if (!empty($variables['element']['#active'])) {
+    // Add text to indicate active tab for non-visual users.
+    $active = '<span class="element-invisible">' . t('(active tab)') . '</span>';
+
+    // If the link does not contain HTML already, check_plain() it now.
+    // After we set 'html'=TRUE the link will not be sanitized by l().
+    if (empty($link['localized_options']['html'])) {
+      $link['title'] = check_plain($link['title']);
+    }
+    $link['localized_options']['html'] = TRUE;
+    $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active));
+  }
+
+  return '<li' . (!empty($variables['element']['#active']) ? ' class="active"' : '') . '>' . l($link_text, $link['href'], $link['localized_options']) . "</li>\n";
+}
+
+/**
+ * Returns HTML for a single local action link.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element containing:
+ *     - #link: A menu link array with 'title', 'href', and 'localized_options'
+ *       keys.
+ *
+ * @ingroup themeable
+ */
+function theme_menu_local_action($variables) {
+  $link = $variables['element']['#link'];
+
+  $output = '<li>';
+  if (isset($link['href'])) {
+    $output .= l($link['title'], $link['href'], isset($link['localized_options']) ? $link['localized_options'] : array());
+  }
+  elseif (!empty($link['localized_options']['html'])) {
+    $output .= $link['title'];
+  }
+  else {
+    $output .= check_plain($link['title']);
+  }
+  $output .= "</li>\n";
+
+  return $output;
+}
+
+/**
+ * Generates elements for the $arg array in the help hook.
+ */
+function drupal_help_arg($arg = array()) {
+  // Note - the number of empty elements should be > MENU_MAX_PARTS.
+  return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
+}
+
+/**
+ * Returns the help associated with the active menu item.
+ */
+function menu_get_active_help() {
+  $output = '';
+  $router_path = menu_tab_root_path();
+  // We will always have a path unless we are on a 403 or 404.
+  if (!$router_path) {
+    return '';
+  }
+
+  $arg = drupal_help_arg(arg(NULL));
+
+  foreach (module_implements('help') as $module) {
+    $function = $module . '_help';
+    // Lookup help for this path.
+    if ($help = $function($router_path, $arg)) {
+      $output .= $help . "\n";
+    }
+  }
+  return $output;
+}
+
+/**
+ * Gets the custom theme for the current page, if there is one.
+ *
+ * @param $initialize
+ *   This parameter should only be used internally; it is set to TRUE in order
+ *   to force the custom theme to be initialized for the current page request.
+ *
+ * @return
+ *   The machine-readable name of the custom theme, if there is one.
+ *
+ * @see menu_set_custom_theme()
+ */
+function menu_get_custom_theme($initialize = FALSE) {
+  $custom_theme = &drupal_static(__FUNCTION__);
+  // Skip this if the site is offline or being installed or updated, since the
+  // menu system may not be correctly initialized then.
+  if ($initialize && !_menu_site_is_offline(TRUE) && (!defined('MAINTENANCE_MODE') || (MAINTENANCE_MODE != 'update' && MAINTENANCE_MODE != 'install'))) {
+    // First allow modules to dynamically set a custom theme for the current
+    // page. Since we can only have one, the last module to return a valid
+    // theme takes precedence.
+    $custom_themes = array_filter(module_invoke_all('custom_theme'), 'drupal_theme_access');
+    if (!empty($custom_themes)) {
+      $custom_theme = array_pop($custom_themes);
+    }
+    // If there is a theme callback function for the current page, execute it.
+    // If this returns a valid theme, it will override any theme that was set
+    // by a hook_custom_theme() implementation above.
+    $router_item = menu_get_item();
+    if (!empty($router_item['access']) && !empty($router_item['theme_callback']) && function_exists($router_item['theme_callback'])) {
+      $theme_name = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']);
+      if (drupal_theme_access($theme_name)) {
+        $custom_theme = $theme_name;
+      }
+    }
+  }
+  return $custom_theme;
+}
+
+/**
+ * Sets a custom theme for the current page, if there is one.
+ */
+function menu_set_custom_theme() {
+  menu_get_custom_theme(TRUE);
+}
+
+/**
+ * Build a list of named menus.
+ */
+function menu_get_names() {
+  $names = &drupal_static(__FUNCTION__);
+
+  if (empty($names)) {
+    $names = db_select('menu_links')
+      ->distinct()
+      ->fields('menu_links', array('menu_name'))
+      ->orderBy('menu_name')
+      ->execute()->fetchCol();
+  }
+  return $names;
+}
+
+/**
+ * Returns an array containing the names of system-defined (default) menus.
+ */
+function menu_list_system_menus() {
+  return array(
+    'navigation' => 'Navigation',
+    'management' => 'Management',
+    'user-menu' => 'User menu',
+    'main-menu' => 'Main menu',
+  );
+}
+
+/**
+ * Returns an array of links to be rendered as the Main menu.
+ */
+function menu_main_menu() {
+  return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu'));
+}
+
+/**
+ * Returns an array of links to be rendered as the Secondary links.
+ */
+function menu_secondary_menu() {
+
+  // If the secondary menu source is set as the primary menu, we display the
+  // second level of the primary menu.
+  if (variable_get('menu_secondary_links_source', 'user-menu') == variable_get('menu_main_links_source', 'main-menu')) {
+    return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu'), 1);
+  }
+  else {
+    return menu_navigation_links(variable_get('menu_secondary_links_source', 'user-menu'), 0);
+  }
+}
+
+/**
+ * Returns an array of links for a navigation menu.
+ *
+ * @param $menu_name
+ *   The name of the menu.
+ * @param $level
+ *   Optional, the depth of the menu to be returned.
+ *
+ * @return
+ *   An array of links of the specified menu and level.
+ */
+function menu_navigation_links($menu_name, $level = 0) {
+  // Don't even bother querying the menu table if no menu is specified.
+  if (empty($menu_name)) {
+    return array();
+  }
+
+  // Get the menu hierarchy for the current page.
+  $tree = menu_tree_page_data($menu_name, $level + 1);
+
+  // Go down the active trail until the right level is reached.
+  while ($level-- > 0 && $tree) {
+    // Loop through the current level's items until we find one that is in trail.
+    while ($item = array_shift($tree)) {
+      if ($item['link']['in_active_trail']) {
+        // If the item is in the active trail, we continue in the subtree.
+        $tree = empty($item['below']) ? array() : $item['below'];
+        break;
+      }
+    }
+  }
+
+  // Create a single level of links.
+  $router_item = menu_get_item();
+  $links = array();
+  foreach ($tree as $item) {
+    if (!$item['link']['hidden']) {
+      $class = '';
+      $l = $item['link']['localized_options'];
+      $l['href'] = $item['link']['href'];
+      $l['title'] = $item['link']['title'];
+      if ($item['link']['in_active_trail']) {
+        $class = ' active-trail';
+        $l['attributes']['class'][] = 'active-trail';
+      }
+      // Normally, l() compares the href of every link with $_GET['q'] and sets
+      // the active class accordingly. But local tasks do not appear in menu
+      // trees, so if the current path is a local task, and this link is its
+      // tab root, then we have to set the class manually.
+      if ($item['link']['href'] == $router_item['tab_root_href'] && $item['link']['href'] != $_GET['q']) {
+        $l['attributes']['class'][] = 'active';
+      }
+      // Keyed with the unique mlid to generate classes in theme_links().
+      $links['menu-' . $item['link']['mlid'] . $class] = $l;
+    }
+  }
+  return $links;
+}
+
+/**
+ * Collects the local tasks (tabs), action links, and the root path.
+ *
+ * @param $level
+ *   The level of tasks you ask for. Primary tasks are 0, secondary are 1.
+ *
+ * @return
+ *   An array containing
+ *   - tabs: Local tasks for the requested level:
+ *     - count: The number of local tasks.
+ *     - output: The themed output of local tasks.
+ *   - actions: Action links for the requested level:
+ *     - count: The number of action links.
+ *     - output: The themed output of action links.
+ *   - root_path: The router path for the current page. If the current page is
+ *     a default local task, then this corresponds to the parent tab.
+ */
+function menu_local_tasks($level = 0) {
+  $data = &drupal_static(__FUNCTION__);
+  $root_path = &drupal_static(__FUNCTION__ . ':root_path', '');
+  $empty = array(
+    'tabs' => array('count' => 0, 'output' => array()),
+    'actions' => array('count' => 0, 'output' => array()),
+    'root_path' => &$root_path,
+  );
+
+  if (!isset($data)) {
+    $data = array();
+    // Set defaults in case there are no actions or tabs.
+    $actions = $empty['actions'];
+    $tabs = array();
+
+    $router_item = menu_get_item();
+
+    // If this router item points to its parent, start from the parents to
+    // compute tabs and actions.
+    if ($router_item && ($router_item['type'] & MENU_LINKS_TO_PARENT)) {
+      $router_item = menu_get_item($router_item['tab_parent_href']);
+    }
+
+    // If we failed to fetch a router item or the current user doesn't have
+    // access to it, don't bother computing the tabs.
+    if (!$router_item || !$router_item['access']) {
+      return $empty;
+    }
+
+    // Get all tabs (also known as local tasks) and the root page.
+    $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC))
+      ->fields('menu_router')
+      ->condition('tab_root', $router_item['tab_root'])
+      ->condition('context', MENU_CONTEXT_INLINE, '<>')
+      ->orderBy('weight')
+      ->orderBy('title')
+      ->execute();
+    $map = $router_item['original_map'];
+    $children = array();
+    $tasks = array();
+    $root_path = $router_item['path'];
+
+    foreach ($result as $item) {
+      _menu_translate($item, $map, TRUE);
+      if ($item['tab_parent']) {
+        // All tabs, but not the root page.
+        $children[$item['tab_parent']][$item['path']] = $item;
+      }
+      // Store the translated item for later use.
+      $tasks[$item['path']] = $item;
+    }
+
+    // Find all tabs below the current path.
+    $path = $router_item['path'];
+    // Tab parenting may skip levels, so the number of parts in the path may not
+    // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort).
+    $depth = 1001;
+    $actions['count'] = 0;
+    $actions['output'] = array();
+    while (isset($children[$path])) {
+      $tabs_current = array();
+      $actions_current = array();
+      $next_path = '';
+      $tab_count = 0;
+      $action_count = 0;
+      foreach ($children[$path] as $item) {
+        // Local tasks can be normal items too, so bitmask with
+        // MENU_IS_LOCAL_TASK before checking.
+        if (!($item['type'] & MENU_IS_LOCAL_TASK)) {
+          // This item is not a tab, skip it.
+          continue;
+        }
+        if ($item['access']) {
+          $link = $item;
+          // The default task is always active. As tabs can be normal items
+          // too, so bitmask with MENU_LINKS_TO_PARENT before checking.
+          if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
+            // Find the first parent which is not a default local task or action.
+            for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
+            // Use the path of the parent instead.
+            $link['href'] = $tasks[$p]['href'];
+            // Mark the link as active, if the current path happens to be the
+            // path of the default local task itself (i.e., instead of its
+            // tab_parent_href or tab_root_href). Normally, links for default
+            // local tasks link to their parent, but the path of default local
+            // tasks can still be accessed directly, in which case this link
+            // would not be marked as active, since l() only compares the href
+            // with $_GET['q'].
+            if ($link['href'] != $_GET['q']) {
+              $link['localized_options']['attributes']['class'][] = 'active';
+            }
+            $tabs_current[] = array(
+              '#theme' => 'menu_local_task',
+              '#link' => $link,
+              '#active' => TRUE,
+            );
+            $next_path = $item['path'];
+            $tab_count++;
+          }
+          else {
+            // Actions can be normal items too, so bitmask with
+            // MENU_IS_LOCAL_ACTION before checking.
+            if (($item['type'] & MENU_IS_LOCAL_ACTION) == MENU_IS_LOCAL_ACTION) {
+              // The item is an action, display it as such.
+              $actions_current[] = array(
+                '#theme' => 'menu_local_action',
+                '#link' => $link,
+              );
+              $action_count++;
+            }
+            else {
+              // Otherwise, it's a normal tab.
+              $tabs_current[] = array(
+                '#theme' => 'menu_local_task',
+                '#link' => $link,
+              );
+              $tab_count++;
+            }
+          }
+        }
+      }
+      $path = $next_path;
+      $tabs[$depth]['count'] = $tab_count;
+      $tabs[$depth]['output'] = $tabs_current;
+      $actions['count'] += $action_count;
+      $actions['output'] = array_merge($actions['output'], $actions_current);
+      $depth++;
+    }
+    $data['actions'] = $actions;
+    // Find all tabs at the same level or above the current one.
+    $parent = $router_item['tab_parent'];
+    $path = $router_item['path'];
+    $current = $router_item;
+    $depth = 1000;
+    while (isset($children[$parent])) {
+      $tabs_current = array();
+      $next_path = '';
+      $next_parent = '';
+      $count = 0;
+      foreach ($children[$parent] as $item) {
+        // Skip local actions.
+        if ($item['type'] & MENU_IS_LOCAL_ACTION) {
+          continue;
+        }
+        if ($item['access']) {
+          $count++;
+          $link = $item;
+          // Local tasks can be normal items too, so bitmask with
+          // MENU_LINKS_TO_PARENT before checking.
+          if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
+            // Find the first parent which is not a default local task.
+            for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
+            // Use the path of the parent instead.
+            $link['href'] = $tasks[$p]['href'];
+            if ($item['path'] == $router_item['path']) {
+              $root_path = $tasks[$p]['path'];
+            }
+          }
+          // We check for the active tab.
+          if ($item['path'] == $path) {
+            // Mark the link as active, if the current path is a (second-level)
+            // local task of a default local task. Since this default local task
+            // links to its parent, l() will not mark it as active, as it only
+            // compares the link's href to $_GET['q'].
+            if ($link['href'] != $_GET['q']) {
+              $link['localized_options']['attributes']['class'][] = 'active';
+            }
+            $tabs_current[] = array(
+              '#theme' => 'menu_local_task',
+              '#link' => $link,
+              '#active' => TRUE,
+            );
+            $next_path = $item['tab_parent'];
+            if (isset($tasks[$next_path])) {
+              $next_parent = $tasks[$next_path]['tab_parent'];
+            }
+          }
+          else {
+            $tabs_current[] = array(
+              '#theme' => 'menu_local_task',
+              '#link' => $link,
+            );
+          }
+        }
+      }
+      $path = $next_path;
+      $parent = $next_parent;
+      $tabs[$depth]['count'] = $count;
+      $tabs[$depth]['output'] = $tabs_current;
+      $depth--;
+    }
+    // Sort by depth.
+    ksort($tabs);
+    // Remove the depth, we are interested only in their relative placement.
+    $tabs = array_values($tabs);
+    $data['tabs'] = $tabs;
+
+    // Allow modules to alter local tasks or dynamically append further tasks.
+    drupal_alter('menu_local_tasks', $data, $router_item, $root_path);
+  }
+
+  if (isset($data['tabs'][$level])) {
+    return array(
+      'tabs' => $data['tabs'][$level],
+      'actions' => $data['actions'],
+      'root_path' => $root_path,
+    );
+  }
+  // @todo If there are no tabs, then there still can be actions; for example,
+  //   when added via hook_menu_local_tasks_alter().
+  elseif (!empty($data['actions']['output'])) {
+    return array('actions' => $data['actions']) + $empty;
+  }
+  return $empty;
+}
+
+/**
+ * Retrieves contextual links for a path based on registered local tasks.
+ *
+ * This leverages the menu system to retrieve the first layer of registered
+ * local tasks for a given system path. All local tasks of the tab type
+ * MENU_CONTEXT_INLINE are taken into account.
+ *
+ * For example, when considering the following registered local tasks:
+ * - node/%node/view (default local task) with no 'context' defined
+ * - node/%node/edit with context: MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE
+ * - node/%node/revisions with context: MENU_CONTEXT_PAGE
+ * - node/%node/report-as-spam with context: MENU_CONTEXT_INLINE
+ *
+ * If the path "node/123" is passed to this function, then it will return the
+ * links for 'edit' and 'report-as-spam'.
+ *
+ * @param $module
+ *   The name of the implementing module. This is used to prefix the key for
+ *   each contextual link, which is transformed into a CSS class during
+ *   rendering by theme_links(). For example, if $module is 'block' and the
+ *   retrieved local task path argument is 'edit', then the resulting CSS class
+ *   will be 'block-edit'.
+ * @param $parent_path
+ *   The static menu router path of the object to retrieve local tasks for, for
+ *   example 'node' or 'admin/structure/block/manage'.
+ * @param $args
+ *   A list of dynamic path arguments to append to $parent_path to form the
+ *   fully-qualified menu router path, for example array(123) for a certain
+ *   node or array('system', 'navigation') for a certain block.
+ *
+ * @return
+ *   A list of menu router items that are local tasks for the passed-in path.
+ *
+ * @see contextual_links_preprocess()
+ * @see hook_menu()
+ */
+function menu_contextual_links($module, $parent_path, $args) {
+  static $path_empty = array();
+
+  $links = array();
+  // Performance: In case a previous invocation for the same parent path did not
+  // return any links, we immediately return here.
+  if (isset($path_empty[$parent_path]) && strpos($parent_path, '%') !== FALSE) {
+    return $links;
+  }
+  // Construct the item-specific parent path.
+  $path = $parent_path . '/' . implode('/', $args);
+
+  // Get the router item for the given parent link path.
+  $router_item = menu_get_item($path);
+  if (!$router_item || !$router_item['access']) {
+    $path_empty[$parent_path] = TRUE;
+    return $links;
+  }
+  $data = &drupal_static(__FUNCTION__, array());
+  $root_path = $router_item['path'];
+
+  // Performance: For a single, normalized path (such as 'node/%') we only query
+  // available tasks once per request.
+  if (!isset($data[$root_path])) {
+    // Get all contextual links that are direct children of the router item and
+    // not of the tab type 'view'.
+    $data[$root_path] = db_select('menu_router', 'm')
+      ->fields('m')
+      ->condition('tab_parent', $router_item['tab_root'])
+      ->condition('context', MENU_CONTEXT_NONE, '<>')
+      ->condition('context', MENU_CONTEXT_PAGE, '<>')
+      ->orderBy('weight')
+      ->orderBy('title')
+      ->execute()
+      ->fetchAllAssoc('path', PDO::FETCH_ASSOC);
+  }
+  $parent_length = drupal_strlen($root_path) + 1;
+  $map = $router_item['original_map'];
+  foreach ($data[$root_path] as $item) {
+    // Extract the actual "task" string from the path argument.
+    $key = drupal_substr($item['path'], $parent_length);
+
+    // Denormalize and translate the contextual link.
+    _menu_translate($item, $map, TRUE);
+    if (!$item['access']) {
+      continue;
+    }
+    // All contextual links are keyed by the actual "task" path argument,
+    // prefixed with the name of the implementing module.
+    $links[$module . '-' . $key] = $item;
+  }
+
+  // Allow modules to alter contextual links.
+  drupal_alter('menu_contextual_links', $links, $router_item, $root_path);
+
+  // Performance: If the current user does not have access to any links for this
+  // router path and no other module added further links, we assign FALSE here
+  // to skip the entire process the next time the same router path is requested.
+  if (empty($links)) {
+    $path_empty[$parent_path] = TRUE;
+  }
+
+  return $links;
+}
+
+/**
+ * Returns the rendered local tasks at the top level.
+ */
+function menu_primary_local_tasks() {
+  $links = menu_local_tasks(0);
+  // Do not display single tabs.
+  return ($links['tabs']['count'] > 1 ? $links['tabs']['output'] : '');
+}
+
+/**
+ * Returns the rendered local tasks at the second level.
+ */
+function menu_secondary_local_tasks() {
+  $links = menu_local_tasks(1);
+  // Do not display single tabs.
+  return ($links['tabs']['count'] > 1 ? $links['tabs']['output'] : '');
+}
+
+/**
+ * Returns the rendered local actions at the current level.
+ */
+function menu_local_actions() {
+  $links = menu_local_tasks();
+  return $links['actions']['output'];
+}
+
+/**
+ * Returns the router path, or the path for a default local task's parent.
+ */
+function menu_tab_root_path() {
+  $links = menu_local_tasks();
+  return $links['root_path'];
+}
+
+/**
+ * Returns a renderable element for the primary and secondary tabs.
+ */
+function menu_local_tabs() {
+  return array(
+    '#theme' => 'menu_local_tasks',
+    '#primary' => menu_primary_local_tasks(),
+    '#secondary' => menu_secondary_local_tasks(),
+  );
+}
+
+/**
+ * Returns HTML for primary and secondary local tasks.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *     - primary: (optional) An array of local tasks (tabs).
+ *     - secondary: (optional) An array of local tasks (tabs).
+ *
+ * @ingroup themeable
+ * @see menu_local_tasks()
+ */
+function theme_menu_local_tasks(&$variables) {
+  $output = '';
+
+  if (!empty($variables['primary'])) {
+    $variables['primary']['#prefix'] = '<h2 class="element-invisible">' . t('Primary tabs') . '</h2>';
+    $variables['primary']['#prefix'] .= '<ul class="tabs primary">';
+    $variables['primary']['#suffix'] = '</ul>';
+    $output .= drupal_render($variables['primary']);
+  }
+  if (!empty($variables['secondary'])) {
+    $variables['secondary']['#prefix'] = '<h2 class="element-invisible">' . t('Secondary tabs') . '</h2>';
+    $variables['secondary']['#prefix'] .= '<ul class="tabs secondary">';
+    $variables['secondary']['#suffix'] = '</ul>';
+    $output .= drupal_render($variables['secondary']);
+  }
+
+  return $output;
+}
+
+/**
+ * Sets (or gets) the active menu for the current page.
+ *
+ * The active menu for the page determines the active trail.
+ *
+ * @return
+ *   An array of menu machine names, in order of preference. The
+ *   'menu_default_active_menus' variable may be used to assert a menu order
+ *   different from the order of creation, or to prevent a particular menu from
+ *   being used at all in the active trail.
+ *   E.g., $conf['menu_default_active_menus'] = array('navigation', 'main-menu')
+ */
+function menu_set_active_menu_names($menu_names = NULL) {
+  $active = &drupal_static(__FUNCTION__);
+
+  if (isset($menu_names) && is_array($menu_names)) {
+    $active = $menu_names;
+  }
+  elseif (!isset($active)) {
+    $active = variable_get('menu_default_active_menus', array_keys(menu_list_system_menus()));
+  }
+  return $active;
+}
+
+/**
+ * Gets the active menu for the current page.
+ */
+function menu_get_active_menu_names() {
+  return menu_set_active_menu_names();
+}
+
+/**
+ * Sets the active path, which determines which page is loaded.
+ *
+ * Note that this may not have the desired effect unless invoked very early
+ * in the page load, such as during hook_boot(), or unless you call
+ * menu_execute_active_handler() to generate your page output.
+ *
+ * @param $path
+ *   A Drupal path - not a path alias.
+ */
+function menu_set_active_item($path) {
+  $_GET['q'] = $path;
+  // Since the active item has changed, the active menu trail may also be out
+  // of date.
+  drupal_static_reset('menu_set_active_trail');
+}
+
+/**
+ * Sets the active trail (path to the menu tree root) of the current page.
+ *
+ * Any trail set by this function will only be used for functionality that calls
+ * menu_get_active_trail(). Drupal core only uses trails set here for
+ * breadcrumbs and the page title and not for menu trees or page content.
+ * Additionally, breadcrumbs set by drupal_set_breadcrumb() will override any
+ * trail set here.
+ *
+ * To affect the trail used by menu trees, use menu_tree_set_path(). To affect
+ * the page content, use menu_set_active_item() instead.
+ *
+ * @param $new_trail
+ *   Menu trail to set; the value is saved in a static variable and can be
+ *   retrieved by menu_get_active_trail(). The format of this array should be
+ *   the same as the return value of menu_get_active_trail().
+ *
+ * @return
+ *   The active trail. See menu_get_active_trail() for details.
+ */
+function menu_set_active_trail($new_trail = NULL) {
+  $trail = &drupal_static(__FUNCTION__);
+
+  if (isset($new_trail)) {
+    $trail = $new_trail;
+  }
+  elseif (!isset($trail)) {
+    $trail = array();
+    $trail[] = array(
+      'title' => t('Home'),
+      'href' => '<front>',
+      'link_path' => '',
+      'localized_options' => array(),
+      'type' => 0,
+    );
+
+    // Try to retrieve a menu link corresponding to the current path. If more
+    // than one exists, the link from the most preferred menu is returned.
+    $preferred_link = menu_link_get_preferred();
+    $current_item = menu_get_item();
+
+    // There is a link for the current path.
+    if ($preferred_link) {
+      // Pass TRUE for $only_active_trail to make menu_tree_page_data() build
+      // a stripped down menu tree containing the active trail only, in case
+      // the given menu has not been built in this request yet.
+      $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE);
+      list($key, $curr) = each($tree);
+    }
+    // There is no link for the current path.
+    else {
+      $preferred_link = $current_item;
+      $curr = FALSE;
+    }
+
+    while ($curr) {
+      $link = $curr['link'];
+      if ($link['in_active_trail']) {
+        // Add the link to the trail, unless it links to its parent.
+        if (!($link['type'] & MENU_LINKS_TO_PARENT)) {
+          // The menu tree for the active trail may contain additional links
+          // that have not been translated yet, since they contain dynamic
+          // argument placeholders (%). Such links are not contained in regular
+          // menu trees, and have only been loaded for the additional
+          // translation that happens here, so as to be able to display them in
+          // the breadcumb for the current page.
+          // @see _menu_tree_check_access()
+          // @see _menu_link_translate()
+          if (strpos($link['href'], '%') !== FALSE) {
+            _menu_link_translate($link, TRUE);
+          }
+          if ($link['access']) {
+            $trail[] = $link;
+          }
+        }
+        $tree = $curr['below'] ? $curr['below'] : array();
+      }
+      list($key, $curr) = each($tree);
+    }
+    // Make sure the current page is in the trail to build the page title, by
+    // appending either the preferred link or the menu router item for the
+    // current page. Exclude it if we are on the front page.
+    $last = end($trail);
+    if ($preferred_link && $last['href'] != $preferred_link['href'] && !drupal_is_front_page()) {
+      $trail[] = $preferred_link;
+    }
+  }
+  return $trail;
+}
+
+/**
+ * Looks up the preferred menu link for a given system path.
+ *
+ * @param $path
+ *   The path, for example 'node/5'. The function will find the corresponding
+ *   menu link ('node/5' if it exists, or fallback to 'node/%').
+ * @param $selected_menu
+ *   The name of a menu used to restrict the search for a preferred menu link.
+ *   If not specified, all the menus returned by menu_get_active_menu_names()
+ *   will be used.
+ *
+ * @return
+ *   A fully translated menu link, or FALSE if no matching menu link was
+ *   found. The most specific menu link ('node/5' preferred over 'node/%') in
+ *   the most preferred menu (as defined by menu_get_active_menu_names()) is
+ *   returned.
+ */
+function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
+  $preferred_links = &drupal_static(__FUNCTION__);
+
+  if (!isset($path)) {
+    $path = $_GET['q'];
+  }
+
+  if (empty($selected_menu)) {
+    // Use an illegal menu name as the key for the preferred menu link.
+    $selected_menu = MENU_PREFERRED_LINK;
+  }
+
+  if (!isset($preferred_links[$path])) {
+    // Look for the correct menu link by building a list of candidate paths,
+    // which are ordered by priority (translated hrefs are preferred over
+    // untranslated paths). Afterwards, the most relevant path is picked from
+    // the menus, ordered by menu preference.
+    $item = menu_get_item($path);
+    $path_candidates = array();
+    // 1. The current item href.
+    $path_candidates[$item['href']] = $item['href'];
+    // 2. The tab root href of the current item (if any).
+    if ($item['tab_parent'] && ($tab_root = menu_get_item($item['tab_root_href']))) {
+      $path_candidates[$tab_root['href']] = $tab_root['href'];
+    }
+    // 3. The current item path (with wildcards).
+    $path_candidates[$item['path']] = $item['path'];
+    // 4. The tab root path of the current item (if any).
+    if (!empty($tab_root)) {
+      $path_candidates[$tab_root['path']] = $tab_root['path'];
+    }
+
+    // Retrieve a list of menu names, ordered by preference.
+    $menu_names = menu_get_active_menu_names();
+    // Put the selected menu at the front of the list.
+    array_unshift($menu_names, $selected_menu);
+
+    $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
+    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
+    $query->fields('ml');
+    // Weight must be taken from {menu_links}, not {menu_router}.
+    $query->addField('ml', 'weight', 'link_weight');
+    $query->fields('m');
+    $query->condition('ml.link_path', $path_candidates, 'IN');
+
+    // Sort candidates by link path and menu name.
+    $candidates = array();
+    foreach ($query->execute() as $candidate) {
+      $candidate['weight'] = $candidate['link_weight'];
+      $candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate;
+      // Add any menus not already in the menu name search list.
+      if (!in_array($candidate['menu_name'], $menu_names)) {
+        $menu_names[] = $candidate['menu_name'];
+      }
+    }
+
+    // Store the most specific link for each menu. Also save the most specific
+    // link of the most preferred menu in $preferred_link.
+    foreach ($path_candidates as $link_path) {
+      if (isset($candidates[$link_path])) {
+        foreach ($menu_names as $menu_name) {
+          if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) {
+            $candidate_item = $candidates[$link_path][$menu_name];
+            $map = explode('/', $path);
+            _menu_translate($candidate_item, $map);
+            if ($candidate_item['access']) {
+              $preferred_links[$path][$menu_name] = $candidate_item;
+              if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) {
+                // Store the most specific link.
+                $preferred_links[$path][MENU_PREFERRED_LINK] = $candidate_item;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return isset($preferred_links[$path][$selected_menu]) ? $preferred_links[$path][$selected_menu] : FALSE;
+}
+
+/**
+ * Gets the active trail (path to root menu root) of the current page.
+ *
+ * If a trail is supplied to menu_set_active_trail(), that value is returned. If
+ * a trail is not supplied to menu_set_active_trail(), the path to the current
+ * page is calculated and returned. The calculated trail is also saved as a
+ * static value for use by subsequent calls to menu_get_active_trail().
+ *
+ * @return
+ *   Path to menu root of the current page, as an array of menu link items,
+ *   starting with the site's home page. Each link item is an associative array
+ *   with the following components:
+ *   - title: Title of the item.
+ *   - href: Drupal path of the item.
+ *   - localized_options: Options for passing into the l() function.
+ *   - type: A menu type constant, such as MENU_DEFAULT_LOCAL_TASK, or 0 to
+ *     indicate it's not really in the menu (used for the home page item).
+ */
+function menu_get_active_trail() {
+  return menu_set_active_trail();
+}
+
+/**
+ * Gets the breadcrumb for the current page, as determined by the active trail.
+ *
+ * @see menu_set_active_trail()
+ */
+function menu_get_active_breadcrumb() {
+  $breadcrumb = array();
+
+  // No breadcrumb for the front page.
+  if (drupal_is_front_page()) {
+    return $breadcrumb;
+  }
+
+  $item = menu_get_item();
+  if (!empty($item['access'])) {
+    $active_trail = menu_get_active_trail();
+
+    // Allow modules to alter the breadcrumb, if possible, as that is much
+    // faster than rebuilding an entirely new active trail.
+    drupal_alter('menu_breadcrumb', $active_trail, $item);
+
+    // Don't show a link to the current page in the breadcrumb trail.
+    $end = end($active_trail);
+    if ($item['href'] == $end['href']) {
+      array_pop($active_trail);
+    }
+
+    // Remove the tab root (parent) if the current path links to its parent.
+    // Normally, the tab root link is included in the breadcrumb, as soon as we
+    // are on a local task or any other child link. However, if we are on a
+    // default local task (e.g., node/%/view), then we do not want the tab root
+    // link (e.g., node/%) to appear, as it would be identical to the current
+    // page. Since this behavior also needs to work recursively (i.e., on
+    // default local tasks of default local tasks), and since the last non-task
+    // link in the trail is used as page title (see menu_get_active_title()),
+    // this condition cannot be cleanly integrated into menu_get_active_trail().
+    // menu_get_active_trail() already skips all links that link to their parent
+    // (commonly MENU_DEFAULT_LOCAL_TASK). In order to also hide the parent link
+    // itself, we always remove the last link in the trail, if the current
+    // router item links to its parent.
+    if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
+      array_pop($active_trail);
+    }
+
+    foreach ($active_trail as $parent) {
+      $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']);
+    }
+  }
+  return $breadcrumb;
+}
+
+/**
+ * Gets the title of the current page, as determined by the active trail.
+ */
+function menu_get_active_title() {
+  $active_trail = menu_get_active_trail();
+
+  foreach (array_reverse($active_trail) as $item) {
+    if (!(bool) ($item['type'] & MENU_IS_LOCAL_TASK)) {
+      return $item['title'];
+    }
+  }
+}
+
+/**
+ * Gets a translated, access-checked menu link that is ready for rendering.
+ *
+ * This function should never be called from within node_load() or any other
+ * function used as a menu object load function since an infinite recursion may
+ * occur.
+ *
+ * @param $mlid
+ *   The mlid of the menu item.
+ *
+ * @return
+ *   A menu link, with $item['access'] filled and link translated for
+ *   rendering.
+ */
+function menu_link_load($mlid) {
+  if (is_numeric($mlid)) {
+    $query = db_select('menu_links', 'ml');
+    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
+    $query->fields('ml');
+    // Weight should be taken from {menu_links}, not {menu_router}.
+    $query->addField('ml', 'weight', 'link_weight');
+    $query->fields('m');
+    $query->condition('ml.mlid', $mlid);
+    if ($item = $query->execute()->fetchAssoc()) {
+      $item['weight'] = $item['link_weight'];
+      _menu_link_translate($item);
+      return $item;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Clears the cached cached data for a single named menu.
+ */
+function menu_cache_clear($menu_name = 'navigation') {
+  $cache_cleared = &drupal_static(__FUNCTION__, array());
+
+  if (empty($cache_cleared[$menu_name])) {
+    cache_clear_all('links:' . $menu_name . ':', 'cache_menu', TRUE);
+    $cache_cleared[$menu_name] = 1;
+  }
+  elseif ($cache_cleared[$menu_name] == 1) {
+    drupal_register_shutdown_function('cache_clear_all', 'links:' . $menu_name . ':', 'cache_menu', TRUE);
+    $cache_cleared[$menu_name] = 2;
+  }
+
+  // Also clear the menu system static caches.
+  menu_reset_static_cache();
+}
+
+/**
+ * Clears all cached menu data.
+ *
+ * This should be called any time broad changes
+ * might have been made to the router items or menu links.
+ */
+function menu_cache_clear_all() {
+  cache_clear_all('*', 'cache_menu', TRUE);
+  menu_reset_static_cache();
+}
+
+/**
+ * Resets the menu system static cache.
+ */
+function menu_reset_static_cache() {
+  drupal_static_reset('_menu_build_tree');
+  drupal_static_reset('menu_tree');
+  drupal_static_reset('menu_tree_all_data');
+  drupal_static_reset('menu_tree_page_data');
+  drupal_static_reset('menu_load_all');
+  drupal_static_reset('menu_link_get_preferred');
+}
+
+/**
+ * Populates the database tables used by various menu functions.
+ *
+ * This function will clear and populate the {menu_router} table, add entries
+ * to {menu_links} for new router items, and then remove stale items from
+ * {menu_links}. If called from update.php or install.php, it will also
+ * schedule a call to itself on the first real page load from
+ * menu_execute_active_handler(), because the maintenance page environment
+ * is different and leaves stale data in the menu tables.
+ *
+ * @return
+ *   TRUE if the menu was rebuilt, FALSE if another thread was rebuilding
+ *   in parallel and the current thread just waited for completion.
+ */
+function menu_rebuild() {
+  if (!lock_acquire('menu_rebuild')) {
+    // Wait for another request that is already doing this work.
+    // We choose to block here since otherwise the router item may not
+    // be available in menu_execute_active_handler() resulting in a 404.
+    lock_wait('menu_rebuild');
+    return FALSE;
+  }
+
+  $transaction = db_transaction();
+
+  try {
+    list($menu, $masks) = menu_router_build();
+    _menu_router_save($menu, $masks);
+    _menu_navigation_links_rebuild($menu);
+    // Clear the menu, page and block caches.
+    menu_cache_clear_all();
+    _menu_clear_page_cache();
+
+    if (defined('MAINTENANCE_MODE')) {
+      variable_set('menu_rebuild_needed', TRUE);
+    }
+    else {
+      variable_del('menu_rebuild_needed');
+    }
+  }
+  catch (Exception $e) {
+    $transaction->rollback();
+    watchdog_exception('menu', $e);
+  }
+
+  lock_release('menu_rebuild');
+  return TRUE;
+}
+
+/**
+ * Collects and alters the menu definitions.
+ */
+function menu_router_build() {
+  // We need to manually call each module so that we can know which module
+  // a given item came from.
+  $callbacks = array();
+  foreach (module_implements('menu') as $module) {
+    $router_items = call_user_func($module . '_menu');
+    if (isset($router_items) && is_array($router_items)) {
+      foreach (array_keys($router_items) as $path) {
+        $router_items[$path]['module'] = $module;
+      }
+      $callbacks = array_merge($callbacks, $router_items);
+    }
+  }
+  // Alter the menu as defined in modules, keys are like user/%user.
+  drupal_alter('menu', $callbacks);
+  list($menu, $masks) = _menu_router_build($callbacks);
+  _menu_router_cache($menu);
+
+  return array($menu, $masks);
+}
+
+/**
+ * Stores the menu router if we have it in memory.
+ */
+function _menu_router_cache($new_menu = NULL) {
+  $menu = &drupal_static(__FUNCTION__);
+
+  if (isset($new_menu)) {
+    $menu = $new_menu;
+  }
+  return $menu;
+}
+
+/**
+ * Gets the menu router.
+ */
+function menu_get_router() {
+  // Check first if we have it in memory already.
+  $menu = _menu_router_cache();
+  if (empty($menu)) {
+    list($menu, $masks) = menu_router_build();
+  }
+  return $menu;
+}
+
+/**
+ * Builds a link from a router item.
+ */
+function _menu_link_build($item) {
+  // Suggested items are disabled by default.
+  if ($item['type'] == MENU_SUGGESTED_ITEM) {
+    $item['hidden'] = 1;
+  }
+  // Hide all items that are not visible in the tree.
+  elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) {
+    $item['hidden'] = -1;
+  }
+  // Note, we set this as 'system', so that we can be sure to distinguish all
+  // the menu links generated automatically from entries in {menu_router}.
+  $item['module'] = 'system';
+  $item += array(
+    'menu_name' => 'navigation',
+    'link_title' => $item['title'],
+    'link_path' => $item['path'],
+    'hidden' => 0,
+    'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
+  );
+  return $item;
+}
+
+/**
+ * Builds menu links for the items in the menu router.
+ */
+function _menu_navigation_links_rebuild($menu) {
+  // Add normal and suggested items as links.
+  $menu_links = array();
+  foreach ($menu as $path => $item) {
+    if ($item['_visible']) {
+      $menu_links[$path] = $item;
+      $sort[$path] = $item['_number_parts'];
+    }
+  }
+  if ($menu_links) {
+    // Keep an array of processed menu links, to allow menu_link_save() to
+    // check this for parents instead of querying the database.
+    $parent_candidates = array();
+    // Make sure no child comes before its parent.
+    array_multisort($sort, SORT_NUMERIC, $menu_links);
+
+    foreach ($menu_links as $key => $item) {
+      $existing_item = db_select('menu_links')
+        ->fields('menu_links')
+        ->condition('link_path', $item['path'])
+        ->condition('module', 'system')
+        ->execute()->fetchAssoc();
+      if ($existing_item) {
+        $item['mlid'] = $existing_item['mlid'];
+        // A change in hook_menu may move the link to a different menu
+        if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) {
+          $item['menu_name'] = $existing_item['menu_name'];
+          $item['plid'] = $existing_item['plid'];
+        }
+        else {
+          // It moved to a new menu. Let menu_link_save() try to find a new
+          // parent based on the path.
+          unset($item['plid']);
+        }
+        $item['has_children'] = $existing_item['has_children'];
+        $item['updated'] = $existing_item['updated'];
+      }
+      if ($existing_item && $existing_item['customized']) {
+        $parent_candidates[$existing_item['mlid']] = $existing_item;
+      }
+      else {
+        $item = _menu_link_build($item);
+        menu_link_save($item, $existing_item, $parent_candidates);
+        $parent_candidates[$item['mlid']] = $item;
+        unset($menu_links[$key]);
+      }
+    }
+  }
+  $paths = array_keys($menu);
+  // Updated and customized items whose router paths are gone need new ones.
+  $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
+    ->fields('menu_links', array(
+      'link_path',
+      'mlid',
+      'router_path',
+      'updated',
+    ))
+    ->condition(db_or()
+      ->condition('updated', 1)
+      ->condition(db_and()
+        ->condition('router_path', $paths, 'NOT IN')
+        ->condition('external', 0)
+        ->condition('customized', 1)
+      )
+    )
+    ->execute();
+  foreach ($result as $item) {
+    $router_path = _menu_find_router_path($item['link_path']);
+    if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) {
+      // If the router path and the link path matches, it's surely a working
+      // item, so we clear the updated flag.
+      $updated = $item['updated'] && $router_path != $item['link_path'];
+      db_update('menu_links')
+        ->fields(array(
+          'router_path' => $router_path,
+          'updated' => (int) $updated,
+        ))
+        ->condition('mlid', $item['mlid'])
+        ->execute();
+    }
+  }
+  // Find any item whose router path does not exist any more.
+  $result = db_select('menu_links')
+    ->fields('menu_links')
+    ->condition('router_path', $paths, 'NOT IN')
+    ->condition('external', 0)
+    ->condition('updated', 0)
+    ->condition('customized', 0)
+    ->orderBy('depth', 'DESC')
+    ->execute();
+  // Remove all such items. Starting from those with the greatest depth will
+  // minimize the amount of re-parenting done by menu_link_delete().
+  foreach ($result as $item) {
+    _menu_delete_item($item, TRUE);
+  }
+}
+
+/**
+ * Clones an array of menu links.
+ *
+ * @param $links
+ *   An array of menu links to clone.
+ * @param $menu_name
+ *   (optional) The name of a menu that the links will be cloned for. If not
+ *   set, the cloned links will be in the same menu as the original set of
+ *   links that were passed in.
+ *
+ * @return
+ *   An array of menu links with the same properties as the passed-in array,
+ *   but with the link identifiers removed so that a new link will be created
+ *   when any of them is passed in to menu_link_save().
+ *
+ * @see menu_link_save()
+ */
+function menu_links_clone($links, $menu_name = NULL) {
+  foreach ($links as &$link) {
+    unset($link['mlid']);
+    unset($link['plid']);
+    if (isset($menu_name)) {
+      $link['menu_name'] = $menu_name;
+    }
+  }
+  return $links;
+}
+
+/**
+ * Returns an array containing all links for a menu.
+ *
+ * @param $menu_name
+ *   The name of the menu whose links should be returned.
+ *
+ * @return
+ *   An array of menu links.
+ */
+function menu_load_links($menu_name) {
+  $links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC))
+    ->fields('ml')
+    ->condition('ml.menu_name', $menu_name)
+    // Order by weight so as to be helpful for menus that are only one level
+    // deep.
+    ->orderBy('weight')
+    ->execute()
+    ->fetchAll();
+
+  foreach ($links as &$link) {
+    $link['options'] = unserialize($link['options']);
+  }
+  return $links;
+}
+
+/**
+ * Deletes all links for a menu.
+ *
+ * @param $menu_name
+ *   The name of the menu whose links will be deleted.
+ */
+function menu_delete_links($menu_name) {
+  $links = menu_load_links($menu_name);
+  foreach ($links as $link) {
+    // To speed up the deletion process, we reset some link properties that
+    // would trigger re-parenting logic in _menu_delete_item() and
+    // _menu_update_parental_status().
+    $link['has_children'] = FALSE;
+    $link['plid'] = 0;
+    _menu_delete_item($link);
+  }
+}
+
+/**
+ * Delete one or several menu links.
+ *
+ * @param $mlid
+ *   A valid menu link mlid or NULL. If NULL, $path is used.
+ * @param $path
+ *   The path to the menu items to be deleted. $mlid must be NULL.
+ */
+function menu_link_delete($mlid, $path = NULL) {
+  if (isset($mlid)) {
+    _menu_delete_item(db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc());
+  }
+  else {
+    $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $path));
+    foreach ($result as $link) {
+      _menu_delete_item($link);
+    }
+  }
+}
+
+/**
+ * Deletes a single menu link.
+ *
+ * @param $item
+ *   Item to be deleted.
+ * @param $force
+ *   Forces deletion. Internal use only, setting to TRUE is discouraged.
+ *
+ * @see menu_link_delete()
+ */
+function _menu_delete_item($item, $force = FALSE) {
+  $item = is_object($item) ? get_object_vars($item) : $item;
+  if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) {
+    // Children get re-attached to the item's parent.
+    if ($item['has_children']) {
+      $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = :plid", array(':plid' => $item['mlid']));
+      foreach ($result as $m) {
+        $child = menu_link_load($m->mlid);
+        $child['plid'] = $item['plid'];
+        menu_link_save($child);
+      }
+    }
+
+    // Notify modules we are deleting the item.
+    module_invoke_all('menu_link_delete', $item);
+
+    db_delete('menu_links')->condition('mlid', $item['mlid'])->execute();
+
+    // Update the has_children status of the parent.
+    _menu_update_parental_status($item);
+    menu_cache_clear($item['menu_name']);
+    _menu_clear_page_cache();
+  }
+}
+
+/**
+ * Saves a menu link.
+ *
+ * After calling this function, rebuild the menu cache using
+ * menu_cache_clear_all().
+ *
+ * @param $item
+ *   An associative array representing a menu link item, with elements:
+ *   - link_path: (required) The path of the menu item, which should be
+ *     normalized first by calling drupal_get_normal_path() on it.
+ *   - link_title: (required) Title to appear in menu for the link.
+ *   - menu_name: (optional) The machine name of the menu for the link.
+ *     Defaults to 'navigation'.
+ *   - weight: (optional) Integer to determine position in menu. Default is 0.
+ *   - expanded: (optional) Boolean that determines if the item is expanded.
+ *   - options: (optional) An array of options, see l() for more.
+ *   - mlid: (optional) Menu link identifier, the primary integer key for each
+ *     menu link. Can be set to an existing value, or to 0 or NULL
+ *     to insert a new link.
+ *   - plid: (optional) The mlid of the parent.
+ *   - router_path: (optional) The path of the relevant router item.
+ * @param $existing_item
+ *   Optional, the current record from the {menu_links} table as an array.
+ * @param $parent_candidates
+ *   Optional array of menu links keyed by mlid. Used by
+ *   _menu_navigation_links_rebuild() only.
+ *
+ * @return
+ *   The mlid of the saved menu link, or FALSE if the menu link could not be
+ *   saved.
+ */
+function menu_link_save(&$item, $existing_item = array(), $parent_candidates = array()) {
+  drupal_alter('menu_link', $item);
+
+  // This is the easiest way to handle the unique internal path '<front>',
+  // since a path marked as external does not need to match a router path.
+  $item['external'] = (url_is_external($item['link_path'])  || $item['link_path'] == '<front>') ? 1 : 0;
+  // Load defaults.
+  $item += array(
+    'menu_name' => 'navigation',
+    'weight' => 0,
+    'link_title' => '',
+    'hidden' => 0,
+    'has_children' => 0,
+    'expanded' => 0,
+    'options' => array(),
+    'module' => 'menu',
+    'customized' => 0,
+    'updated' => 0,
+  );
+  if (isset($item['mlid'])) {
+    if (!$existing_item) {
+      $existing_item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array('mlid' => $item['mlid']))->fetchAssoc();
+    }
+    if ($existing_item) {
+      $existing_item['options'] = unserialize($existing_item['options']);
+    }
+  }
+  else {
+    $existing_item = FALSE;
+  }
+
+  // Try to find a parent link. If found, assign it and derive its menu.
+  $parent = _menu_link_find_parent($item, $parent_candidates);
+  if (!empty($parent['mlid'])) {
+    $item['plid'] = $parent['mlid'];
+    $item['menu_name'] = $parent['menu_name'];
+  }
+  // If no corresponding parent link was found, move the link to the top-level.
+  else {
+    $item['plid'] = 0;
+  }
+  $menu_name = $item['menu_name'];
+
+  if (!$existing_item) {
+    $item['mlid'] = db_insert('menu_links')
+      ->fields(array(
+        'menu_name' => $item['menu_name'],
+        'plid' => $item['plid'],
+        'link_path' => $item['link_path'],
+        'hidden' => $item['hidden'],
+        'external' => $item['external'],
+        'has_children' => $item['has_children'],
+        'expanded' => $item['expanded'],
+        'weight' => $item['weight'],
+        'module' => $item['module'],
+        'link_title' => $item['link_title'],
+        'options' => serialize($item['options']),
+        'customized' => $item['customized'],
+        'updated' => $item['updated'],
+      ))
+      ->execute();
+  }
+
+  // Directly fill parents for top-level links.
+  if ($item['plid'] == 0) {
+    $item['p1'] = $item['mlid'];
+    for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
+      $item["p$i"] = 0;
+    }
+    $item['depth'] = 1;
+  }
+  // Otherwise, ensure that this link's depth is not beyond the maximum depth
+  // and fill parents based on the parent link.
+  else {
+    if ($item['has_children'] && $existing_item) {
+      $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
+    }
+    else {
+      $limit = MENU_MAX_DEPTH - 1;
+    }
+    if ($parent['depth'] > $limit) {
+      return FALSE;
+    }
+    $item['depth'] = $parent['depth'] + 1;
+    _menu_link_parents_set($item, $parent);
+  }
+  // Need to check both plid and menu_name, since plid can be 0 in any menu.
+  if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) {
+    _menu_link_move_children($item, $existing_item);
+  }
+  // Find the router_path.
+  if (empty($item['router_path'])  || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) {
+    if ($item['external']) {
+      $item['router_path'] = '';
+    }
+    else {
+      // Find the router path which will serve this path.
+      $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS);
+      $item['router_path'] = _menu_find_router_path($item['link_path']);
+    }
+  }
+  // If every value in $existing_item is the same in the $item, there is no
+  // reason to run the update queries or clear the caches. We use
+  // array_intersect_key() with the $item as the first parameter because
+  // $item may have additional keys left over from building a router entry.
+  // The intersect removes the extra keys, allowing a meaningful comparison.
+  if (!$existing_item || (array_intersect_key($item, $existing_item) != $existing_item)) {
+    db_update('menu_links')
+      ->fields(array(
+        'menu_name' => $item['menu_name'],
+        'plid' => $item['plid'],
+        'link_path' => $item['link_path'],
+        'router_path' => $item['router_path'],
+        'hidden' => $item['hidden'],
+        'external' => $item['external'],
+        'has_children' => $item['has_children'],
+        'expanded' => $item['expanded'],
+        'weight' => $item['weight'],
+        'depth' => $item['depth'],
+        'p1' => $item['p1'],
+        'p2' => $item['p2'],
+        'p3' => $item['p3'],
+        'p4' => $item['p4'],
+        'p5' => $item['p5'],
+        'p6' => $item['p6'],
+        'p7' => $item['p7'],
+        'p8' => $item['p8'],
+        'p9' => $item['p9'],
+        'module' => $item['module'],
+        'link_title' => $item['link_title'],
+        'options' => serialize($item['options']),
+        'customized' => $item['customized'],
+      ))
+      ->condition('mlid', $item['mlid'])
+      ->execute();
+    // Check the has_children status of the parent.
+    _menu_update_parental_status($item);
+    menu_cache_clear($menu_name);
+    if ($existing_item && $menu_name != $existing_item['menu_name']) {
+      menu_cache_clear($existing_item['menu_name']);
+    }
+    // Notify modules we have acted on a menu item.
+    $hook = 'menu_link_insert';
+    if ($existing_item) {
+      $hook = 'menu_link_update';
+    }
+    module_invoke_all($hook, $item);
+    // Now clear the cache.
+    _menu_clear_page_cache();
+  }
+  return $item['mlid'];
+}
+
+/**
+ * Finds a possible parent for a given menu link.
+ *
+ * Because the parent of a given link might not exist anymore in the database,
+ * we apply a set of heuristics to determine a proper parent:
+ *
+ *  - use the passed parent link if specified and existing.
+ *  - else, use the first existing link down the previous link hierarchy
+ *  - else, for system menu links (derived from hook_menu()), reparent
+ *    based on the path hierarchy.
+ *
+ * @param $menu_link
+ *   A menu link.
+ * @param $parent_candidates
+ *   An array of menu links keyed by mlid.
+ *
+ * @return
+ *   A menu link structure of the possible parent or FALSE if no valid parent
+ *   has been found.
+ */
+function _menu_link_find_parent($menu_link, $parent_candidates = array()) {
+  $parent = FALSE;
+
+  // This item is explicitely top-level, skip the rest of the parenting.
+  if (isset($menu_link['plid']) && empty($menu_link['plid'])) {
+    return $parent;
+  }
+
+  // If we have a parent link ID, try to use that.
+  $candidates = array();
+  if (isset($menu_link['plid'])) {
+    $candidates[] = $menu_link['plid'];
+  }
+
+  // Else, if we have a link hierarchy try to find a valid parent in there.
+  if (!empty($menu_link['depth']) && $menu_link['depth'] > 1) {
+    for ($depth = $menu_link['depth'] - 1; $depth >= 1; $depth--) {
+      $candidates[] = $menu_link['p' . $depth];
+    }
+  }
+
+  foreach ($candidates as $mlid) {
+    if (isset($parent_candidates[$mlid])) {
+      $parent = $parent_candidates[$mlid];
+    }
+    else {
+      $parent = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc();
+    }
+    if ($parent) {
+      return $parent;
+    }
+  }
+
+  // If everything else failed, try to derive the parent from the path
+  // hierarchy. This only makes sense for links derived from menu router
+  // items (ie. from hook_menu()).
+  if ($menu_link['module'] == 'system') {
+    $query = db_select('menu_links');
+    $query->condition('module', 'system');
+    // We always respect the link's 'menu_name'; inheritance for router items is
+    // ensured in _menu_router_build().
+    $query->condition('menu_name', $menu_link['menu_name']);
+
+    // Find the parent - it must be unique.
+    $parent_path = $menu_link['link_path'];
+    do {
+      $parent = FALSE;
+      $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
+      $new_query = clone $query;
+      $new_query->condition('link_path', $parent_path);
+      // Only valid if we get a unique result.
+      if ($new_query->countQuery()->execute()->fetchField() == 1) {
+        $parent = $new_query->fields('menu_links')->execute()->fetchAssoc();
+      }
+    } while ($parent === FALSE && $parent_path);
+  }
+
+  return $parent;
+}
+
+/**
+ * Clears the page and block caches at most twice per page load.
+ */
+function _menu_clear_page_cache() {
+  $cache_cleared = &drupal_static(__FUNCTION__, 0);
+
+  // Clear the page and block caches, but at most twice, including at
+  //  the end of the page load when there are multiple links saved or deleted.
+  if ($cache_cleared == 0) {
+    cache_clear_all();
+    // Keep track of which menus have expanded items.
+    _menu_set_expanded_menus();
+    $cache_cleared = 1;
+  }
+  elseif ($cache_cleared == 1) {
+    drupal_register_shutdown_function('cache_clear_all');
+    // Keep track of which menus have expanded items.
+    drupal_register_shutdown_function('_menu_set_expanded_menus');
+    $cache_cleared = 2;
+  }
+}
+
+/**
+ * Updates a list of menus with expanded items.
+ */
+function _menu_set_expanded_menus() {
+  $names = db_query("SELECT menu_name FROM {menu_links} WHERE expanded <> 0 GROUP BY menu_name")->fetchCol();
+  variable_set('menu_expanded', $names);
+}
+
+/**
+ * Finds the router path which will serve this path.
+ *
+ * @param $link_path
+ *  The path for we are looking up its router path.
+ *
+ * @return
+ *  A path from $menu keys or empty if $link_path points to a nonexisting
+ *  place.
+ */
+function _menu_find_router_path($link_path) {
+  // $menu will only have data during a menu rebuild.
+  $menu = _menu_router_cache();
+
+  $router_path = $link_path;
+  $parts = explode('/', $link_path, MENU_MAX_PARTS);
+  $ancestors = menu_get_ancestors($parts);
+
+  if (empty($menu)) {
+    // Not during a menu rebuild, so look up in the database.
+    $router_path = (string) db_select('menu_router')
+      ->fields('menu_router', array('path'))
+      ->condition('path', $ancestors, 'IN')
+      ->orderBy('fit', 'DESC')
+      ->range(0, 1)
+      ->execute()->fetchField();
+  }
+  elseif (!isset($menu[$router_path])) {
+    // Add an empty router path as a fallback.
+    $ancestors[] = '';
+    foreach ($ancestors as $key => $router_path) {
+      if (isset($menu[$router_path])) {
+        // Exit the loop leaving $router_path as the first match.
+        break;
+      }
+    }
+    // If we did not find the path, $router_path will be the empty string
+    // at the end of $ancestors.
+  }
+  return $router_path;
+}
+
+/**
+ * Inserts, updates, or deletes an uncustomized menu link related to a module.
+ *
+ * @param $module
+ *   The name of the module.
+ * @param $op
+ *   Operation to perform: insert, update or delete.
+ * @param $link_path
+ *   The path this link points to.
+ * @param $link_title
+ *   Title of the link to insert or new title to update the link to.
+ *   Unused for delete.
+ *
+ * @return
+ *   The insert op returns the mlid of the new item. Others op return NULL.
+ */
+function menu_link_maintain($module, $op, $link_path, $link_title) {
+  switch ($op) {
+    case 'insert':
+      $menu_link = array(
+        'link_title' => $link_title,
+        'link_path' => $link_path,
+        'module' => $module,
+      );
+      return menu_link_save($menu_link);
+      break;
+    case 'update':
+      $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC);
+      foreach ($result as $link) {
+        $link['link_title'] = $link_title;
+        $link['options'] = unserialize($link['options']);
+        menu_link_save($link);
+      }
+      break;
+    case 'delete':
+      menu_link_delete(NULL, $link_path);
+      break;
+  }
+}
+
+/**
+ * Finds the depth of an item's children relative to its depth.
+ *
+ * For example, if the item has a depth of 2, and the maximum of any child in
+ * the menu link tree is 5, the relative depth is 3.
+ *
+ * @param $item
+ *   An array representing a menu link item.
+ *
+ * @return
+ *   The relative depth, or zero.
+ *
+ */
+function menu_link_children_relative_depth($item) {
+  $query = db_select('menu_links');
+  $query->addField('menu_links', 'depth');
+  $query->condition('menu_name', $item['menu_name']);
+  $query->orderBy('depth', 'DESC');
+  $query->range(0, 1);
+
+  $i = 1;
+  $p = 'p1';
+  while ($i <= MENU_MAX_DEPTH && $item[$p]) {
+    $query->condition($p, $item[$p]);
+    $p = 'p' . ++$i;
+  }
+
+  $max_depth = $query->execute()->fetchField();
+
+  return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
+}
+
+/**
+ * Updates the children of a menu link that is being moved.
+ *
+ * The menu name, parents (p1 - p6), and depth are updated for all children of
+ * the link, and the has_children status of the previous parent is updated.
+ */
+function _menu_link_move_children($item, $existing_item) {
+  $query = db_update('menu_links');
+
+  $query->fields(array('menu_name' => $item['menu_name']));
+
+  $p = 'p1';
+  $expressions = array();
+  for ($i = 1; $i <= $item['depth']; $p = 'p' . ++$i) {
+    $expressions[] = array($p, ":p_$i", array(":p_$i" => $item[$p]));
+  }
+  $j = $existing_item['depth'] + 1;
+  while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
+    $expressions[] = array('p' . $i++, 'p' . $j++, array());
+  }
+  while ($i <= MENU_MAX_DEPTH) {
+    $expressions[] = array('p' . $i++, 0, array());
+  }
+
+  $shift = $item['depth'] - $existing_item['depth'];
+  if ($shift > 0) {
+    // The order of expressions must be reversed so the new values don't
+    // overwrite the old ones before they can be used because "Single-table
+    // UPDATE assignments are generally evaluated from left to right"
+    // see: http://dev.mysql.com/doc/refman/5.0/en/update.html
+    $expressions = array_reverse($expressions);
+  }
+  foreach ($expressions as $expression) {
+    $query->expression($expression[0], $expression[1], $expression[2]);
+  }
+
+  $query->expression('depth', 'depth + :depth', array(':depth' => $shift));
+  $query->condition('menu_name', $existing_item['menu_name']);
+  $p = 'p1';
+  for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p' . ++$i) {
+    $query->condition($p, $existing_item[$p]);
+  }
+
+  $query->execute();
+
+  // Check the has_children status of the parent, while excluding this item.
+  _menu_update_parental_status($existing_item, TRUE);
+}
+
+/**
+ * Checks and updates the 'has_children' status for the parent of a link.
+ */
+function _menu_update_parental_status($item, $exclude = FALSE) {
+  // If plid == 0, there is nothing to update.
+  if ($item['plid']) {
+    // Check if at least one visible child exists in the table.
+    $query = db_select('menu_links');
+    $query->addField('menu_links', 'mlid');
+    $query->condition('menu_name', $item['menu_name']);
+    $query->condition('hidden', 0);
+    $query->condition('plid', $item['plid']);
+    $query->range(0, 1);
+    if ($exclude) {
+      $query->condition('mlid', $item['mlid'], '<>');
+    }
+    $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
+    db_update('menu_links')
+      ->fields(array('has_children' => $parent_has_children))
+      ->condition('mlid', $item['plid'])
+      ->execute();
+  }
+}
+
+/**
+ * Sets the p1 through p9 values for a menu link being saved.
+ */
+function _menu_link_parents_set(&$item, $parent) {
+  $i = 1;
+  while ($i < $item['depth']) {
+    $p = 'p' . $i++;
+    $item[$p] = $parent[$p];
+  }
+  $p = 'p' . $i++;
+  // The parent (p1 - p9) corresponding to the depth always equals the mlid.
+  $item[$p] = $item['mlid'];
+  while ($i <= MENU_MAX_DEPTH) {
+    $p = 'p' . $i++;
+    $item[$p] = 0;
+  }
+}
+
+/**
+ * Builds the router table based on the data from hook_menu().
+ */
+function _menu_router_build($callbacks) {
+  // First pass: separate callbacks from paths, making paths ready for
+  // matching. Calculate fitness, and fill some default values.
+  $menu = array();
+  $masks = array();
+  foreach ($callbacks as $path => $item) {
+    $load_functions = array();
+    $to_arg_functions = array();
+    $fit = 0;
+    $move = FALSE;
+
+    $parts = explode('/', $path, MENU_MAX_PARTS);
+    $number_parts = count($parts);
+    // We store the highest index of parts here to save some work in the fit
+    // calculation loop.
+    $slashes = $number_parts - 1;
+    // Extract load and to_arg functions.
+    foreach ($parts as $k => $part) {
+      $match = FALSE;
+      // Look for wildcards in the form allowed to be used in PHP functions,
+      // because we are using these to construct the load function names.
+      if (preg_match('/^%(|' . DRUPAL_PHP_FUNCTION_PATTERN . ')$/', $part, $matches)) {
+        if (empty($matches[1])) {
+          $match = TRUE;
+          $load_functions[$k] = NULL;
+        }
+        else {
+          if (function_exists($matches[1] . '_to_arg')) {
+            $to_arg_functions[$k] = $matches[1] . '_to_arg';
+            $load_functions[$k] = NULL;
+            $match = TRUE;
+          }
+          if (function_exists($matches[1] . '_load')) {
+            $function = $matches[1] . '_load';
+            // Create an array of arguments that will be passed to the _load
+            // function when this menu path is checked, if 'load arguments'
+            // exists.
+            $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function;
+            $match = TRUE;
+          }
+        }
+      }
+      if ($match) {
+        $parts[$k] = '%';
+      }
+      else {
+        $fit |=  1 << ($slashes - $k);
+      }
+    }
+    if ($fit) {
+      $move = TRUE;
+    }
+    else {
+      // If there is no %, it fits maximally.
+      $fit = (1 << $number_parts) - 1;
+    }
+    $masks[$fit] = 1;
+    $item['_load_functions'] = $load_functions;
+    $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
+    $item += array(
+      'title' => '',
+      'weight' => 0,
+      'type' => MENU_NORMAL_ITEM,
+      'module' => '',
+      '_number_parts' => $number_parts,
+      '_parts' => $parts,
+      '_fit' => $fit,
+    );
+    $item += array(
+      '_visible' => (bool) ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB),
+      '_tab' => (bool) ($item['type'] & MENU_IS_LOCAL_TASK),
+    );
+    if ($move) {
+      $new_path = implode('/', $item['_parts']);
+      $menu[$new_path] = $item;
+      $sort[$new_path] = $number_parts;
+    }
+    else {
+      $menu[$path] = $item;
+      $sort[$path] = $number_parts;
+    }
+  }
+  array_multisort($sort, SORT_NUMERIC, $menu);
+  // Apply inheritance rules.
+  foreach ($menu as $path => $v) {
+    $item = &$menu[$path];
+    if (!$item['_tab']) {
+      // Non-tab items.
+      $item['tab_parent'] = '';
+      $item['tab_root'] = $path;
+    }
+    // If not specified, assign the default tab type for local tasks.
+    elseif (!isset($item['context'])) {
+      $item['context'] = MENU_CONTEXT_PAGE;
+    }
+    for ($i = $item['_number_parts'] - 1; $i; $i--) {
+      $parent_path = implode('/', array_slice($item['_parts'], 0, $i));
+      if (isset($menu[$parent_path])) {
+
+        $parent = &$menu[$parent_path];
+
+        // If we have no menu name, try to inherit it from parent items.
+        if (!isset($item['menu_name'])) {
+          // If the parent item of this item does not define a menu name (and no
+          // previous iteration assigned one already), try to find the menu name
+          // of the parent item in the currently stored menu links.
+          if (!isset($parent['menu_name'])) {
+            $menu_name = db_query("SELECT menu_name FROM {menu_links} WHERE router_path = :router_path AND module = 'system'", array(':router_path' => $parent_path))->fetchField();
+            if ($menu_name) {
+              $parent['menu_name'] = $menu_name;
+            }
+          }
+          // If the parent item defines a menu name, inherit it.
+          if (!empty($parent['menu_name'])) {
+            $item['menu_name'] = $parent['menu_name'];
+          }
+        }
+        if (!isset($item['tab_parent'])) {
+          // Parent stores the parent of the path.
+          $item['tab_parent'] = $parent_path;
+        }
+        if (!isset($item['tab_root']) && !$parent['_tab']) {
+          $item['tab_root'] = $parent_path;
+        }
+        // If an access callback is not found for a default local task we use
+        // the callback from the parent, since we expect them to be identical.
+        // In all other cases, the access parameters must be specified.
+        if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) {
+          $item['access callback'] = $parent['access callback'];
+          if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
+            $item['access arguments'] = $parent['access arguments'];
+          }
+        }
+        // Same for page callbacks.
+        if (!isset($item['page callback']) && isset($parent['page callback'])) {
+          $item['page callback'] = $parent['page callback'];
+          if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
+            $item['page arguments'] = $parent['page arguments'];
+          }
+          if (!isset($item['file path']) && isset($parent['file path'])) {
+            $item['file path'] = $parent['file path'];
+          }
+          if (!isset($item['file']) && isset($parent['file'])) {
+            $item['file'] = $parent['file'];
+            if (empty($item['file path']) && isset($item['module']) && isset($parent['module']) && $item['module'] != $parent['module']) {
+              $item['file path'] = drupal_get_path('module', $parent['module']);
+            }
+          }
+        }
+        // Same for delivery callbacks.
+        if (!isset($item['delivery callback']) && isset($parent['delivery callback'])) {
+          $item['delivery callback'] = $parent['delivery callback'];
+        }
+        // Same for theme callbacks.
+        if (!isset($item['theme callback']) && isset($parent['theme callback'])) {
+          $item['theme callback'] = $parent['theme callback'];
+          if (!isset($item['theme arguments']) && isset($parent['theme arguments'])) {
+            $item['theme arguments'] = $parent['theme arguments'];
+          }
+        }
+        // Same for load arguments: if a loader doesn't have any explict
+        // arguments, try to find arguments in the parent.
+        if (!isset($item['load arguments'])) {
+          foreach ($item['_load_functions'] as $k => $function) {
+            // This loader doesn't have any explict arguments...
+            if (!is_array($function)) {
+              // ... check the parent for a loader at the same position
+              // using the same function name and defining arguments...
+              if (isset($parent['_load_functions'][$k]) && is_array($parent['_load_functions'][$k]) && key($parent['_load_functions'][$k]) === $function) {
+                // ... and inherit the arguments on the child.
+                $item['_load_functions'][$k] = $parent['_load_functions'][$k];
+              }
+            }
+          }
+        }
+      }
+    }
+    if (!isset($item['access callback']) && isset($item['access arguments'])) {
+      // Default callback.
+      $item['access callback'] = 'user_access';
+    }
+    if (!isset($item['access callback']) || empty($item['page callback'])) {
+      $item['access callback'] = 0;
+    }
+    if (is_bool($item['access callback'])) {
+      $item['access callback'] = intval($item['access callback']);
+    }
+
+    $item['load_functions'] = empty($item['_load_functions']) ? '' : serialize($item['_load_functions']);
+    $item += array(
+      'access arguments' => array(),
+      'access callback' => '',
+      'page arguments' => array(),
+      'page callback' => '',
+      'delivery callback' => '',
+      'title arguments' => array(),
+      'title callback' => 't',
+      'theme arguments' => array(),
+      'theme callback' => '',
+      'description' => '',
+      'position' => '',
+      'context' => 0,
+      'tab_parent' => '',
+      'tab_root' => $path,
+      'path' => $path,
+      'file' => '',
+      'file path' => '',
+      'include file' => '',
+    );
+
+    // Calculate out the file to be included for each callback, if any.
+    if ($item['file']) {
+      $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']);
+      $item['include file'] = $file_path . '/' . $item['file'];
+    }
+  }
+
+  // Sort the masks so they are in order of descending fit.
+  $masks = array_keys($masks);
+  rsort($masks);
+
+  return array($menu, $masks);
+}
+
+/**
+ * Saves data from menu_router_build() to the router table.
+ */
+function _menu_router_save($menu, $masks) {
+  // Delete the existing router since we have some data to replace it.
+  db_truncate('menu_router')->execute();
+
+  // Prepare insert object.
+  $insert = db_insert('menu_router')
+    ->fields(array(
+      'path',
+      'load_functions',
+      'to_arg_functions',
+      'access_callback',
+      'access_arguments',
+      'page_callback',
+      'page_arguments',
+      'delivery_callback',
+      'fit',
+      'number_parts',
+      'context',
+      'tab_parent',
+      'tab_root',
+      'title',
+      'title_callback',
+      'title_arguments',
+      'theme_callback',
+      'theme_arguments',
+      'type',
+      'description',
+      'position',
+      'weight',
+      'include_file',
+    ));
+
+  $num_records = 0;
+
+  foreach ($menu as $path => $item) {
+    // Fill in insert object values.
+    $insert->values(array(
+      'path' => $item['path'],
+      'load_functions' => $item['load_functions'],
+      'to_arg_functions' => $item['to_arg_functions'],
+      'access_callback' => $item['access callback'],
+      'access_arguments' => serialize($item['access arguments']),
+      'page_callback' => $item['page callback'],
+      'page_arguments' => serialize($item['page arguments']),
+      'delivery_callback' => $item['delivery callback'],
+      'fit' => $item['_fit'],
+      'number_parts' => $item['_number_parts'],
+      'context' => $item['context'],
+      'tab_parent' => $item['tab_parent'],
+      'tab_root' => $item['tab_root'],
+      'title' => $item['title'],
+      'title_callback' => $item['title callback'],
+      'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''),
+      'theme_callback' => $item['theme callback'],
+      'theme_arguments' => serialize($item['theme arguments']),
+      'type' => $item['type'],
+      'description' => $item['description'],
+      'position' => $item['position'],
+      'weight' => $item['weight'],
+      'include_file' => $item['include file'],
+    ));
+
+    // Execute in batches to avoid the memory overhead of all of those records
+    // in the query object.
+    if (++$num_records == 20) {
+      $insert->execute();
+      $num_records = 0;
+    }
+  }
+  // Insert any remaining records.
+  $insert->execute();
+  // Store the masks.
+  variable_set('menu_masks', $masks);
+
+  return $menu;
+}
+
+/**
+ * Checks whether the site is in maintenance mode.
+ *
+ * This function will log the current user out and redirect to front page
+ * if the current user has no 'access site in maintenance mode' permission.
+ *
+ * @param $check_only
+ *   If this is set to TRUE, the function will perform the access checks and
+ *   return the site offline status, but not log the user out or display any
+ *   messages.
+ *
+ * @return
+ *   FALSE if the site is not in maintenance mode, the user login page is
+ *   displayed, or the user has the 'access site in maintenance mode'
+ *   permission. TRUE for anonymous users not being on the login page when the
+ *   site is in maintenance mode.
+ */
+function _menu_site_is_offline($check_only = FALSE) {
+  // Check if site is in maintenance mode.
+  if (variable_get('maintenance_mode', 0)) {
+    if (user_access('access site in maintenance mode')) {
+      // Ensure that the maintenance mode message is displayed only once
+      // (allowing for page redirects) and specifically suppress its display on
+      // the maintenance mode settings page.
+      if (!$check_only && $_GET['q'] != 'admin/config/development/maintenance') {
+        if (user_access('administer site configuration')) {
+          drupal_set_message(t('Operating in maintenance mode. <a href="@url">Go online.</a>', array('@url' => url('admin/config/development/maintenance'))), 'status', FALSE);
+        }
+        else {
+          drupal_set_message(t('Operating in maintenance mode.'), 'status', FALSE);
+        }
+      }
+    }
+    else {
+      return TRUE;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * @} End of "defgroup menu".
+ */

+ 1065 - 0
includes/module.inc

@@ -0,0 +1,1065 @@
+<?php
+
+/**
+ * @file
+ * API for loading and interacting with Drupal modules.
+ */
+
+/**
+ * Loads all the modules that have been enabled in the system table.
+ *
+ * @param $bootstrap
+ *   Whether to load only the reduced set of modules loaded in "bootstrap mode"
+ *   for cached pages. See bootstrap.inc.
+ *
+ * @return
+ *   If $bootstrap is NULL, return a boolean indicating whether all modules
+ *   have been loaded.
+ */
+function module_load_all($bootstrap = FALSE) {
+  static $has_run = FALSE;
+
+  if (isset($bootstrap)) {
+    foreach (module_list(TRUE, $bootstrap) as $module) {
+      drupal_load('module', $module);
+    }
+    // $has_run will be TRUE if $bootstrap is FALSE.
+    $has_run = !$bootstrap;
+  }
+  return $has_run;
+}
+
+
+/**
+ * Returns a list of currently active modules.
+ *
+ * Usually, this returns a list of all enabled modules. When called early on in
+ * the bootstrap, it will return a list of vital modules only (those needed to
+ * generate cached pages).
+ *
+ * All parameters to this function are optional and should generally not be
+ * changed from their defaults.
+ *
+ * @param $refresh
+ *   (optional) Whether to force the module list to be regenerated (such as
+ *   after the administrator has changed the system settings). Defaults to
+ *   FALSE.
+ * @param $bootstrap_refresh
+ *   (optional) When $refresh is TRUE, setting $bootstrap_refresh to TRUE forces
+ *   the module list to be regenerated using the reduced set of modules loaded
+ *   in "bootstrap mode" for cached pages. Otherwise, setting $refresh to TRUE
+ *   generates the complete list of enabled modules.
+ * @param $sort
+ *   (optional) By default, modules are ordered by weight and module name. Set
+ *   this option to TRUE to return a module list ordered only by module name.
+ * @param $fixed_list
+ *   (optional) If an array of module names is provided, this will override the
+ *   module list with the given set of modules. This will persist until the next
+ *   call with $refresh set to TRUE or with a new $fixed_list passed in. This
+ *   parameter is primarily intended for internal use (e.g., in install.php and
+ *   update.php).
+ *
+ * @return
+ *   An associative array whose keys and values are the names of the modules in
+ *   the list.
+ */
+function module_list($refresh = FALSE, $bootstrap_refresh = FALSE, $sort = FALSE, $fixed_list = NULL) {
+  static $list = array(), $sorted_list;
+
+  if (empty($list) || $refresh || $fixed_list) {
+    $list = array();
+    $sorted_list = NULL;
+    if ($fixed_list) {
+      foreach ($fixed_list as $name => $module) {
+        drupal_get_filename('module', $name, $module['filename']);
+        $list[$name] = $name;
+      }
+    }
+    else {
+      if ($refresh) {
+        // For the $refresh case, make sure that system_list() returns fresh
+        // data.
+        drupal_static_reset('system_list');
+      }
+      if ($bootstrap_refresh) {
+        $list = system_list('bootstrap');
+      }
+      else {
+        // Not using drupal_map_assoc() here as that requires common.inc.
+        $list = array_keys(system_list('module_enabled'));
+        $list = (!empty($list) ? array_combine($list, $list) : array());
+      }
+    }
+  }
+  if ($sort) {
+    if (!isset($sorted_list)) {
+      $sorted_list = $list;
+      ksort($sorted_list);
+    }
+    return $sorted_list;
+  }
+  return $list;
+}
+
+/**
+ * Builds a list of bootstrap modules and enabled modules and themes.
+ *
+ * @param $type
+ *   The type of list to return:
+ *   - module_enabled: All enabled modules.
+ *   - bootstrap: All enabled modules required for bootstrap.
+ *   - theme: All themes.
+ *
+ * @return
+ *   An associative array of modules or themes, keyed by name. For $type
+ *   'bootstrap', the array values equal the keys. For $type 'module_enabled'
+ *   or 'theme', the array values are objects representing the respective
+ *   database row, with the 'info' property already unserialized.
+ *
+ * @see module_list()
+ * @see list_themes()
+ */
+function system_list($type) {
+  $lists = &drupal_static(__FUNCTION__);
+
+  // For bootstrap modules, attempt to fetch the list from cache if possible.
+  // if not fetch only the required information to fire bootstrap hooks
+  // in case we are going to serve the page from cache.
+  if ($type == 'bootstrap') {
+    if (isset($lists['bootstrap'])) {
+      return $lists['bootstrap'];
+    }
+    if ($cached = cache_get('bootstrap_modules', 'cache_bootstrap')) {
+      $bootstrap_list = $cached->data;
+    }
+    else {
+      $bootstrap_list = db_query("SELECT name, filename FROM {system} WHERE status = 1 AND bootstrap = 1 AND type = 'module' ORDER BY weight ASC, name ASC")->fetchAllAssoc('name');
+      cache_set('bootstrap_modules', $bootstrap_list, 'cache_bootstrap');
+    }
+    // To avoid a separate database lookup for the filepath, prime the
+    // drupal_get_filename() static cache for bootstrap modules only.
+    // The rest is stored separately to keep the bootstrap module cache small.
+    foreach ($bootstrap_list as $module) {
+      drupal_get_filename('module', $module->name, $module->filename);
+    }
+    // We only return the module names here since module_list() doesn't need
+    // the filename itself.
+    $lists['bootstrap'] = array_keys($bootstrap_list);
+  }
+  // Otherwise build the list for enabled modules and themes.
+  elseif (!isset($lists['module_enabled'])) {
+    if ($cached = cache_get('system_list', 'cache_bootstrap')) {
+      $lists = $cached->data;
+    }
+    else {
+      $lists = array(
+        'module_enabled' => array(),
+        'theme' => array(),
+        'filepaths' => array(),
+      );
+      // The module name (rather than the filename) is used as the fallback
+      // weighting in order to guarantee consistent behavior across different
+      // Drupal installations, which might have modules installed in different
+      // locations in the file system. The ordering here must also be
+      // consistent with the one used in module_implements().
+      $result = db_query("SELECT * FROM {system} WHERE type = 'theme' OR (type = 'module' AND status = 1) ORDER BY weight ASC, name ASC");
+      foreach ($result as $record) {
+        $record->info = unserialize($record->info);
+        // Build a list of all enabled modules.
+        if ($record->type == 'module') {
+          $lists['module_enabled'][$record->name] = $record;
+        }
+        // Build a list of themes.
+        if ($record->type == 'theme') {
+          $lists['theme'][$record->name] = $record;
+        }
+        // Build a list of filenames so drupal_get_filename can use it.
+        if ($record->status) {
+          $lists['filepaths'][] = array('type' => $record->type, 'name' => $record->name, 'filepath' => $record->filename);
+        }
+      }
+      foreach ($lists['theme'] as $key => $theme) {
+        if (!empty($theme->info['base theme'])) {
+          // Make a list of the theme's base themes.
+          require_once DRUPAL_ROOT . '/includes/theme.inc';
+          $lists['theme'][$key]->base_themes = drupal_find_base_themes($lists['theme'], $key);
+          // Don't proceed if there was a problem with the root base theme.
+          if (!current($lists['theme'][$key]->base_themes)) {
+            continue;
+          }
+          // Determine the root base theme.
+          $base_key = key($lists['theme'][$key]->base_themes);
+          // Add to the list of sub-themes for each of the theme's base themes.
+          foreach (array_keys($lists['theme'][$key]->base_themes) as $base_theme) {
+            $lists['theme'][$base_theme]->sub_themes[$key] = $lists['theme'][$key]->info['name'];
+          }
+          // Add the base theme's theme engine info.
+          $lists['theme'][$key]->info['engine'] = isset($lists['theme'][$base_key]->info['engine']) ? $lists['theme'][$base_key]->info['engine'] : 'theme';
+        }
+        else {
+          // A plain theme is its own engine.
+          $base_key = $key;
+          if (!isset($lists['theme'][$key]->info['engine'])) {
+            $lists['theme'][$key]->info['engine'] = 'theme';
+          }
+        }
+        // Set the theme engine prefix.
+        $lists['theme'][$key]->prefix = ($lists['theme'][$key]->info['engine'] == 'theme') ? $base_key : $lists['theme'][$key]->info['engine'];
+      }
+      cache_set('system_list', $lists, 'cache_bootstrap');
+    }
+    // To avoid a separate database lookup for the filepath, prime the
+    // drupal_get_filename() static cache with all enabled modules and themes.
+    foreach ($lists['filepaths'] as $item) {
+      drupal_get_filename($item['type'], $item['name'], $item['filepath']);
+    }
+  }
+
+  return $lists[$type];
+}
+
+/**
+ * Resets all system_list() caches.
+ */
+function system_list_reset() {
+  drupal_static_reset('system_list');
+  drupal_static_reset('system_rebuild_module_data');
+  drupal_static_reset('list_themes');
+  cache_clear_all('bootstrap_modules', 'cache_bootstrap');
+  cache_clear_all('system_list', 'cache_bootstrap');
+}
+
+/**
+ * Determines which modules require and are required by each module.
+ *
+ * @param $files
+ *   The array of filesystem objects used to rebuild the cache.
+ *
+ * @return
+ *   The same array with the new keys for each module:
+ *   - requires: An array with the keys being the modules that this module
+ *     requires.
+ *   - required_by: An array with the keys being the modules that will not work
+ *     without this module.
+ */
+function _module_build_dependencies($files) {
+  require_once DRUPAL_ROOT . '/includes/graph.inc';
+  foreach ($files as $filename => $file) {
+    $graph[$file->name]['edges'] = array();
+    if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) {
+      foreach ($file->info['dependencies'] as $dependency) {
+        $dependency_data = drupal_parse_dependency($dependency);
+        $graph[$file->name]['edges'][$dependency_data['name']] = $dependency_data;
+      }
+    }
+  }
+  drupal_depth_first_search($graph);
+  foreach ($graph as $module => $data) {
+    $files[$module]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : array();
+    $files[$module]->requires = isset($data['paths']) ? $data['paths'] : array();
+    $files[$module]->sort = $data['weight'];
+  }
+  return $files;
+}
+
+/**
+ * Determines whether a given module exists.
+ *
+ * @param $module
+ *   The name of the module (without the .module extension).
+ *
+ * @return
+ *   TRUE if the module is both installed and enabled.
+ */
+function module_exists($module) {
+  $list = module_list();
+  return isset($list[$module]);
+}
+
+/**
+ * Loads a module's installation hooks.
+ *
+ * @param $module
+ *   The name of the module (without the .module extension).
+ *
+ * @return
+ *   The name of the module's install file, if successful; FALSE otherwise.
+ */
+function module_load_install($module) {
+  // Make sure the installation API is available
+  include_once DRUPAL_ROOT . '/includes/install.inc';
+
+  return module_load_include('install', $module);
+}
+
+/**
+ * Loads a module include file.
+ *
+ * Examples:
+ * @code
+ *   // Load node.admin.inc from the node module.
+ *   module_load_include('inc', 'node', 'node.admin');
+ *   // Load content_types.inc from the node module.
+ *   module_load_include('inc', 'node', 'content_types');
+ * @endcode
+ *
+ * Do not use this function to load an install file, use module_load_install()
+ * instead. Do not use this function in a global context since it requires
+ * Drupal to be fully bootstrapped, use require_once DRUPAL_ROOT . '/path/file'
+ * instead.
+ *
+ * @param $type
+ *   The include file's type (file extension).
+ * @param $module
+ *   The module to which the include file belongs.
+ * @param $name
+ *   (optional) The base file name (without the $type extension). If omitted,
+ *   $module is used; i.e., resulting in "$module.$type" by default.
+ *
+ * @return
+ *   The name of the included file, if successful; FALSE otherwise.
+ */
+function module_load_include($type, $module, $name = NULL) {
+  if (!isset($name)) {
+    $name = $module;
+  }
+
+  if (function_exists('drupal_get_path')) {
+    $file = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$name.$type";
+    if (is_file($file)) {
+      require_once $file;
+      return $file;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Loads an include file for each module enabled in the {system} table.
+ */
+function module_load_all_includes($type, $name = NULL) {
+  $modules = module_list();
+  foreach ($modules as $module) {
+    module_load_include($type, $module, $name);
+  }
+}
+
+/**
+ * Enables or installs a given list of modules.
+ *
+ * Definitions:
+ * - "Enabling" is the process of activating a module for use by Drupal.
+ * - "Disabling" is the process of deactivating a module.
+ * - "Installing" is the process of enabling it for the first time or after it
+ *   has been uninstalled.
+ * - "Uninstalling" is the process of removing all traces of a module.
+ *
+ * Order of events:
+ * - Gather and add module dependencies to $module_list (if applicable).
+ * - For each module that is being enabled:
+ *   - Install module schema and update system registries and caches.
+ *   - If the module is being enabled for the first time or had been
+ *     uninstalled, invoke hook_install() and add it to the list of installed
+ *     modules.
+ *   - Invoke hook_enable().
+ * - Invoke hook_modules_installed().
+ * - Invoke hook_modules_enabled().
+ *
+ * @param $module_list
+ *   An array of module names.
+ * @param $enable_dependencies
+ *   If TRUE, dependencies will automatically be added and enabled in the
+ *   correct order. This incurs a significant performance cost, so use FALSE
+ *   if you know $module_list is already complete and in the correct order.
+ *
+ * @return
+ *   FALSE if one or more dependencies are missing, TRUE otherwise.
+ *
+ * @see hook_install()
+ * @see hook_enable()
+ * @see hook_modules_installed()
+ * @see hook_modules_enabled()
+ */
+function module_enable($module_list, $enable_dependencies = TRUE) {
+  if ($enable_dependencies) {
+    // Get all module data so we can find dependencies and sort.
+    $module_data = system_rebuild_module_data();
+    // Create an associative array with weights as values.
+    $module_list = array_flip(array_values($module_list));
+
+    while (list($module) = each($module_list)) {
+      if (!isset($module_data[$module])) {
+        // This module is not found in the filesystem, abort.
+        return FALSE;
+      }
+      if ($module_data[$module]->status) {
+        // Skip already enabled modules.
+        unset($module_list[$module]);
+        continue;
+      }
+      $module_list[$module] = $module_data[$module]->sort;
+
+      // Add dependencies to the list, with a placeholder weight.
+      // The new modules will be processed as the while loop continues.
+      foreach (array_keys($module_data[$module]->requires) as $dependency) {
+        if (!isset($module_list[$dependency])) {
+          $module_list[$dependency] = 0;
+        }
+      }
+    }
+
+    if (!$module_list) {
+      // Nothing to do. All modules already enabled.
+      return TRUE;
+    }
+
+    // Sort the module list by pre-calculated weights.
+    arsort($module_list);
+    $module_list = array_keys($module_list);
+  }
+
+  // Required for module installation checks.
+  include_once DRUPAL_ROOT . '/includes/install.inc';
+
+  $modules_installed = array();
+  $modules_enabled = array();
+  foreach ($module_list as $module) {
+    // Only process modules that are not already enabled.
+    $existing = db_query("SELECT status FROM {system} WHERE type = :type AND name = :name", array(
+      ':type' => 'module',
+      ':name' => $module))
+      ->fetchObject();
+    if ($existing->status == 0) {
+      // Load the module's code.
+      drupal_load('module', $module);
+      module_load_install($module);
+
+      // Update the database and module list to reflect the new module. This
+      // needs to be done first so that the module's hook implementations,
+      // hook_schema() in particular, can be called while it is being
+      // installed.
+      db_update('system')
+        ->fields(array('status' => 1))
+        ->condition('type', 'module')
+        ->condition('name', $module)
+        ->execute();
+      // Refresh the module list to include it.
+      system_list_reset();
+      module_list(TRUE);
+      module_implements('', FALSE, TRUE);
+      _system_update_bootstrap_status();
+      // Update the registry to include it.
+      registry_update();
+      // Refresh the schema to include it.
+      drupal_get_schema(NULL, TRUE);
+      // Update the theme registry to include it.
+      drupal_theme_rebuild();
+      // Clear entity cache.
+      entity_info_cache_clear();
+
+      // Now install the module if necessary.
+      if (drupal_get_installed_schema_version($module, TRUE) == SCHEMA_UNINSTALLED) {
+        drupal_install_schema($module);
+
+        // Set the schema version to the number of the last update provided
+        // by the module.
+        $versions = drupal_get_schema_versions($module);
+        $version = $versions ? max($versions) : SCHEMA_INSTALLED;
+
+        // If the module has no current updates, but has some that were
+        // previously removed, set the version to the value of
+        // hook_update_last_removed().
+        if ($last_removed = module_invoke($module, 'update_last_removed')) {
+          $version = max($version, $last_removed);
+        }
+        drupal_set_installed_schema_version($module, $version);
+        // Allow the module to perform install tasks.
+        module_invoke($module, 'install');
+        // Record the fact that it was installed.
+        $modules_installed[] = $module;
+        watchdog('system', '%module module installed.', array('%module' => $module), WATCHDOG_INFO);
+      }
+
+      // Enable the module.
+      module_invoke($module, 'enable');
+
+      // Record the fact that it was enabled.
+      $modules_enabled[] = $module;
+      watchdog('system', '%module module enabled.', array('%module' => $module), WATCHDOG_INFO);
+    }
+  }
+
+  // If any modules were newly installed, invoke hook_modules_installed().
+  if (!empty($modules_installed)) {
+    module_invoke_all('modules_installed', $modules_installed);
+  }
+
+  // If any modules were newly enabled, invoke hook_modules_enabled().
+  if (!empty($modules_enabled)) {
+    module_invoke_all('modules_enabled', $modules_enabled);
+  }
+
+  return TRUE;
+}
+
+/**
+ * Disables a given set of modules.
+ *
+ * @param $module_list
+ *   An array of module names.
+ * @param $disable_dependents
+ *   If TRUE, dependent modules will automatically be added and disabled in the
+ *   correct order. This incurs a significant performance cost, so use FALSE
+ *   if you know $module_list is already complete and in the correct order.
+ */
+function module_disable($module_list, $disable_dependents = TRUE) {
+  if ($disable_dependents) {
+    // Get all module data so we can find dependents and sort.
+    $module_data = system_rebuild_module_data();
+    // Create an associative array with weights as values.
+    $module_list = array_flip(array_values($module_list));
+
+    $profile = drupal_get_profile();
+    while (list($module) = each($module_list)) {
+      if (!isset($module_data[$module]) || !$module_data[$module]->status) {
+        // This module doesn't exist or is already disabled, skip it.
+        unset($module_list[$module]);
+        continue;
+      }
+      $module_list[$module] = $module_data[$module]->sort;
+
+      // Add dependent modules to the list, with a placeholder weight.
+      // The new modules will be processed as the while loop continues.
+      foreach ($module_data[$module]->required_by as $dependent => $dependent_data) {
+        if (!isset($module_list[$dependent]) && $dependent != $profile) {
+          $module_list[$dependent] = 0;
+        }
+      }
+    }
+
+    // Sort the module list by pre-calculated weights.
+    asort($module_list);
+    $module_list = array_keys($module_list);
+  }
+
+  $invoke_modules = array();
+
+  foreach ($module_list as $module) {
+    if (module_exists($module)) {
+      // Check if node_access table needs rebuilding.
+      if (!node_access_needs_rebuild() && module_hook($module, 'node_grants')) {
+        node_access_needs_rebuild(TRUE);
+      }
+
+      module_load_install($module);
+      module_invoke($module, 'disable');
+      db_update('system')
+        ->fields(array('status' => 0))
+        ->condition('type', 'module')
+        ->condition('name', $module)
+        ->execute();
+      $invoke_modules[] = $module;
+      watchdog('system', '%module module disabled.', array('%module' => $module), WATCHDOG_INFO);
+    }
+  }
+
+  if (!empty($invoke_modules)) {
+    // Refresh the module list to exclude the disabled modules.
+    system_list_reset();
+    module_list(TRUE);
+    module_implements('', FALSE, TRUE);
+    entity_info_cache_clear();
+    // Invoke hook_modules_disabled before disabling modules,
+    // so we can still call module hooks to get information.
+    module_invoke_all('modules_disabled', $invoke_modules);
+    // Update the registry to remove the newly-disabled module.
+    registry_update();
+    _system_update_bootstrap_status();
+    // Update the theme registry to remove the newly-disabled module.
+    drupal_theme_rebuild();
+  }
+
+  // If there remains no more node_access module, rebuilding will be
+  // straightforward, we can do it right now.
+  if (node_access_needs_rebuild() && count(module_implements('node_grants')) == 0) {
+    node_access_rebuild();
+  }
+}
+
+/**
+ * @defgroup hooks Hooks
+ * @{
+ * Allow modules to interact with the Drupal core.
+ *
+ * Drupal's module system is based on the concept of "hooks". A hook is a PHP
+ * function that is named foo_bar(), where "foo" is the name of the module
+ * (whose filename is thus foo.module) and "bar" is the name of the hook. Each
+ * hook has a defined set of parameters and a specified result type.
+ *
+ * To extend Drupal, a module need simply implement a hook. When Drupal wishes
+ * to allow intervention from modules, it determines which modules implement a
+ * hook and calls that hook in all enabled modules that implement it.
+ *
+ * The available hooks to implement are explained here in the Hooks section of
+ * the developer documentation. The string "hook" is used as a placeholder for
+ * the module name in the hook definitions. For example, if the module file is
+ * called example.module, then hook_help() as implemented by that module would
+ * be defined as example_help().
+ *
+ * The example functions included are not part of the Drupal core, they are
+ * just models that you can modify. Only the hooks implemented within modules
+ * are executed when running Drupal.
+ *
+ * See also @link themeable the themeable group page. @endlink
+ */
+
+/**
+ * Determines whether a module implements a hook.
+ *
+ * @param $module
+ *   The name of the module (without the .module extension).
+ * @param $hook
+ *   The name of the hook (e.g. "help" or "menu").
+ *
+ * @return
+ *   TRUE if the module is both installed and enabled, and the hook is
+ *   implemented in that module.
+ */
+function module_hook($module, $hook) {
+  $function = $module . '_' . $hook;
+  if (function_exists($function)) {
+    return TRUE;
+  }
+  // If the hook implementation does not exist, check whether it may live in an
+  // optional include file registered via hook_hook_info().
+  $hook_info = module_hook_info();
+  if (isset($hook_info[$hook]['group'])) {
+    module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']);
+    if (function_exists($function)) {
+      return TRUE;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Determines which modules are implementing a hook.
+ *
+ * @param $hook
+ *   The name of the hook (e.g. "help" or "menu").
+ * @param $sort
+ *   By default, modules are ordered by weight and filename, settings this option
+ *   to TRUE, module list will be ordered by module name.
+ * @param $reset
+ *   For internal use only: Whether to force the stored list of hook
+ *   implementations to be regenerated (such as after enabling a new module,
+ *   before processing hook_enable).
+ *
+ * @return
+ *   An array with the names of the modules which are implementing this hook.
+ *
+ * @see module_implements_write_cache()
+ */
+function module_implements($hook, $sort = FALSE, $reset = FALSE) {
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['implementations'] = &drupal_static(__FUNCTION__);
+  }
+  $implementations = &$drupal_static_fast['implementations'];
+
+  // We maintain a persistent cache of hook implementations in addition to the
+  // static cache to avoid looping through every module and every hook on each
+  // request. Benchmarks show that the benefit of this caching outweighs the
+  // additional database hit even when using the default database caching
+  // backend and only a small number of modules are enabled. The cost of the
+  // cache_get() is more or less constant and reduced further when non-database
+  // caching backends are used, so there will be more significant gains when a
+  // large number of modules are installed or hooks invoked, since this can
+  // quickly lead to module_hook() being called several thousand times
+  // per request.
+  if ($reset) {
+    $implementations = array();
+    cache_set('module_implements', array(), 'cache_bootstrap');
+    drupal_static_reset('module_hook_info');
+    drupal_static_reset('drupal_alter');
+    cache_clear_all('hook_info', 'cache_bootstrap');
+    return;
+  }
+
+  // Fetch implementations from cache.
+  if (empty($implementations)) {
+    $implementations = cache_get('module_implements', 'cache_bootstrap');
+    if ($implementations === FALSE) {
+      $implementations = array();
+    }
+    else {
+      $implementations = $implementations->data;
+    }
+  }
+
+  if (!isset($implementations[$hook])) {
+    // The hook is not cached, so ensure that whether or not it has
+    // implementations, that the cache is updated at the end of the request.
+    $implementations['#write_cache'] = TRUE;
+    $hook_info = module_hook_info();
+    $implementations[$hook] = array();
+    $list = module_list(FALSE, FALSE, $sort);
+    foreach ($list as $module) {
+      $include_file = isset($hook_info[$hook]['group']) && module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']);
+      // Since module_hook() may needlessly try to load the include file again,
+      // function_exists() is used directly here.
+      if (function_exists($module . '_' . $hook)) {
+        $implementations[$hook][$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
+      }
+    }
+    // Allow modules to change the weight of specific implementations but avoid
+    // an infinite loop.
+    if ($hook != 'module_implements_alter') {
+      drupal_alter('module_implements', $implementations[$hook], $hook);
+    }
+  }
+  else {
+    foreach ($implementations[$hook] as $module => $group) {
+      // If this hook implementation is stored in a lazy-loaded file, so include
+      // that file first.
+      if ($group) {
+        module_load_include('inc', $module, "$module.$group");
+      }
+      // It is possible that a module removed a hook implementation without the
+      // implementations cache being rebuilt yet, so we check whether the
+      // function exists on each request to avoid undefined function errors.
+      // Since module_hook() may needlessly try to load the include file again,
+      // function_exists() is used directly here.
+      if (!function_exists($module . '_' . $hook)) {
+        // Clear out the stale implementation from the cache and force a cache
+        // refresh to forget about no longer existing hook implementations.
+        unset($implementations[$hook][$module]);
+        $implementations['#write_cache'] = TRUE;
+      }
+    }
+  }
+
+  return array_keys($implementations[$hook]);
+}
+
+/**
+ * Retrieves a list of hooks that are declared through hook_hook_info().
+ *
+ * @return
+ *   An associative array whose keys are hook names and whose values are an
+ *   associative array containing a group name. The structure of the array
+ *   is the same as the return value of hook_hook_info().
+ *
+ * @see hook_hook_info()
+ */
+function module_hook_info() {
+  // This function is indirectly invoked from bootstrap_invoke_all(), in which
+  // case common.inc, subsystems, and modules are not loaded yet, so it does not
+  // make sense to support hook groups resp. lazy-loaded include files prior to
+  // full bootstrap.
+  if (drupal_bootstrap(NULL, FALSE) != DRUPAL_BOOTSTRAP_FULL) {
+    return array();
+  }
+  $hook_info = &drupal_static(__FUNCTION__);
+
+  if (!isset($hook_info)) {
+    $hook_info = array();
+    $cache = cache_get('hook_info', 'cache_bootstrap');
+    if ($cache === FALSE) {
+      // Rebuild the cache and save it.
+      // We can't use module_invoke_all() here or it would cause an infinite
+      // loop.
+      foreach (module_list() as $module) {
+        $function = $module . '_hook_info';
+        if (function_exists($function)) {
+          $result = $function();
+          if (isset($result) && is_array($result)) {
+            $hook_info = array_merge_recursive($hook_info, $result);
+          }
+        }
+      }
+      // We can't use drupal_alter() for the same reason as above.
+      foreach (module_list() as $module) {
+        $function = $module . '_hook_info_alter';
+        if (function_exists($function)) {
+          $function($hook_info);
+        }
+      }
+      cache_set('hook_info', $hook_info, 'cache_bootstrap');
+    }
+    else {
+      $hook_info = $cache->data;
+    }
+  }
+
+  return $hook_info;
+}
+
+/**
+ * Writes the hook implementation cache.
+ *
+ * @see module_implements()
+ */
+function module_implements_write_cache() {
+  $implementations = &drupal_static('module_implements');
+  // Check whether we need to write the cache. We do not want to cache hooks
+  // which are only invoked on HTTP POST requests since these do not need to be
+  // optimized as tightly, and not doing so keeps the cache entry smaller.
+  if (isset($implementations['#write_cache']) && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')) {
+    unset($implementations['#write_cache']);
+    cache_set('module_implements', $implementations, 'cache_bootstrap');
+  }
+}
+
+/**
+ * Invokes a hook in a particular module.
+ *
+ * @param $module
+ *   The name of the module (without the .module extension).
+ * @param $hook
+ *   The name of the hook to invoke.
+ * @param ...
+ *   Arguments to pass to the hook implementation.
+ *
+ * @return
+ *   The return value of the hook implementation.
+ */
+function module_invoke($module, $hook) {
+  $args = func_get_args();
+  // Remove $module and $hook from the arguments.
+  unset($args[0], $args[1]);
+  if (module_hook($module, $hook)) {
+    return call_user_func_array($module . '_' . $hook, $args);
+  }
+}
+
+/**
+ * Invokes a hook in all enabled modules that implement it.
+ *
+ * @param $hook
+ *   The name of the hook to invoke.
+ * @param ...
+ *   Arguments to pass to the hook.
+ *
+ * @return
+ *   An array of return values of the hook implementations. If modules return
+ *   arrays from their implementations, those are merged into one array.
+ */
+function module_invoke_all($hook) {
+  $args = func_get_args();
+  // Remove $hook from the arguments.
+  unset($args[0]);
+  $return = array();
+  foreach (module_implements($hook) as $module) {
+    $function = $module . '_' . $hook;
+    if (function_exists($function)) {
+      $result = call_user_func_array($function, $args);
+      if (isset($result) && is_array($result)) {
+        $return = array_merge_recursive($return, $result);
+      }
+      elseif (isset($result)) {
+        $return[] = $result;
+      }
+    }
+  }
+
+  return $return;
+}
+
+/**
+ * @} End of "defgroup hooks".
+ */
+
+/**
+ * Returns an array of modules required by core.
+ */
+function drupal_required_modules() {
+  $files = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info$/', 'modules', 'name', 0);
+  $required = array();
+
+  // An installation profile is required and one must always be loaded.
+  $required[] = drupal_get_profile();
+
+  foreach ($files as $name => $file) {
+    $info = drupal_parse_info_file($file->uri);
+    if (!empty($info) && !empty($info['required']) && $info['required']) {
+      $required[] = $name;
+    }
+  }
+
+  return $required;
+}
+
+/**
+ * Passes alterable variables to specific hook_TYPE_alter() implementations.
+ *
+ * This dispatch function hands off the passed-in variables to type-specific
+ * hook_TYPE_alter() implementations in modules. It ensures a consistent
+ * interface for all altering operations.
+ *
+ * A maximum of 2 alterable arguments is supported (a third is supported for
+ * legacy reasons, but should not be used in new code). In case more arguments
+ * need to be passed and alterable, modules provide additional variables
+ * assigned by reference in the last $context argument:
+ * @code
+ *   $context = array(
+ *     'alterable' => &$alterable,
+ *     'unalterable' => $unalterable,
+ *     'foo' => 'bar',
+ *   );
+ *   drupal_alter('mymodule_data', $alterable1, $alterable2, $context);
+ * @endcode
+ *
+ * Note that objects are always passed by reference in PHP5. If it is absolutely
+ * required that no implementation alters a passed object in $context, then an
+ * object needs to be cloned:
+ * @code
+ *   $context = array(
+ *     'unalterable_object' => clone $object,
+ *   );
+ *   drupal_alter('mymodule_data', $data, $context);
+ * @endcode
+ *
+ * @param $type
+ *   A string describing the type of the alterable $data. 'form', 'links',
+ *   'node_content', and so on are several examples. Alternatively can be an
+ *   array, in which case hook_TYPE_alter() is invoked for each value in the
+ *   array, ordered first by module, and then for each module, in the order of
+ *   values in $type. For example, when Form API is using drupal_alter() to
+ *   execute both hook_form_alter() and hook_form_FORM_ID_alter()
+ *   implementations, it passes array('form', 'form_' . $form_id) for $type.
+ * @param $data
+ *   The variable that will be passed to hook_TYPE_alter() implementations to be
+ *   altered. The type of this variable depends on the value of the $type
+ *   argument. For example, when altering a 'form', $data will be a structured
+ *   array. When altering a 'profile', $data will be an object.
+ * @param $context1
+ *   (optional) An additional variable that is passed by reference.
+ * @param $context2
+ *   (optional) An additional variable that is passed by reference. If more
+ *   context needs to be provided to implementations, then this should be an
+ *   associative array as described above.
+ * @param $context3
+ *   (optional) An additional variable that is passed by reference. This
+ *   parameter is deprecated and will not exist in Drupal 8; consequently, it
+ *   should not be used for new Drupal 7 code either. It is here only for
+ *   backwards compatibility with older code that passed additional arguments
+ *   to drupal_alter().
+ */
+function drupal_alter($type, &$data, &$context1 = NULL, &$context2 = NULL, &$context3 = NULL) {
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['functions'] = &drupal_static(__FUNCTION__);
+  }
+  $functions = &$drupal_static_fast['functions'];
+
+  // Most of the time, $type is passed as a string, so for performance,
+  // normalize it to that. When passed as an array, usually the first item in
+  // the array is a generic type, and additional items in the array are more
+  // specific variants of it, as in the case of array('form', 'form_FORM_ID').
+  if (is_array($type)) {
+    $cid = implode(',', $type);
+    $extra_types = $type;
+    $type = array_shift($extra_types);
+    // Allow if statements in this function to use the faster isset() rather
+    // than !empty() both when $type is passed as a string, or as an array with
+    // one item.
+    if (empty($extra_types)) {
+      unset($extra_types);
+    }
+  }
+  else {
+    $cid = $type;
+  }
+
+  // Some alter hooks are invoked many times per page request, so statically
+  // cache the list of functions to call, and on subsequent calls, iterate
+  // through them quickly.
+  if (!isset($functions[$cid])) {
+    $functions[$cid] = array();
+    $hook = $type . '_alter';
+    $modules = module_implements($hook);
+    if (!isset($extra_types)) {
+      // For the more common case of a single hook, we do not need to call
+      // function_exists(), since module_implements() returns only modules with
+      // implementations.
+      foreach ($modules as $module) {
+        $functions[$cid][] = $module . '_' . $hook;
+      }
+    }
+    else {
+      // For multiple hooks, we need $modules to contain every module that
+      // implements at least one of them.
+      $extra_modules = array();
+      foreach ($extra_types as $extra_type) {
+        $extra_modules = array_merge($extra_modules, module_implements($extra_type . '_alter'));
+      }
+      // If any modules implement one of the extra hooks that do not implement
+      // the primary hook, we need to add them to the $modules array in their
+      // appropriate order. module_implements() can only return ordered
+      // implementations of a single hook. To get the ordered implementations
+      // of multiple hooks, we mimic the module_implements() logic of first
+      // ordering by module_list(), and then calling
+      // drupal_alter('module_implements').
+      if (array_diff($extra_modules, $modules)) {
+        // Merge the arrays and order by module_list().
+        $modules = array_intersect(module_list(), array_merge($modules, $extra_modules));
+        // Since module_implements() already took care of loading the necessary
+        // include files, we can safely pass FALSE for the array values.
+        $implementations = array_fill_keys($modules, FALSE);
+        // Let modules adjust the order solely based on the primary hook. This
+        // ensures the same module order regardless of whether this if block
+        // runs. Calling drupal_alter() recursively in this way does not result
+        // in an infinite loop, because this call is for a single $type, so we
+        // won't end up in this code block again.
+        drupal_alter('module_implements', $implementations, $hook);
+        $modules = array_keys($implementations);
+      }
+      foreach ($modules as $module) {
+        // Since $modules is a merged array, for any given module, we do not
+        // know whether it has any particular implementation, so we need a
+        // function_exists().
+        $function = $module . '_' . $hook;
+        if (function_exists($function)) {
+          $functions[$cid][] = $function;
+        }
+        foreach ($extra_types as $extra_type) {
+          $function = $module . '_' . $extra_type . '_alter';
+          if (function_exists($function)) {
+            $functions[$cid][] = $function;
+          }
+        }
+      }
+    }
+    // Allow the theme to alter variables after the theme system has been
+    // initialized.
+    global $theme, $base_theme_info;
+    if (isset($theme)) {
+      $theme_keys = array();
+      foreach ($base_theme_info as $base) {
+        $theme_keys[] = $base->name;
+      }
+      $theme_keys[] = $theme;
+      foreach ($theme_keys as $theme_key) {
+        $function = $theme_key . '_' . $hook;
+        if (function_exists($function)) {
+          $functions[$cid][] = $function;
+        }
+        if (isset($extra_types)) {
+          foreach ($extra_types as $extra_type) {
+            $function = $theme_key . '_' . $extra_type . '_alter';
+            if (function_exists($function)) {
+              $functions[$cid][] = $function;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  foreach ($functions[$cid] as $function) {
+    $function($data, $context1, $context2, $context3);
+  }
+}

+ 664 - 0
includes/pager.inc

@@ -0,0 +1,664 @@
+<?php
+
+/**
+ * @file
+ * Functions to aid in presenting database results as a set of pages.
+ */
+
+
+/**
+ * Query extender for pager queries.
+ *
+ * This is the "default" pager mechanism.  It creates a paged query with a fixed
+ * number of entries per page.
+ */
+class PagerDefault extends SelectQueryExtender {
+
+  /**
+   * The highest element we've autogenerated so far.
+   *
+   * @var int
+   */
+  static $maxElement = 0;
+
+  /**
+   * The number of elements per page to allow.
+   *
+   * @var int
+   */
+  protected $limit = 10;
+
+  /**
+   * The unique ID of this pager on this page.
+   *
+   * @var int
+   */
+  protected $element = NULL;
+
+  /**
+   * The count query that will be used for this pager.
+   *
+   * @var SelectQueryInterface
+   */
+  protected $customCountQuery = FALSE;
+
+  public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
+    parent::__construct($query, $connection);
+
+    // Add pager tag. Do this here to ensure that it is always added before
+    // preExecute() is called.
+    $this->addTag('pager');
+  }
+
+  /**
+   * Override the execute method.
+   *
+   * Before we run the query, we need to add pager-based range() instructions
+   * to it.
+   */
+  public function execute() {
+
+    // Add convenience tag to mark that this is an extended query. We have to
+    // do this in the constructor to ensure that it is set before preExecute()
+    // gets called.
+    if (!$this->preExecute($this)) {
+      return NULL;
+    }
+
+    // A NULL limit is the "kill switch" for pager queries.
+    if (empty($this->limit)) {
+      return;
+    }
+    $this->ensureElement();
+
+    $total_items = $this->getCountQuery()->execute()->fetchField();
+    $current_page = pager_default_initialize($total_items, $this->limit, $this->element);
+    $this->range($current_page * $this->limit, $this->limit);
+
+    // Now that we've added our pager-based range instructions, run the query normally.
+    return $this->query->execute();
+  }
+
+  /**
+   * Ensure that there is an element associated with this query.
+   * If an element was not specified previously, then the value of the
+   * $maxElement counter is taken, after which the counter is incremented.
+   *
+   * After running this method, access $this->element to get the element for this
+   * query.
+   */
+  protected function ensureElement() {
+    if (!isset($this->element)) {
+      $this->element = self::$maxElement++;
+    }
+  }
+
+  /**
+   * Specify the count query object to use for this pager.
+   *
+   * You will rarely need to specify a count query directly.  If not specified,
+   * one is generated off of the pager query itself.
+   *
+   * @param SelectQueryInterface $query
+   *   The count query object.  It must return a single row with a single column,
+   *   which is the total number of records.
+   */
+  public function setCountQuery(SelectQueryInterface $query) {
+    $this->customCountQuery = $query;
+  }
+
+  /**
+   * Retrieve the count query for this pager.
+   *
+   * The count query may be specified manually or, by default, taken from the
+   * query we are extending.
+   *
+   * @return SelectQueryInterface
+   *   A count query object.
+   */
+  public function getCountQuery() {
+    if ($this->customCountQuery) {
+      return $this->customCountQuery;
+    }
+    else {
+      return $this->query->countQuery();
+    }
+  }
+
+  /**
+   * Specify the maximum number of elements per page for this query.
+   *
+   * The default if not specified is 10 items per page.
+   *
+   * @param $limit
+   *   An integer specifying the number of elements per page.  If passed a false
+   *   value (FALSE, 0, NULL), the pager is disabled.
+   */
+  public function limit($limit = 10) {
+    $this->limit = $limit;
+    return $this;
+  }
+
+  /**
+   * Specify the element ID for this pager query.
+   *
+   * The element is used to differentiate different pager queries on the same
+   * page so that they may be operated independently.  If you do not specify an
+   * element, every pager query on the page will get a unique element.  If for
+   * whatever reason you want to explicitly define an element for a given query,
+   * you may do so here.
+   *
+   * Setting the element here also increments the static $maxElement counter,
+   * which is used for determining the $element when there's none specified.
+   *
+   * Note that no collision detection is done when setting an element ID
+   * explicitly, so it is possible for two pagers to end up using the same ID
+   * if both are set explicitly.
+   *
+   * @param $element
+   */
+  public function element($element) {
+    $this->element = $element;
+    if ($element >= self::$maxElement) {
+      self::$maxElement = $element + 1;
+    }
+    return $this;
+  }
+}
+
+/**
+ * Returns the current page being requested for display within a pager.
+ *
+ * @param $element
+ *  An optional integer to distinguish between multiple pagers on one page.
+ *
+ * @return
+ *  The number of the current requested page, within the pager represented by
+ *  $element. This is determined from the URL query parameter $_GET['page'], or
+ *  0 by default. Note that this number may differ from the actual page being
+ *  displayed. For example, if a search for "example text" brings up three
+ *  pages of results, but a users visits search/node/example+text?page=10, this
+ *  function will return 10, even though the default pager implementation
+ *  adjusts for this and still displays the third page of search results at
+ *  that URL.
+ *
+ * @see pager_default_initialize()
+ */
+function pager_find_page($element = 0) {
+  $page = isset($_GET['page']) ? $_GET['page'] : '';
+  $page_array = explode(',', $page);
+  if (!isset($page_array[$element])) {
+    $page_array[$element] = 0;
+  }
+  return (int) $page_array[$element];
+}
+
+/**
+ * Initializes a pager for theme('pager').
+ *
+ * This function sets up the necessary global variables so that future calls
+ * to theme('pager') will render a pager that correctly corresponds to the
+ * items being displayed.
+ *
+ * If the items being displayed result from a database query performed using
+ * Drupal's database API, and if you have control over the construction of the
+ * database query, you do not need to call this function directly; instead, you
+ * can simply extend the query object with the 'PagerDefault' extender before
+ * executing it. For example:
+ * @code
+ *   $query = db_select('some_table')->extend('PagerDefault');
+ * @endcode
+ *
+ * However, if you are using a different method for generating the items to be
+ * paged through, then you should call this function in preparation.
+ *
+ * The following example shows how this function can be used in a page callback
+ * that invokes an external datastore with an SQL-like syntax:
+ * @code
+ *   // First find the total number of items and initialize the pager.
+ *   $where = "status = 1";
+ *   $total = mymodule_select("SELECT COUNT(*) FROM data " . $where)->result();
+ *   $num_per_page = variable_get('mymodule_num_per_page', 10);
+ *   $page = pager_default_initialize($total, $num_per_page);
+ *
+ *   // Next, retrieve and display the items for the current page.
+ *   $offset = $num_per_page * $page;
+ *   $result = mymodule_select("SELECT * FROM data " . $where . " LIMIT %d, %d", $offset, $num_per_page)->fetchAll();
+ *   $output = theme('mymodule_results', array('result' => $result));
+ *
+ *   // Finally, display the pager controls, and return.
+ *   $output .= theme('pager');
+ *   return $output;
+ * @endcode
+ *
+ * A second example involves a page callback that invokes an external search
+ * service where the total number of matching results is provided as part of
+ * the returned set (so that we do not need a separate query in order to obtain
+ * this information). Here, we call pager_find_page() to calculate the desired
+ * offset before the search is invoked:
+ * @code
+ *   // Perform the query, using the requested offset from pager_find_page().
+ *   // This comes from a URL parameter, so here we are assuming that the URL
+ *   // parameter corresponds to an actual page of results that will exist
+ *   // within the set.
+ *   $page = pager_find_page();
+ *   $num_per_page = variable_get('mymodule_num_per_page', 10);
+ *   $offset = $num_per_page * $page;
+ *   $result = mymodule_remote_search($keywords, $offset, $num_per_page);
+ *
+ *   // Now that we have the total number of results, initialize the pager.
+ *   pager_default_initialize($result->total, $num_per_page);
+ *
+ *   // Display the search results.
+ *   $output = theme('search_results', array('results' => $result->data, 'type' => 'remote'));
+ *
+ *   // Finally, display the pager controls, and return.
+ *   $output .= theme('pager');
+ *   return $output;
+ * @endcode
+ *
+ * @param $total
+ *  The total number of items to be paged.
+ * @param $limit
+ *  The number of items the calling code will display per page.
+ * @param $element
+ *  An optional integer to distinguish between multiple pagers on one page.
+ *
+ * @return
+ *   The number of the current page, within the pager represented by $element.
+ *   This is determined from the URL query parameter $_GET['page'], or 0 by
+ *   default. However, if a page that does not correspond to the actual range
+ *   of the result set was requested, this function will return the closest
+ *   page actually within the result set.
+ */
+function pager_default_initialize($total, $limit, $element = 0) {
+  global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;
+
+  $page = pager_find_page($element);
+
+  // We calculate the total of pages as ceil(items / limit).
+  $pager_total_items[$element] = $total;
+  $pager_total[$element] = ceil($pager_total_items[$element] / $limit);
+  $pager_page_array[$element] = max(0, min($page, ((int) $pager_total[$element]) - 1));
+  $pager_limits[$element] = $limit;
+  return $pager_page_array[$element];
+}
+
+/**
+ * Compose a URL query parameter array for pager links.
+ *
+ * @return
+ *   A URL query parameter array that consists of all components of the current
+ *   page request except for those pertaining to paging.
+ */
+function pager_get_query_parameters() {
+  $query = &drupal_static(__FUNCTION__);
+  if (!isset($query)) {
+    $query = drupal_get_query_parameters($_GET, array('q', 'page'));
+  }
+  return $query;
+}
+
+/**
+ * Returns HTML for a query pager.
+ *
+ * Menu callbacks that display paged query results should call theme('pager') to
+ * retrieve a pager control so that users can view other results. Format a list
+ * of nearby pages with additional query results.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - tags: An array of labels for the controls in the pager.
+ *   - element: An optional integer to distinguish between multiple pagers on
+ *     one page.
+ *   - parameters: An associative array of query string parameters to append to
+ *     the pager links.
+ *   - quantity: The number of pages in the list.
+ *
+ * @ingroup themeable
+ */
+function theme_pager($variables) {
+  $tags = $variables['tags'];
+  $element = $variables['element'];
+  $parameters = $variables['parameters'];
+  $quantity = $variables['quantity'];
+  global $pager_page_array, $pager_total;
+
+  // Calculate various markers within this pager piece:
+  // Middle is used to "center" pages around the current page.
+  $pager_middle = ceil($quantity / 2);
+  // current is the page we are currently paged to
+  $pager_current = $pager_page_array[$element] + 1;
+  // first is the first page listed by this pager piece (re quantity)
+  $pager_first = $pager_current - $pager_middle + 1;
+  // last is the last page listed by this pager piece (re quantity)
+  $pager_last = $pager_current + $quantity - $pager_middle;
+  // max is the maximum page number
+  $pager_max = $pager_total[$element];
+  // End of marker calculations.
+
+  // Prepare for generation loop.
+  $i = $pager_first;
+  if ($pager_last > $pager_max) {
+    // Adjust "center" if at end of query.
+    $i = $i + ($pager_max - $pager_last);
+    $pager_last = $pager_max;
+  }
+  if ($i <= 0) {
+    // Adjust "center" if at start of query.
+    $pager_last = $pager_last + (1 - $i);
+    $i = 1;
+  }
+  // End of generation loop preparation.
+
+  $li_first = theme('pager_first', array('text' => (isset($tags[0]) ? $tags[0] : t('« first')), 'element' => $element, 'parameters' => $parameters));
+  $li_previous = theme('pager_previous', array('text' => (isset($tags[1]) ? $tags[1] : t('‹ previous')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters));
+  $li_next = theme('pager_next', array('text' => (isset($tags[3]) ? $tags[3] : t('next ›')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters));
+  $li_last = theme('pager_last', array('text' => (isset($tags[4]) ? $tags[4] : t('last »')), 'element' => $element, 'parameters' => $parameters));
+
+  if ($pager_total[$element] > 1) {
+    if ($li_first) {
+      $items[] = array(
+        'class' => array('pager-first'),
+        'data' => $li_first,
+      );
+    }
+    if ($li_previous) {
+      $items[] = array(
+        'class' => array('pager-previous'),
+        'data' => $li_previous,
+      );
+    }
+
+    // When there is more than one page, create the pager list.
+    if ($i != $pager_max) {
+      if ($i > 1) {
+        $items[] = array(
+          'class' => array('pager-ellipsis'),
+          'data' => '…',
+        );
+      }
+      // Now generate the actual pager piece.
+      for (; $i <= $pager_last && $i <= $pager_max; $i++) {
+        if ($i < $pager_current) {
+          $items[] = array(
+            'class' => array('pager-item'),
+            'data' => theme('pager_previous', array('text' => $i, 'element' => $element, 'interval' => ($pager_current - $i), 'parameters' => $parameters)),
+          );
+        }
+        if ($i == $pager_current) {
+          $items[] = array(
+            'class' => array('pager-current'),
+            'data' => $i,
+          );
+        }
+        if ($i > $pager_current) {
+          $items[] = array(
+            'class' => array('pager-item'),
+            'data' => theme('pager_next', array('text' => $i, 'element' => $element, 'interval' => ($i - $pager_current), 'parameters' => $parameters)),
+          );
+        }
+      }
+      if ($i < $pager_max) {
+        $items[] = array(
+          'class' => array('pager-ellipsis'),
+          'data' => '…',
+        );
+      }
+    }
+    // End generation.
+    if ($li_next) {
+      $items[] = array(
+        'class' => array('pager-next'),
+        'data' => $li_next,
+      );
+    }
+    if ($li_last) {
+      $items[] = array(
+        'class' => array('pager-last'),
+        'data' => $li_last,
+      );
+    }
+    return '<h2 class="element-invisible">' . t('Pages') . '</h2>' . theme('item_list', array(
+      'items' => $items,
+      'attributes' => array('class' => array('pager')),
+    ));
+  }
+}
+
+
+/**
+ * @defgroup pagerpieces Pager pieces
+ * @{
+ * Theme functions for customizing pager elements.
+ *
+ * Note that you should NOT modify this file to customize your pager.
+ */
+
+/**
+ * Returns HTML for the "first page" link in a query pager.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - text: The name (or image) of the link.
+ *   - element: An optional integer to distinguish between multiple pagers on
+ *     one page.
+ *   - parameters: An associative array of query string parameters to append to
+ *     the pager links.
+ *
+ * @ingroup themeable
+ */
+function theme_pager_first($variables) {
+  $text = $variables['text'];
+  $element = $variables['element'];
+  $parameters = $variables['parameters'];
+  global $pager_page_array;
+  $output = '';
+
+  // If we are anywhere but the first page
+  if ($pager_page_array[$element] > 0) {
+    $output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array(0, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));
+  }
+
+  return $output;
+}
+
+/**
+ * Returns HTML for the "previous page" link in a query pager.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - text: The name (or image) of the link.
+ *   - element: An optional integer to distinguish between multiple pagers on
+ *     one page.
+ *   - interval: The number of pages to move backward when the link is clicked.
+ *   - parameters: An associative array of query string parameters to append to
+ *     the pager links.
+ *
+ * @ingroup themeable
+ */
+function theme_pager_previous($variables) {
+  $text = $variables['text'];
+  $element = $variables['element'];
+  $interval = $variables['interval'];
+  $parameters = $variables['parameters'];
+  global $pager_page_array;
+  $output = '';
+
+  // If we are anywhere but the first page
+  if ($pager_page_array[$element] > 0) {
+    $page_new = pager_load_array($pager_page_array[$element] - $interval, $element, $pager_page_array);
+
+    // If the previous page is the first page, mark the link as such.
+    if ($page_new[$element] == 0) {
+      $output = theme('pager_first', array('text' => $text, 'element' => $element, 'parameters' => $parameters));
+    }
+    // The previous page is not the first page.
+    else {
+      $output = theme('pager_link', array('text' => $text, 'page_new' => $page_new, 'element' => $element, 'parameters' => $parameters));
+    }
+  }
+
+  return $output;
+}
+
+/**
+ * Returns HTML for the "next page" link in a query pager.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - text: The name (or image) of the link.
+ *   - element: An optional integer to distinguish between multiple pagers on
+ *     one page.
+ *   - interval: The number of pages to move forward when the link is clicked.
+ *   - parameters: An associative array of query string parameters to append to
+ *     the pager links.
+ *
+ * @ingroup themeable
+ */
+function theme_pager_next($variables) {
+  $text = $variables['text'];
+  $element = $variables['element'];
+  $interval = $variables['interval'];
+  $parameters = $variables['parameters'];
+  global $pager_page_array, $pager_total;
+  $output = '';
+
+  // If we are anywhere but the last page
+  if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
+    $page_new = pager_load_array($pager_page_array[$element] + $interval, $element, $pager_page_array);
+    // If the next page is the last page, mark the link as such.
+    if ($page_new[$element] == ($pager_total[$element] - 1)) {
+      $output = theme('pager_last', array('text' => $text, 'element' => $element, 'parameters' => $parameters));
+    }
+    // The next page is not the last page.
+    else {
+      $output = theme('pager_link', array('text' => $text, 'page_new' => $page_new, 'element' => $element, 'parameters' => $parameters));
+    }
+  }
+
+  return $output;
+}
+
+/**
+ * Returns HTML for the "last page" link in query pager.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - text: The name (or image) of the link.
+ *   - element: An optional integer to distinguish between multiple pagers on
+ *     one page.
+ *   - parameters: An associative array of query string parameters to append to
+ *     the pager links.
+ *
+ * @ingroup themeable
+ */
+function theme_pager_last($variables) {
+  $text = $variables['text'];
+  $element = $variables['element'];
+  $parameters = $variables['parameters'];
+  global $pager_page_array, $pager_total;
+  $output = '';
+
+  // If we are anywhere but the last page
+  if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
+    $output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array($pager_total[$element] - 1, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));
+  }
+
+  return $output;
+}
+
+
+/**
+ * Returns HTML for a link to a specific query result page.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - text: The link text. Also used to figure out the title attribute of the
+ *     link, if it is not provided in $variables['attributes']['title']; in
+ *     this case, $variables['text'] must be one of the standard pager link
+ *     text strings that would be generated by the pager theme functions, such
+ *     as a number or t('« first').
+ *   - page_new: The first result to display on the linked page.
+ *   - element: An optional integer to distinguish between multiple pagers on
+ *     one page.
+ *   - parameters: An associative array of query string parameters to append to
+ *     the pager link.
+ *   - attributes: An associative array of HTML attributes to apply to the
+ *     pager link.
+ *
+ * @see theme_pager()
+ *
+ * @ingroup themeable
+ */
+function theme_pager_link($variables) {
+  $text = $variables['text'];
+  $page_new = $variables['page_new'];
+  $element = $variables['element'];
+  $parameters = $variables['parameters'];
+  $attributes = $variables['attributes'];
+
+  $page = isset($_GET['page']) ? $_GET['page'] : '';
+  if ($new_page = implode(',', pager_load_array($page_new[$element], $element, explode(',', $page)))) {
+    $parameters['page'] = $new_page;
+  }
+
+  $query = array();
+  if (count($parameters)) {
+    $query = drupal_get_query_parameters($parameters, array());
+  }
+  if ($query_pager = pager_get_query_parameters()) {
+    $query = array_merge($query, $query_pager);
+  }
+
+  // Set each pager link title
+  if (!isset($attributes['title'])) {
+    static $titles = NULL;
+    if (!isset($titles)) {
+      $titles = array(
+        t('« first') => t('Go to first page'),
+        t('‹ previous') => t('Go to previous page'),
+        t('next ›') => t('Go to next page'),
+        t('last »') => t('Go to last page'),
+      );
+    }
+    if (isset($titles[$text])) {
+      $attributes['title'] = $titles[$text];
+    }
+    elseif (is_numeric($text)) {
+      $attributes['title'] = t('Go to page @number', array('@number' => $text));
+    }
+  }
+
+  // @todo l() cannot be used here, since it adds an 'active' class based on the
+  //   path only (which is always the current path for pager links). Apparently,
+  //   none of the pager links is active at any time - but it should still be
+  //   possible to use l() here.
+  // @see http://drupal.org/node/1410574
+  $attributes['href'] = url($_GET['q'], array('query' => $query));
+  return '<a' . drupal_attributes($attributes) . '>' . check_plain($text) . '</a>';
+}
+
+/**
+ * @} End of "Pager pieces".
+ */
+
+/**
+ * Helper function
+ *
+ * Copies $old_array to $new_array and sets $new_array[$element] = $value
+ * Fills in $new_array[0 .. $element - 1] = 0
+ */
+function pager_load_array($value, $element, $old_array) {
+  $new_array = $old_array;
+  // Look for empty elements.
+  for ($i = 0; $i < $element; $i++) {
+    if (empty($new_array[$i])) {
+      // Load found empty element with 0.
+      $new_array[$i] = 0;
+    }
+  }
+  // Update the changed element.
+  $new_array[$element] = (int) $value;
+  return $new_array;
+}

+ 287 - 0
includes/password.inc

@@ -0,0 +1,287 @@
+<?php
+
+/**
+ * @file
+ * Secure password hashing functions for user authentication.
+ *
+ * Based on the Portable PHP password hashing framework.
+ * @see http://www.openwall.com/phpass/
+ *
+ * An alternative or custom version of this password hashing API may be
+ * used by setting the variable password_inc to the name of the PHP file
+ * containing replacement user_hash_password(), user_check_password(), and
+ * user_needs_new_hash() functions.
+ */
+
+/**
+ * The standard log2 number of iterations for password stretching. This should
+ * increase by 1 every Drupal version in order to counteract increases in the
+ * speed and power of computers available to crack the hashes.
+ */
+define('DRUPAL_HASH_COUNT', 15);
+
+/**
+ * The minimum allowed log2 number of iterations for password stretching.
+ */
+define('DRUPAL_MIN_HASH_COUNT', 7);
+
+/**
+ * The maximum allowed log2 number of iterations for password stretching.
+ */
+define('DRUPAL_MAX_HASH_COUNT', 30);
+
+/**
+ * The expected (and maximum) number of characters in a hashed password.
+ */
+define('DRUPAL_HASH_LENGTH', 55);
+
+/**
+ * Returns a string for mapping an int to the corresponding base 64 character.
+ */
+function _password_itoa64() {
+  return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+}
+
+/**
+ * Encodes bytes into printable base 64 using the *nix standard from crypt().
+ *
+ * @param $input
+ *   The string containing bytes to encode.
+ * @param $count
+ *   The number of characters (bytes) to encode.
+ *
+ * @return
+ *   Encoded string
+ */
+function _password_base64_encode($input, $count) {
+  $output = '';
+  $i = 0;
+  $itoa64 = _password_itoa64();
+  do {
+    $value = ord($input[$i++]);
+    $output .= $itoa64[$value & 0x3f];
+    if ($i < $count) {
+      $value |= ord($input[$i]) << 8;
+    }
+    $output .= $itoa64[($value >> 6) & 0x3f];
+    if ($i++ >= $count) {
+      break;
+    }
+    if ($i < $count) {
+      $value |= ord($input[$i]) << 16;
+    }
+    $output .= $itoa64[($value >> 12) & 0x3f];
+    if ($i++ >= $count) {
+      break;
+    }
+    $output .= $itoa64[($value >> 18) & 0x3f];
+  } while ($i < $count);
+
+  return $output;
+}
+
+/**
+ * Generates a random base 64-encoded salt prefixed with settings for the hash.
+ *
+ * Proper use of salts may defeat a number of attacks, including:
+ *  - The ability to try candidate passwords against multiple hashes at once.
+ *  - The ability to use pre-hashed lists of candidate passwords.
+ *  - The ability to determine whether two users have the same (or different)
+ *    password without actually having to guess one of the passwords.
+ *
+ * @param $count_log2
+ *   Integer that determines the number of iterations used in the hashing
+ *   process. A larger value is more secure, but takes more time to complete.
+ *
+ * @return
+ *   A 12 character string containing the iteration count and a random salt.
+ */
+function _password_generate_salt($count_log2) {
+  $output = '$S$';
+  // Ensure that $count_log2 is within set bounds.
+  $count_log2 = _password_enforce_log2_boundaries($count_log2);
+  // We encode the final log2 iteration count in base 64.
+  $itoa64 = _password_itoa64();
+  $output .= $itoa64[$count_log2];
+  // 6 bytes is the standard salt for a portable phpass hash.
+  $output .= _password_base64_encode(drupal_random_bytes(6), 6);
+  return $output;
+}
+
+/**
+ * Ensures that $count_log2 is within set bounds.
+ *
+ * @param $count_log2
+ *   Integer that determines the number of iterations used in the hashing
+ *   process. A larger value is more secure, but takes more time to complete.
+ *
+ * @return
+ *   Integer within set bounds that is closest to $count_log2.
+ */
+function _password_enforce_log2_boundaries($count_log2) {
+  if ($count_log2 < DRUPAL_MIN_HASH_COUNT) {
+    return DRUPAL_MIN_HASH_COUNT;
+  }
+  elseif ($count_log2 > DRUPAL_MAX_HASH_COUNT) {
+    return DRUPAL_MAX_HASH_COUNT;
+  }
+
+  return (int) $count_log2;
+}
+
+/**
+ * Hash a password using a secure stretched hash.
+ *
+ * By using a salt and repeated hashing the password is "stretched". Its
+ * security is increased because it becomes much more computationally costly
+ * for an attacker to try to break the hash by brute-force computation of the
+ * hashes of a large number of plain-text words or strings to find a match.
+ *
+ * @param $algo
+ *   The string name of a hashing algorithm usable by hash(), like 'sha256'.
+ * @param $password
+ *   The plain-text password to hash.
+ * @param $setting
+ *   An existing hash or the output of _password_generate_salt().  Must be
+ *   at least 12 characters (the settings and salt).
+ *
+ * @return
+ *   A string containing the hashed password (and salt) or FALSE on failure.
+ *   The return string will be truncated at DRUPAL_HASH_LENGTH characters max.
+ */
+function _password_crypt($algo, $password, $setting) {
+  // The first 12 characters of an existing hash are its setting string.
+  $setting = substr($setting, 0, 12);
+
+  if ($setting[0] != '$' || $setting[2] != '$') {
+    return FALSE;
+  }
+  $count_log2 = _password_get_count_log2($setting);
+  // Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT
+  if ($count_log2 < DRUPAL_MIN_HASH_COUNT || $count_log2 > DRUPAL_MAX_HASH_COUNT) {
+    return FALSE;
+  }
+  $salt = substr($setting, 4, 8);
+  // Hashes must have an 8 character salt.
+  if (strlen($salt) != 8) {
+    return FALSE;
+  }
+
+  // Convert the base 2 logarithm into an integer.
+  $count = 1 << $count_log2;
+
+  // We rely on the hash() function being available in PHP 5.2+.
+  $hash = hash($algo, $salt . $password, TRUE);
+  do {
+    $hash = hash($algo, $hash . $password, TRUE);
+  } while (--$count);
+
+  $len = strlen($hash);
+  $output =  $setting . _password_base64_encode($hash, $len);
+  // _password_base64_encode() of a 16 byte MD5 will always be 22 characters.
+  // _password_base64_encode() of a 64 byte sha512 will always be 86 characters.
+  $expected = 12 + ceil((8 * $len) / 6);
+  return (strlen($output) == $expected) ? substr($output, 0, DRUPAL_HASH_LENGTH) : FALSE;
+}
+
+/**
+ * Parse the log2 iteration count from a stored hash or setting string.
+ */
+function _password_get_count_log2($setting) {
+  $itoa64 = _password_itoa64();
+  return strpos($itoa64, $setting[3]);
+}
+
+/**
+ * Hash a password using a secure hash.
+ *
+ * @param $password
+ *   A plain-text password.
+ * @param $count_log2
+ *   Optional integer to specify the iteration count. Generally used only during
+ *   mass operations where a value less than the default is needed for speed.
+ *
+ * @return
+ *   A string containing the hashed password (and a salt), or FALSE on failure.
+ */
+function user_hash_password($password, $count_log2 = 0) {
+  if (empty($count_log2)) {
+    // Use the standard iteration count.
+    $count_log2 = variable_get('password_count_log2', DRUPAL_HASH_COUNT);
+  }
+  return _password_crypt('sha512', $password, _password_generate_salt($count_log2));
+}
+
+/**
+ * Check whether a plain text password matches a stored hashed password.
+ *
+ * Alternative implementations of this function may use other data in the
+ * $account object, for example the uid to look up the hash in a custom table
+ * or remote database.
+ *
+ * @param $password
+ *   A plain-text password
+ * @param $account
+ *   A user object with at least the fields from the {users} table.
+ *
+ * @return
+ *   TRUE or FALSE.
+ */
+function user_check_password($password, $account) {
+  if (substr($account->pass, 0, 2) == 'U$') {
+    // This may be an updated password from user_update_7000(). Such hashes
+    // have 'U' added as the first character and need an extra md5().
+    $stored_hash = substr($account->pass, 1);
+    $password = md5($password);
+  }
+  else {
+    $stored_hash = $account->pass;
+  }
+
+  $type = substr($stored_hash, 0, 3);
+  switch ($type) {
+    case '$S$':
+      // A normal Drupal 7 password using sha512.
+      $hash = _password_crypt('sha512', $password, $stored_hash);
+      break;
+    case '$H$':
+      // phpBB3 uses "$H$" for the same thing as "$P$".
+    case '$P$':
+      // A phpass password generated using md5.  This is an
+      // imported password or from an earlier Drupal version.
+      $hash = _password_crypt('md5', $password, $stored_hash);
+      break;
+    default:
+      return FALSE;
+  }
+  return ($hash && $stored_hash == $hash);
+}
+
+/**
+ * Check whether a user's hashed password needs to be replaced with a new hash.
+ *
+ * This is typically called during the login process when the plain text
+ * password is available. A new hash is needed when the desired iteration count
+ * has changed through a change in the variable password_count_log2 or
+ * DRUPAL_HASH_COUNT or if the user's password hash was generated in an update
+ * like user_update_7000().
+ *
+ * Alternative implementations of this function might use other criteria based
+ * on the fields in $account.
+ *
+ * @param $account
+ *   A user object with at least the fields from the {users} table.
+ *
+ * @return
+ *   TRUE or FALSE.
+ */
+function user_needs_new_hash($account) {
+  // Check whether this was an updated password.
+  if ((substr($account->pass, 0, 3) != '$S$') || (strlen($account->pass) != DRUPAL_HASH_LENGTH)) {
+    return TRUE;
+  }
+  // Ensure that $count_log2 is within set bounds.
+  $count_log2 = _password_enforce_log2_boundaries(variable_get('password_count_log2', DRUPAL_HASH_COUNT));
+  // Check whether the iteration count used differs from the standard number.
+  return (_password_get_count_log2($account->pass) !== $count_log2);
+}

+ 587 - 0
includes/path.inc

@@ -0,0 +1,587 @@
+<?php
+
+/**
+ * @file
+ * Functions to handle paths in Drupal, including path aliasing.
+ *
+ * These functions are not loaded for cached pages, but modules that need
+ * to use them in hook_boot() or hook exit() can make them available, by
+ * executing "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);".
+ */
+
+/**
+ * Initialize the $_GET['q'] variable to the proper normal path.
+ */
+function drupal_path_initialize() {
+  // Ensure $_GET['q'] is set before calling drupal_normal_path(), to support
+  // path caching with hook_url_inbound_alter().
+  if (empty($_GET['q'])) {
+    $_GET['q'] = variable_get('site_frontpage', 'node');
+  }
+  $_GET['q'] = drupal_get_normal_path($_GET['q']);
+}
+
+/**
+ * Given an alias, return its Drupal system URL if one exists. Given a Drupal
+ * system URL return one of its aliases if such a one exists. Otherwise,
+ * return FALSE.
+ *
+ * @param $action
+ *   One of the following values:
+ *   - wipe: delete the alias cache.
+ *   - alias: return an alias for a given Drupal system path (if one exists).
+ *   - source: return the Drupal system URL for a path alias (if one exists).
+ * @param $path
+ *   The path to investigate for corresponding aliases or system URLs.
+ * @param $path_language
+ *   Optional language code to search the path with. Defaults to the page language.
+ *   If there's no path defined for that language it will search paths without
+ *   language.
+ *
+ * @return
+ *   Either a Drupal system path, an aliased path, or FALSE if no path was
+ *   found.
+ */
+function drupal_lookup_path($action, $path = '', $path_language = NULL) {
+  global $language_url;
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
+  }
+  $cache = &$drupal_static_fast['cache'];
+
+  if (!isset($cache)) {
+    $cache = array(
+      'map' => array(),
+      'no_source' => array(),
+      'whitelist' => NULL,
+      'system_paths' => array(),
+      'no_aliases' => array(),
+      'first_call' => TRUE,
+    );
+  }
+
+  // Retrieve the path alias whitelist.
+  if (!isset($cache['whitelist'])) {
+    $cache['whitelist'] = variable_get('path_alias_whitelist', NULL);
+    if (!isset($cache['whitelist'])) {
+      $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
+    }
+  }
+
+  // If no language is explicitly specified we default to the current URL
+  // language. If we used a language different from the one conveyed by the
+  // requested URL, we might end up being unable to check if there is a path
+  // alias matching the URL path.
+  $path_language = $path_language ? $path_language : $language_url->language;
+
+  if ($action == 'wipe') {
+    $cache = array();
+    $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
+  }
+  elseif ($cache['whitelist'] && $path != '') {
+    if ($action == 'alias') {
+      // During the first call to drupal_lookup_path() per language, load the
+      // expected system paths for the page from cache.
+      if (!empty($cache['first_call'])) {
+        $cache['first_call'] = FALSE;
+
+        $cache['map'][$path_language] = array();
+        // Load system paths from cache.
+        $cid = current_path();
+        if ($cached = cache_get($cid, 'cache_path')) {
+          $cache['system_paths'] = $cached->data;
+          // Now fetch the aliases corresponding to these system paths.
+          $args = array(
+            ':system' => $cache['system_paths'],
+            ':language' => $path_language,
+            ':language_none' => LANGUAGE_NONE,
+          );
+          // Always get the language-specific alias before the language-neutral
+          // one. For example 'de' is less than 'und' so the order needs to be
+          // ASC, while 'xx-lolspeak' is more than 'und' so the order needs to
+          // be DESC. We also order by pid ASC so that fetchAllKeyed() returns
+          // the most recently created alias for each source. Subsequent queries
+          // using fetchField() must use pid DESC to have the same effect.
+          // For performance reasons, the query builder is not used here.
+          if ($path_language == LANGUAGE_NONE) {
+            // Prevent PDO from complaining about a token the query doesn't use.
+            unset($args[':language']);
+            $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language = :language_none ORDER BY pid ASC', $args);
+          }
+          elseif ($path_language < LANGUAGE_NONE) {
+            $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, :language_none) ORDER BY language ASC, pid ASC', $args);
+          }
+          else {
+            $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, :language_none) ORDER BY language DESC, pid ASC', $args);
+          }
+          $cache['map'][$path_language] = $result->fetchAllKeyed();
+          // Keep a record of paths with no alias to avoid querying twice.
+          $cache['no_aliases'][$path_language] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$path_language])));
+        }
+      }
+      // If the alias has already been loaded, return it.
+      if (isset($cache['map'][$path_language][$path])) {
+        return $cache['map'][$path_language][$path];
+      }
+      // Check the path whitelist, if the top_level part before the first /
+      // is not in the list, then there is no need to do anything further,
+      // it is not in the database.
+      elseif (!isset($cache['whitelist'][strtok($path, '/')])) {
+        return FALSE;
+      }
+      // For system paths which were not cached, query aliases individually.
+      elseif (!isset($cache['no_aliases'][$path_language][$path])) {
+        $args = array(
+          ':source' => $path,
+          ':language' => $path_language,
+          ':language_none' => LANGUAGE_NONE,
+        );
+        // See the queries above.
+        if ($path_language == LANGUAGE_NONE) {
+          unset($args[':language']);
+          $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language = :language_none ORDER BY pid DESC", $args)->fetchField();
+        }
+        elseif ($path_language > LANGUAGE_NONE) {
+          $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", $args)->fetchField();
+        }
+        else {
+          $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language ASC, pid DESC", $args)->fetchField();
+        }
+        $cache['map'][$path_language][$path] = $alias;
+        return $alias;
+      }
+    }
+    // Check $no_source for this $path in case we've already determined that there
+    // isn't a path that has this alias
+    elseif ($action == 'source' && !isset($cache['no_source'][$path_language][$path])) {
+      // Look for the value $path within the cached $map
+      $source = FALSE;
+      if (!isset($cache['map'][$path_language]) || !($source = array_search($path, $cache['map'][$path_language]))) {
+        $args = array(
+          ':alias' => $path,
+          ':language' => $path_language,
+          ':language_none' => LANGUAGE_NONE,
+        );
+        // See the queries above.
+        if ($path_language == LANGUAGE_NONE) {
+          unset($args[':language']);
+          $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language = :language_none ORDER BY pid DESC", $args);
+        }
+        elseif ($path_language > LANGUAGE_NONE) {
+          $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", $args);
+        }
+        else {
+          $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language ASC, pid DESC", $args);
+        }
+        if ($source = $result->fetchField()) {
+          $cache['map'][$path_language][$source] = $path;
+        }
+        else {
+          // We can't record anything into $map because we do not have a valid
+          // index and there is no need because we have not learned anything
+          // about any Drupal path. Thus cache to $no_source.
+          $cache['no_source'][$path_language][$path] = TRUE;
+        }
+      }
+      return $source;
+    }
+  }
+
+  return FALSE;
+}
+
+/**
+ * Cache system paths for a page.
+ *
+ * Cache an array of the system paths available on each page. We assume
+ * that aliases will be needed for the majority of these paths during
+ * subsequent requests, and load them in a single query during
+ * drupal_lookup_path().
+ */
+function drupal_cache_system_paths() {
+  // Check if the system paths for this page were loaded from cache in this
+  // request to avoid writing to cache on every request.
+  $cache = &drupal_static('drupal_lookup_path', array());
+  if (empty($cache['system_paths']) && !empty($cache['map'])) {
+    // Generate a cache ID (cid) specifically for this page.
+    $cid = current_path();
+    // The static $map array used by drupal_lookup_path() includes all
+    // system paths for the page request.
+    if ($paths = current($cache['map'])) {
+      $data = array_keys($paths);
+      $expire = REQUEST_TIME + (60 * 60 * 24);
+      cache_set($cid, $data, 'cache_path', $expire);
+    }
+  }
+}
+
+/**
+ * Given an internal Drupal path, return the alias set by the administrator.
+ *
+ * If no path is provided, the function will return the alias of the current
+ * page.
+ *
+ * @param $path
+ *   An internal Drupal path.
+ * @param $path_language
+ *   An optional language code to look up the path in.
+ *
+ * @return
+ *   An aliased path if one was found, or the original path if no alias was
+ *   found.
+ */
+function drupal_get_path_alias($path = NULL, $path_language = NULL) {
+  // If no path is specified, use the current page's path.
+  if ($path == NULL) {
+    $path = $_GET['q'];
+  }
+  $result = $path;
+  if ($alias = drupal_lookup_path('alias', $path, $path_language)) {
+    $result = $alias;
+  }
+  return $result;
+}
+
+/**
+ * Given a path alias, return the internal path it represents.
+ *
+ * @param $path
+ *   A Drupal path alias.
+ * @param $path_language
+ *   An optional language code to look up the path in.
+ *
+ * @return
+ *   The internal path represented by the alias, or the original alias if no
+ *   internal path was found.
+ */
+function drupal_get_normal_path($path, $path_language = NULL) {
+  $original_path = $path;
+
+  // Lookup the path alias first.
+  if ($source = drupal_lookup_path('source', $path, $path_language)) {
+    $path = $source;
+  }
+
+  // Allow other modules to alter the inbound URL. We cannot use drupal_alter()
+  // here because we need to run hook_url_inbound_alter() in the reverse order
+  // of hook_url_outbound_alter().
+  foreach (array_reverse(module_implements('url_inbound_alter')) as $module) {
+    $function = $module . '_url_inbound_alter';
+    $function($path, $original_path, $path_language);
+  }
+
+  return $path;
+}
+
+/**
+ * Check if the current page is the front page.
+ *
+ * @return
+ *   Boolean value: TRUE if the current page is the front page; FALSE if otherwise.
+ */
+function drupal_is_front_page() {
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['is_front_page'] = &drupal_static(__FUNCTION__);
+  }
+  $is_front_page = &$drupal_static_fast['is_front_page'];
+
+  if (!isset($is_front_page)) {
+    // As drupal_path_initialize updates $_GET['q'] with the 'site_frontpage' path,
+    // we can check it against the 'site_frontpage' variable.
+    $is_front_page = ($_GET['q'] == variable_get('site_frontpage', 'node'));
+  }
+
+  return $is_front_page;
+}
+
+/**
+ * Check if a path matches any pattern in a set of patterns.
+ *
+ * @param $path
+ *   The path to match.
+ * @param $patterns
+ *   String containing a set of patterns separated by \n, \r or \r\n.
+ *
+ * @return
+ *   Boolean value: TRUE if the path matches a pattern, FALSE otherwise.
+ */
+function drupal_match_path($path, $patterns) {
+  $regexps = &drupal_static(__FUNCTION__);
+
+  if (!isset($regexps[$patterns])) {
+    // Convert path settings to a regular expression.
+    // Therefore replace newlines with a logical or, /* with asterisks and the <front> with the frontpage.
+    $to_replace = array(
+      '/(\r\n?|\n)/', // newlines
+      '/\\\\\*/',     // asterisks
+      '/(^|\|)\\\\<front\\\\>($|\|)/' // <front>
+    );
+    $replacements = array(
+      '|',
+      '.*',
+      '\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'
+    );
+    $patterns_quoted = preg_quote($patterns, '/');
+    $regexps[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
+  }
+  return (bool)preg_match($regexps[$patterns], $path);
+}
+
+/**
+ * Return the current URL path of the page being viewed.
+ *
+ * Examples:
+ * - http://example.com/node/306 returns "node/306".
+ * - http://example.com/drupalfolder/node/306 returns "node/306" while
+ *   base_path() returns "/drupalfolder/".
+ * - http://example.com/path/alias (which is a path alias for node/306) returns
+ *   "node/306" as opposed to the path alias.
+ *
+ * This function is not available in hook_boot() so use $_GET['q'] instead.
+ * However, be careful when doing that because in the case of Example #3
+ * $_GET['q'] will contain "path/alias". If "node/306" is needed, calling
+ * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
+ *
+ * @return
+ *   The current Drupal URL path.
+ *
+ * @see request_path()
+ */
+function current_path() {
+  return $_GET['q'];
+}
+
+/**
+ * Rebuild the path alias white list.
+ *
+ * @param $source
+ *   An optional system path for which an alias is being inserted.
+ *
+ * @return
+ *   An array containing a white list of path aliases.
+ */
+function drupal_path_alias_whitelist_rebuild($source = NULL) {
+  // When paths are inserted, only rebuild the whitelist if the system path
+  // has a top level component which is not already in the whitelist.
+  if (!empty($source)) {
+    $whitelist = variable_get('path_alias_whitelist', NULL);
+    if (isset($whitelist[strtok($source, '/')])) {
+      return $whitelist;
+    }
+  }
+  // For each alias in the database, get the top level component of the system
+  // path it corresponds to. This is the portion of the path before the first
+  // '/', if present, otherwise the whole path itself.
+  $whitelist = array();
+  $result = db_query("SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}");
+  foreach ($result as $row) {
+    $whitelist[$row->path] = TRUE;
+  }
+  variable_set('path_alias_whitelist', $whitelist);
+  return $whitelist;
+}
+
+/**
+ * Fetches a specific URL alias from the database.
+ *
+ * @param $conditions
+ *   A string representing the source, a number representing the pid, or an
+ *   array of query conditions.
+ *
+ * @return
+ *   FALSE if no alias was found or an associative array containing the
+ *   following keys:
+ *   - source: The internal system path.
+ *   - alias: The URL alias.
+ *   - pid: Unique path alias identifier.
+ *   - language: The language of the alias.
+ */
+function path_load($conditions) {
+  if (is_numeric($conditions)) {
+    $conditions = array('pid' => $conditions);
+  }
+  elseif (is_string($conditions)) {
+    $conditions = array('source' => $conditions);
+  }
+  elseif (!is_array($conditions)) {
+    return FALSE;
+  }
+  $select = db_select('url_alias');
+  foreach ($conditions as $field => $value) {
+    $select->condition($field, $value);
+  }
+  return $select
+    ->fields('url_alias')
+    ->execute()
+    ->fetchAssoc();
+}
+
+/**
+ * Save a path alias to the database.
+ *
+ * @param $path
+ *   An associative array containing the following keys:
+ *   - source: The internal system path.
+ *   - alias: The URL alias.
+ *   - pid: (optional) Unique path alias identifier.
+ *   - language: (optional) The language of the alias.
+ */
+function path_save(&$path) {
+  $path += array('language' => LANGUAGE_NONE);
+
+  // Load the stored alias, if any.
+  if (!empty($path['pid']) && !isset($path['original'])) {
+    $path['original'] = path_load($path['pid']);
+  }
+
+  if (empty($path['pid'])) {
+    drupal_write_record('url_alias', $path);
+    module_invoke_all('path_insert', $path);
+  }
+  else {
+    drupal_write_record('url_alias', $path, array('pid'));
+    module_invoke_all('path_update', $path);
+  }
+
+  // Clear internal properties.
+  unset($path['original']);
+
+  // Clear the static alias cache.
+  drupal_clear_path_cache($path['source']);
+}
+
+/**
+ * Delete a URL alias.
+ *
+ * @param $criteria
+ *   A number representing the pid or an array of criteria.
+ */
+function path_delete($criteria) {
+  if (!is_array($criteria)) {
+    $criteria = array('pid' => $criteria);
+  }
+  $path = path_load($criteria);
+  $query = db_delete('url_alias');
+  foreach ($criteria as $field => $value) {
+    $query->condition($field, $value);
+  }
+  $query->execute();
+  module_invoke_all('path_delete', $path);
+  drupal_clear_path_cache($path['source']);
+}
+
+/**
+ * Determines whether a path is in the administrative section of the site.
+ *
+ * By default, paths are considered to be non-administrative. If a path does
+ * not match any of the patterns in path_get_admin_paths(), or if it matches
+ * both administrative and non-administrative patterns, it is considered
+ * non-administrative.
+ *
+ * @param $path
+ *   A Drupal path.
+ *
+ * @return
+ *   TRUE if the path is administrative, FALSE otherwise.
+ *
+ * @see path_get_admin_paths()
+ * @see hook_admin_paths()
+ * @see hook_admin_paths_alter()
+ */
+function path_is_admin($path) {
+  $path_map = &drupal_static(__FUNCTION__);
+  if (!isset($path_map['admin'][$path])) {
+    $patterns = path_get_admin_paths();
+    $path_map['admin'][$path] = drupal_match_path($path, $patterns['admin']);
+    $path_map['non_admin'][$path] = drupal_match_path($path, $patterns['non_admin']);
+  }
+  return $path_map['admin'][$path] && !$path_map['non_admin'][$path];
+}
+
+/**
+ * Gets a list of administrative and non-administrative paths.
+ *
+ * @return array
+ *   An associative array containing the following keys:
+ *   'admin': An array of administrative paths and regular expressions
+ *            in a format suitable for drupal_match_path().
+ *   'non_admin': An array of non-administrative paths and regular expressions.
+ *
+ * @see hook_admin_paths()
+ * @see hook_admin_paths_alter()
+ */
+function path_get_admin_paths() {
+  $patterns = &drupal_static(__FUNCTION__);
+  if (!isset($patterns)) {
+    $paths = module_invoke_all('admin_paths');
+    drupal_alter('admin_paths', $paths);
+    // Combine all admin paths into one array, and likewise for non-admin paths,
+    // for easier handling.
+    $patterns = array();
+    $patterns['admin'] = array();
+    $patterns['non_admin'] = array();
+    foreach ($paths as $path => $enabled) {
+      if ($enabled) {
+        $patterns['admin'][] = $path;
+      }
+      else {
+        $patterns['non_admin'][] = $path;
+      }
+    }
+    $patterns['admin'] = implode("\n", $patterns['admin']);
+    $patterns['non_admin'] = implode("\n", $patterns['non_admin']);
+  }
+  return $patterns;
+}
+
+/**
+ * Checks a path exists and the current user has access to it.
+ *
+ * @param $path
+ *   The path to check.
+ * @param $dynamic_allowed
+ *   Whether paths with menu wildcards (like user/%) should be allowed.
+ *
+ * @return
+ *   TRUE if it is a valid path AND the current user has access permission,
+ *   FALSE otherwise.
+ */
+function drupal_valid_path($path, $dynamic_allowed = FALSE) {
+  global $menu_admin;
+  // We indicate that a menu administrator is running the menu access check.
+  $menu_admin = TRUE;
+  if ($path == '<front>' || url_is_external($path)) {
+    $item = array('access' => TRUE);
+  }
+  elseif ($dynamic_allowed && preg_match('/\/\%/', $path)) {
+    // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
+    if ($item = db_query("SELECT * FROM {menu_router} where path = :path", array(':path' => $path))->fetchAssoc()) {
+      $item['link_path']  = $form_item['link_path'];
+      $item['link_title'] = $form_item['link_title'];
+      $item['external']   = FALSE;
+      $item['options'] = '';
+      _menu_link_translate($item);
+    }
+  }
+  else {
+    $item = menu_get_item($path);
+  }
+  $menu_admin = FALSE;
+  return $item && $item['access'];
+}
+
+/**
+ * Clear the path cache.
+ *
+ * @param $source
+ *   An optional system path for which an alias is being changed.
+ */
+function drupal_clear_path_cache($source = NULL) {
+  // Clear the drupal_lookup_path() static cache.
+  drupal_static_reset('drupal_lookup_path');
+  drupal_path_alias_whitelist_rebuild($source);
+}

+ 189 - 0
includes/registry.inc

@@ -0,0 +1,189 @@
+<?php
+
+/**
+ * @file
+ * This file contains the code registry parser engine.
+ */
+
+/**
+ * @defgroup registry Code registry
+ * @{
+ * The code registry engine.
+ *
+ * Drupal maintains an internal registry of all functions or classes in the
+ * system, allowing it to lazy-load code files as needed (reducing the amount
+ * of code that must be parsed on each request).
+ */
+
+/**
+ * Does the work for registry_update().
+ */
+function _registry_update() {
+
+  // The registry serves as a central autoloader for all classes, including
+  // the database query builders. However, the registry rebuild process
+  // requires write ability to the database, which means having access to the
+  // query builders that require the registry in order to be loaded. That
+  // causes a fatal race condition. Therefore we manually include the
+  // appropriate query builders for the currently active database before the
+  // registry rebuild process runs.
+  $connection_info = Database::getConnectionInfo();
+  $driver = $connection_info['default']['driver'];
+  require_once DRUPAL_ROOT . '/includes/database/query.inc';
+  require_once DRUPAL_ROOT . '/includes/database/select.inc';
+  require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/query.inc';
+
+  // Get current list of modules and their files.
+  $modules = db_query("SELECT * FROM {system} WHERE type = 'module'")->fetchAll();
+  // Get the list of files we are going to parse.
+  $files = array();
+  foreach ($modules as &$module) {
+    $module->info = unserialize($module->info);
+    $dir = dirname($module->filename);
+
+    // Store the module directory for use in hook_registry_files_alter().
+    $module->dir = $dir;
+
+    if ($module->status) {
+      // Add files for enabled modules to the registry.
+      foreach ($module->info['files'] as $file) {
+        $files["$dir/$file"] = array('module' => $module->name, 'weight' => $module->weight);
+      }
+    }
+  }
+  foreach (file_scan_directory('includes', '/\.inc$/') as $filename => $file) {
+    $files["$filename"] = array('module' => '', 'weight' => 0);
+  }
+
+  $transaction = db_transaction();
+  try {
+    // Allow modules to manually modify the list of files before the registry
+    // parses them. The $modules array provides the .info file information, which
+    // includes the list of files registered to each module. Any files in the
+    // list can then be added to the list of files that the registry will parse,
+    // or modify attributes of a file.
+    drupal_alter('registry_files', $files, $modules);
+    foreach (registry_get_parsed_files() as $filename => $file) {
+      // Add the hash for those files we have already parsed.
+      if (isset($files[$filename])) {
+        $files[$filename]['hash'] = $file['hash'];
+      }
+      else {
+        // Flush the registry of resources in files that are no longer on disc
+        // or are in files that no installed modules require to be parsed.
+        db_delete('registry')
+          ->condition('filename', $filename)
+          ->execute();
+        db_delete('registry_file')
+          ->condition('filename', $filename)
+          ->execute();
+      }
+    }
+    $parsed_files = _registry_parse_files($files);
+
+    $unchanged_resources = array();
+    $lookup_cache = array();
+    if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) {
+      $lookup_cache = $cache->data;
+    }
+    foreach ($lookup_cache as $key => $file) {
+      // If the file for this cached resource is carried over unchanged from
+      // the last registry build, then we can safely re-cache it.
+      if ($file && in_array($file, array_keys($files)) && !in_array($file, $parsed_files)) {
+        $unchanged_resources[$key] = $file;
+      }
+    }
+    module_implements('', FALSE, TRUE);
+    _registry_check_code(REGISTRY_RESET_LOOKUP_CACHE);
+  }
+  catch (Exception $e) {
+    $transaction->rollback();
+    watchdog_exception('registry', $e);
+    throw $e;
+  }
+
+  // We have some unchanged resources, warm up the cache - no need to pay
+  // for looking them up again.
+  if (count($unchanged_resources) > 0) {
+    cache_set('lookup_cache', $unchanged_resources, 'cache_bootstrap');
+  }
+}
+
+/**
+ * Return the list of files in registry_file
+ */
+function registry_get_parsed_files() {
+  $files = array();
+  // We want the result as a keyed array.
+  $files = db_query("SELECT * FROM {registry_file}")->fetchAllAssoc('filename', PDO::FETCH_ASSOC);
+  return $files;
+}
+
+/**
+ * Parse all files that have changed since the registry was last built, and save their function and class listings.
+ *
+ * @param $files
+ *  The list of files to check and parse.
+ */
+function _registry_parse_files($files) {
+  $parsed_files = array();
+  foreach ($files as $filename => $file) {
+    if (file_exists($filename)) {
+      $hash = hash_file('sha256', $filename);
+      if (empty($file['hash']) || $file['hash'] != $hash) {
+        $file['hash'] = $hash;
+        $parsed_files[$filename] = $file;
+      }
+    }
+  }
+  foreach ($parsed_files as $filename => $file) {
+    _registry_parse_file($filename, file_get_contents($filename), $file['module'], $file['weight']);
+    db_merge('registry_file')
+      ->key(array('filename' => $filename))
+      ->fields(array(
+        'hash' => $file['hash'],
+      ))
+      ->execute();
+  }
+  return array_keys($parsed_files);
+}
+
+/**
+ * Parse a file and save its function and class listings.
+ *
+ * @param $filename
+ *   Name of the file we are going to parse.
+ * @param $contents
+ *   Contents of the file we are going to parse as a string.
+ * @param $module
+ *   (optional) Name of the module this file belongs to.
+ * @param $weight
+ *   (optional) Weight of the module.
+ */
+function _registry_parse_file($filename, $contents, $module = '', $weight = 0) {
+  if (preg_match_all('/^\s*(?:abstract|final)?\s*(class|interface)\s+([a-zA-Z0-9_]+)/m', $contents, $matches)) {
+    foreach ($matches[2] as $key => $name) {
+      db_merge('registry')
+        ->key(array(
+          'name' => $name,
+          'type' => $matches[1][$key],
+        ))
+        ->fields(array(
+          'filename' => $filename,
+          'module' => $module,
+          'weight' => $weight,
+        ))
+        ->execute();
+    }
+    // Delete any resources for this file where the name is not in the list
+    // we just merged in.
+    db_delete('registry')
+      ->condition('filename', $filename)
+      ->condition('name', $matches[2], 'NOT IN')
+      ->execute();
+  }
+}
+
+/**
+ * @} End of "defgroup registry".
+ */

+ 533 - 0
includes/session.inc

@@ -0,0 +1,533 @@
+<?php
+
+/**
+ * @file
+ * User session handling functions.
+ *
+ * The user-level session storage handlers:
+ * - _drupal_session_open()
+ * - _drupal_session_close()
+ * - _drupal_session_read()
+ * - _drupal_session_write()
+ * - _drupal_session_destroy()
+ * - _drupal_session_garbage_collection()
+ * are assigned by session_set_save_handler() in bootstrap.inc and are called
+ * automatically by PHP. These functions should not be called directly. Session
+ * data should instead be accessed via the $_SESSION superglobal.
+ */
+
+/**
+ * Session handler assigned by session_set_save_handler().
+ *
+ * This function is used to handle any initialization, such as file paths or
+ * database connections, that is needed before accessing session data. Drupal
+ * does not need to initialize anything in this function.
+ *
+ * This function should not be called directly.
+ *
+ * @return
+ *   This function will always return TRUE.
+ */
+function _drupal_session_open() {
+  return TRUE;
+}
+
+/**
+ * Session handler assigned by session_set_save_handler().
+ *
+ * This function is used to close the current session. Because Drupal stores
+ * session data in the database immediately on write, this function does
+ * not need to do anything.
+ *
+ * This function should not be called directly.
+ *
+ * @return
+ *   This function will always return TRUE.
+ */
+function _drupal_session_close() {
+  return TRUE;
+}
+
+/**
+ * Reads an entire session from the database (internal use only).
+ *
+ * Also initializes the $user object for the user associated with the session.
+ * This function is registered with session_set_save_handler() to support
+ * database-backed sessions. It is called on every page load when PHP sets
+ * up the $_SESSION superglobal.
+ *
+ * This function is an internal function and must not be called directly.
+ * Doing so may result in logging out the current user, corrupting session data
+ * or other unexpected behavior. Session data must always be accessed via the
+ * $_SESSION superglobal.
+ *
+ * @param $sid
+ *   The session ID of the session to retrieve.
+ *
+ * @return
+ *   The user's session, or an empty string if no session exists.
+ */
+function _drupal_session_read($sid) {
+  global $user, $is_https;
+
+  // Write and Close handlers are called after destructing objects
+  // since PHP 5.0.5.
+  // Thus destructors can use sessions but session handler can't use objects.
+  // So we are moving session closure before destructing objects.
+  drupal_register_shutdown_function('session_write_close');
+
+  // Handle the case of first time visitors and clients that don't store
+  // cookies (eg. web crawlers).
+  $insecure_session_name = substr(session_name(), 1);
+  if (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name])) {
+    $user = drupal_anonymous_user();
+    return '';
+  }
+
+  // Otherwise, if the session is still active, we have a record of the
+  // client's session in the database. If it's HTTPS then we are either have
+  // a HTTPS session or we are about to log in so we check the sessions table
+  // for an anonymous session with the non-HTTPS-only cookie.
+  if ($is_https) {
+    $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => $sid))->fetchObject();
+    if (!$user) {
+      if (isset($_COOKIE[$insecure_session_name])) {
+        $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array(
+        ':sid' => $_COOKIE[$insecure_session_name]))
+        ->fetchObject();
+      }
+    }
+  }
+  else {
+    $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject();
+  }
+
+  // We found the client's session record and they are an authenticated,
+  // active user.
+  if ($user && $user->uid > 0 && $user->status == 1) {
+    // This is done to unserialize the data member of $user.
+    $user->data = unserialize($user->data);
+
+    // Add roles element to $user.
+    $user->roles = array();
+    $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
+    $user->roles += db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = :uid", array(':uid' => $user->uid))->fetchAllKeyed(0, 1);
+  }
+  elseif ($user) {
+    // The user is anonymous or blocked. Only preserve two fields from the
+    // {sessions} table.
+    $account = drupal_anonymous_user();
+    $account->session = $user->session;
+    $account->timestamp = $user->timestamp;
+    $user = $account;
+  }
+  else {
+    // The session has expired.
+    $user = drupal_anonymous_user();
+    $user->session = '';
+  }
+
+  // Store the session that was read for comparison in _drupal_session_write().
+  $last_read = &drupal_static('drupal_session_last_read');
+  $last_read = array(
+    'sid' => $sid,
+    'value' => $user->session,
+  );
+
+  return $user->session;
+}
+
+/**
+ * Writes an entire session to the database (internal use only).
+ *
+ * This function is registered with session_set_save_handler() to support
+ * database-backed sessions.
+ *
+ * This function is an internal function and must not be called directly.
+ * Doing so may result in corrupted session data or other unexpected behavior.
+ * Session data must always be accessed via the $_SESSION superglobal.
+ *
+ * @param $sid
+ *   The session ID of the session to write to.
+ * @param $value
+ *   Session data to write as a serialized string.
+ *
+ * @return
+ *   Always returns TRUE.
+ */
+function _drupal_session_write($sid, $value) {
+  global $user, $is_https;
+
+  // The exception handler is not active at this point, so we need to do it
+  // manually.
+  try {
+    if (!drupal_save_session()) {
+      // We don't have anything to do if we are not allowed to save the session.
+      return;
+    }
+
+    // Check whether $_SESSION has been changed in this request.
+    $last_read = &drupal_static('drupal_session_last_read');
+    $is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value;
+
+    // For performance reasons, do not update the sessions table, unless
+    // $_SESSION has changed or more than 180 has passed since the last update.
+    if ($is_changed || !isset($user->timestamp) || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) {
+      // Either ssid or sid or both will be added from $key below.
+      $fields = array(
+        'uid' => $user->uid,
+        'cache' => isset($user->cache) ? $user->cache : 0,
+        'hostname' => ip_address(),
+        'session' => $value,
+        'timestamp' => REQUEST_TIME,
+      );
+
+      // Use the session ID as 'sid' and an empty string as 'ssid' by default.
+      // _drupal_session_read() does not allow empty strings so that's a safe
+      // default.
+      $key = array('sid' => $sid, 'ssid' => '');
+      // On HTTPS connections, use the session ID as both 'sid' and 'ssid'.
+      if ($is_https) {
+        $key['ssid'] = $sid;
+        // The "secure pages" setting allows a site to simultaneously use both
+        // secure and insecure session cookies. If enabled and both cookies are
+        // presented then use both keys.
+        if (variable_get('https', FALSE)) {
+          $insecure_session_name = substr(session_name(), 1);
+          if (isset($_COOKIE[$insecure_session_name])) {
+            $key['sid'] = $_COOKIE[$insecure_session_name];
+          }
+        }
+      }
+      elseif (variable_get('https', FALSE)) {
+        unset($key['ssid']);
+      }
+
+      db_merge('sessions')
+        ->key($key)
+        ->fields($fields)
+        ->execute();
+    }
+
+    // Likewise, do not update access time more than once per 180 seconds.
+    if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) {
+      db_update('users')
+        ->fields(array(
+          'access' => REQUEST_TIME
+        ))
+        ->condition('uid', $user->uid)
+        ->execute();
+    }
+
+    return TRUE;
+  }
+  catch (Exception $exception) {
+    require_once DRUPAL_ROOT . '/includes/errors.inc';
+    // If we are displaying errors, then do so with no possibility of a further
+    // uncaught exception being thrown.
+    if (error_displayable()) {
+      print '<h1>Uncaught exception thrown in session handler.</h1>';
+      print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
+    }
+    return FALSE;
+  }
+}
+
+/**
+ * Initializes the session handler, starting a session if needed.
+ */
+function drupal_session_initialize() {
+  global $user, $is_https;
+
+  session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
+
+  // We use !empty() in the following check to ensure that blank session IDs
+  // are not valid.
+  if (!empty($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) {
+    // If a session cookie exists, initialize the session. Otherwise the
+    // session is only started on demand in drupal_session_commit(), making
+    // anonymous users not use a session cookie unless something is stored in
+    // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
+    drupal_session_start();
+    if (!empty($user->uid) || !empty($_SESSION)) {
+      drupal_page_is_cacheable(FALSE);
+    }
+  }
+  else {
+    // Set a session identifier for this request. This is necessary because
+    // we lazily start sessions at the end of this request, and some
+    // processes (like drupal_get_token()) needs to know the future
+    // session ID in advance.
+    $GLOBALS['lazy_session'] = TRUE;
+    $user = drupal_anonymous_user();
+    // Less random sessions (which are much faster to generate) are used for
+    // anonymous users than are generated in drupal_session_regenerate() when
+    // a user becomes authenticated.
+    session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE)));
+    if ($is_https && variable_get('https', FALSE)) {
+      $insecure_session_name = substr(session_name(), 1);
+      $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE));
+      $_COOKIE[$insecure_session_name] = $session_id;
+    }
+  }
+  date_default_timezone_set(drupal_get_user_timezone());
+}
+
+/**
+ * Starts a session forcefully, preserving already set session data.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_session_start() {
+  // Command line clients do not support cookies nor sessions.
+  if (!drupal_session_started() && !drupal_is_cli()) {
+    // Save current session data before starting it, as PHP will destroy it.
+    $session_data = isset($_SESSION) ? $_SESSION : NULL;
+
+    session_start();
+    drupal_session_started(TRUE);
+
+    // Restore session data.
+    if (!empty($session_data)) {
+      $_SESSION += $session_data;
+    }
+  }
+}
+
+/**
+ * Commits the current session, if necessary.
+ *
+ * If an anonymous user already have an empty session, destroy it.
+ */
+function drupal_session_commit() {
+  global $user, $is_https;
+
+  if (!drupal_save_session()) {
+    // We don't have anything to do if we are not allowed to save the session.
+    return;
+  }
+
+  if (empty($user->uid) && empty($_SESSION)) {
+    // There is no session data to store, destroy the session if it was
+    // previously started.
+    if (drupal_session_started()) {
+      session_destroy();
+    }
+  }
+  else {
+    // There is session data to store. Start the session if it is not already
+    // started.
+    if (!drupal_session_started()) {
+      drupal_session_start();
+      if ($is_https && variable_get('https', FALSE)) {
+        $insecure_session_name = substr(session_name(), 1);
+        $params = session_get_cookie_params();
+        $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+        setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
+      }
+    }
+    // Write the session data.
+    session_write_close();
+  }
+}
+
+/**
+ * Returns whether a session has been started.
+ */
+function drupal_session_started($set = NULL) {
+  static $session_started = FALSE;
+  if (isset($set)) {
+    $session_started = $set;
+  }
+  return $session_started && session_id();
+}
+
+/**
+ * Called when an anonymous user becomes authenticated or vice-versa.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_session_regenerate() {
+  global $user, $is_https;
+  // Nothing to do if we are not allowed to change the session.
+  if (!drupal_save_session()) {
+    return;
+  }
+
+  if ($is_https && variable_get('https', FALSE)) {
+    $insecure_session_name = substr(session_name(), 1);
+    if (!isset($GLOBALS['lazy_session']) && isset($_COOKIE[$insecure_session_name])) {
+      $old_insecure_session_id = $_COOKIE[$insecure_session_name];
+    }
+    $params = session_get_cookie_params();
+    $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55));
+    // If a session cookie lifetime is set, the session will expire
+    // $params['lifetime'] seconds from the current request. If it is not set,
+    // it will expire when the browser is closed.
+    $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+    setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
+    $_COOKIE[$insecure_session_name] = $session_id;
+  }
+
+  if (drupal_session_started()) {
+    $old_session_id = session_id();
+  }
+  session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)));
+
+  if (isset($old_session_id)) {
+    $params = session_get_cookie_params();
+    $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+    setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
+    $fields = array('sid' => session_id());
+    if ($is_https) {
+      $fields['ssid'] = session_id();
+      // If the "secure pages" setting is enabled, use the newly-created
+      // insecure session identifier as the regenerated sid.
+      if (variable_get('https', FALSE)) {
+        $fields['sid'] = $session_id;
+      }
+    }
+    db_update('sessions')
+      ->fields($fields)
+      ->condition($is_https ? 'ssid' : 'sid', $old_session_id)
+      ->execute();
+  }
+  elseif (isset($old_insecure_session_id)) {
+    // If logging in to the secure site, and there was no active session on the
+    // secure site but a session was active on the insecure site, update the
+    // insecure session with the new session identifiers.
+    db_update('sessions')
+      ->fields(array('sid' => $session_id, 'ssid' => session_id()))
+      ->condition('sid', $old_insecure_session_id)
+      ->execute();
+  }
+  else {
+    // Start the session when it doesn't exist yet.
+    // Preserve the logged in user, as it will be reset to anonymous
+    // by _drupal_session_read.
+    $account = $user;
+    drupal_session_start();
+    $user = $account;
+  }
+  date_default_timezone_set(drupal_get_user_timezone());
+}
+
+/**
+ * Session handler assigned by session_set_save_handler().
+ *
+ * Cleans up a specific session.
+ *
+ * @param $sid
+ *   Session ID.
+ */
+function _drupal_session_destroy($sid) {
+  global $user, $is_https;
+
+  // Nothing to do if we are not allowed to change the session.
+  if (!drupal_save_session()) {
+    return;
+  }
+
+  // Delete session data.
+  db_delete('sessions')
+    ->condition($is_https ? 'ssid' : 'sid', $sid)
+    ->execute();
+
+  // Reset $_SESSION and $user to prevent a new session from being started
+  // in drupal_session_commit().
+  $_SESSION = array();
+  $user = drupal_anonymous_user();
+
+  // Unset the session cookies.
+  _drupal_session_delete_cookie(session_name());
+  if ($is_https) {
+    _drupal_session_delete_cookie(substr(session_name(), 1), FALSE);
+  }
+  elseif (variable_get('https', FALSE)) {
+    _drupal_session_delete_cookie('S' . session_name(), TRUE);
+  }
+}
+
+/**
+ * Deletes the session cookie.
+ *
+ * @param $name
+ *   Name of session cookie to delete.
+ * @param boolean $secure
+ *   Force the secure value of the cookie.
+ */
+function _drupal_session_delete_cookie($name, $secure = NULL) {
+  global $is_https;
+  if (isset($_COOKIE[$name]) || (!$is_https && $secure === TRUE)) {
+    $params = session_get_cookie_params();
+    if ($secure !== NULL) {
+      $params['secure'] = $secure;
+    }
+    setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
+    unset($_COOKIE[$name]);
+  }
+}
+
+/**
+ * Ends a specific user's session(s).
+ *
+ * @param $uid
+ *   User ID.
+ */
+function drupal_session_destroy_uid($uid) {
+  // Nothing to do if we are not allowed to change the session.
+  if (!drupal_save_session()) {
+    return;
+  }
+
+  db_delete('sessions')
+    ->condition('uid', $uid)
+    ->execute();
+}
+
+/**
+ * Session handler assigned by session_set_save_handler().
+ *
+ * Cleans up stalled sessions.
+ *
+ * @param $lifetime
+ *   The value of session.gc_maxlifetime, passed by PHP.
+ *   Sessions not updated for more than $lifetime seconds will be removed.
+ */
+function _drupal_session_garbage_collection($lifetime) {
+  // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
+  // value. For example, if you want user sessions to stay in your database
+  // for three weeks before deleting them, you need to set gc_maxlifetime
+  // to '1814400'. At that value, only after a user doesn't log in after
+  // three weeks (1814400 seconds) will his/her session be removed.
+  db_delete('sessions')
+    ->condition('timestamp', REQUEST_TIME - $lifetime, '<')
+    ->execute();
+  return TRUE;
+}
+
+/**
+ * Determines whether to save session data of the current request.
+ *
+ * This function allows the caller to temporarily disable writing of
+ * session data, should the request end while performing potentially
+ * dangerous operations, such as manipulating the global $user object.
+ * See http://drupal.org/node/218104 for usage.
+ *
+ * @param $status
+ *   Disables writing of session data when FALSE, (re-)enables
+ *   writing when TRUE.
+ *
+ * @return
+ *   FALSE if writing session data has been disabled. Otherwise, TRUE.
+ */
+function drupal_save_session($status = NULL) {
+  // PHP session ID, session, and cookie handling happens in the global scope.
+  // This value has to persist across calls to drupal_static_reset(), since a
+  // potentially wrong or disallowed session would be written otherwise.
+  static $save_session = TRUE;
+  if (isset($status)) {
+    $save_session = $status;
+  }
+  return $save_session;
+}

+ 836 - 0
includes/stream_wrappers.inc

@@ -0,0 +1,836 @@
+<?php
+
+/**
+ * @file
+ * Drupal stream wrapper interface.
+ *
+ * Provides a Drupal interface and classes to implement PHP stream wrappers for
+ * public, private, and temporary files.
+ *
+ * A stream wrapper is an abstraction of a file system that allows Drupal to
+ * use the same set of methods to access both local files and remote resources.
+ *
+ * Note that PHP 5.2 fopen() only supports URIs of the form "scheme://target"
+ * despite the fact that according to RFC 3986 a URI's scheme component
+ * delimiter is in general just ":", not "://".  Because of this PHP limitation
+ * and for consistency Drupal will only accept URIs of form "scheme://target".
+ *
+ * @see http://www.faqs.org/rfcs/rfc3986.html
+ * @see http://bugs.php.net/bug.php?id=47070
+ */
+
+/**
+ * Stream wrapper bit flags that are the basis for composite types.
+ *
+ * Note that 0x0002 is skipped, because it was the value of a constant that has
+ * since been removed.
+ */
+
+/**
+ * Stream wrapper bit flag -- a filter that matches all wrappers.
+ */
+define('STREAM_WRAPPERS_ALL', 0x0000);
+
+/**
+ * Stream wrapper bit flag -- refers to a local file system location.
+ */
+define('STREAM_WRAPPERS_LOCAL', 0x0001);
+
+/**
+ * Stream wrapper bit flag -- wrapper is readable (almost always true).
+ */
+define('STREAM_WRAPPERS_READ', 0x0004);
+
+/**
+ * Stream wrapper bit flag -- wrapper is writeable.
+ */
+define('STREAM_WRAPPERS_WRITE', 0x0008);
+
+/**
+ * Stream wrapper bit flag -- exposed in the UI and potentially web accessible.
+ */
+define('STREAM_WRAPPERS_VISIBLE', 0x0010);
+
+/**
+ * Composite stream wrapper bit flags that are usually used as the types.
+ */
+
+/**
+ * Stream wrapper type flag -- not visible in the UI or accessible via web,
+ * but readable and writable. E.g. the temporary directory for uploads.
+ */
+define('STREAM_WRAPPERS_HIDDEN', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE);
+
+/**
+ * Stream wrapper type flag -- hidden, readable and writeable using local files.
+ */
+define('STREAM_WRAPPERS_LOCAL_HIDDEN', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_HIDDEN);
+
+/**
+ * Stream wrapper type flag -- visible, readable and writeable.
+ */
+define('STREAM_WRAPPERS_WRITE_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE | STREAM_WRAPPERS_VISIBLE);
+
+/**
+ * Stream wrapper type flag -- visible and read-only.
+ */
+define('STREAM_WRAPPERS_READ_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_VISIBLE);
+
+/**
+ * Stream wrapper type flag -- the default when 'type' is omitted from
+ * hook_stream_wrappers(). This does not include STREAM_WRAPPERS_LOCAL,
+ * because PHP grants a greater trust level to local files (for example, they
+ * can be used in an "include" statement, regardless of the "allow_url_include"
+ * setting), so stream wrappers need to explicitly opt-in to this.
+ */
+define('STREAM_WRAPPERS_NORMAL', STREAM_WRAPPERS_WRITE_VISIBLE);
+
+/**
+ * Stream wrapper type flag -- visible, readable and writeable using local files.
+ */
+define('STREAM_WRAPPERS_LOCAL_NORMAL', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_NORMAL);
+
+/**
+ * Generic PHP stream wrapper interface.
+ *
+ * @see http://www.php.net/manual/en/class.streamwrapper.php
+ */
+interface StreamWrapperInterface {
+  public function stream_open($uri, $mode, $options, &$opened_url);
+  public function stream_close();
+  public function stream_lock($operation);
+  public function stream_read($count);
+  public function stream_write($data);
+  public function stream_eof();
+  public function stream_seek($offset, $whence);
+  public function stream_flush();
+  public function stream_tell();
+  public function stream_stat();
+  public function unlink($uri);
+  public function rename($from_uri, $to_uri);
+  public function mkdir($uri, $mode, $options);
+  public function rmdir($uri, $options);
+  public function url_stat($uri, $flags);
+  public function dir_opendir($uri, $options);
+  public function dir_readdir();
+  public function dir_rewinddir();
+  public function dir_closedir();
+}
+
+/**
+ * Drupal stream wrapper extension.
+ *
+ * Extend the StreamWrapperInterface with methods expected by Drupal stream
+ * wrapper classes.
+ */
+interface DrupalStreamWrapperInterface extends StreamWrapperInterface {
+  /**
+   * Set the absolute stream resource URI.
+   *
+   * This allows you to set the URI. Generally is only called by the factory
+   * method.
+   *
+   * @param $uri
+   *   A string containing the URI that should be used for this instance.
+   */
+  function setUri($uri);
+
+  /**
+   * Returns the stream resource URI.
+   *
+   * @return
+   *   Returns the current URI of the instance.
+   */
+  public function getUri();
+
+  /**
+   * Returns a web accessible URL for the resource.
+   *
+   * This function should return a URL that can be embedded in a web page
+   * and accessed from a browser. For example, the external URL of
+   * "youtube://xIpLd0WQKCY" might be
+   * "http://www.youtube.com/watch?v=xIpLd0WQKCY".
+   *
+   * @return
+   *   Returns a string containing a web accessible URL for the resource.
+   */
+  public function getExternalUrl();
+
+  /**
+   * Returns the MIME type of the resource.
+   *
+   * @param $uri
+   *   The URI, path, or filename.
+   * @param $mapping
+   *   An optional map of extensions to their mimetypes, in the form:
+   *    - 'mimetypes': a list of mimetypes, keyed by an identifier,
+   *    - 'extensions': the mapping itself, an associative array in which
+   *      the key is the extension and the value is the mimetype identifier.
+   *
+   * @return
+   *   Returns a string containing the MIME type of the resource.
+   */
+  public static function getMimeType($uri, $mapping = NULL);
+
+  /**
+   * Changes permissions of the resource.
+   *
+   * PHP lacks this functionality and it is not part of the official stream
+   * wrapper interface. This is a custom implementation for Drupal.
+   *
+   * @param $mode
+   *   Integer value for the permissions. Consult PHP chmod() documentation
+   *   for more information.
+   *
+   * @return
+   *   Returns TRUE on success or FALSE on failure.
+   */
+  public function chmod($mode);
+
+  /**
+   * Returns canonical, absolute path of the resource.
+   *
+   * Implementation placeholder. PHP's realpath() does not support stream
+   * wrappers. We provide this as a default so that individual wrappers may
+   * implement their own solutions.
+   *
+   * @return
+   *   Returns a string with absolute pathname on success (implemented
+   *   by core wrappers), or FALSE on failure or if the registered
+   *   wrapper does not provide an implementation.
+   */
+  public function realpath();
+
+  /**
+   * Gets the name of the directory from a given path.
+   *
+   * This method is usually accessed through drupal_dirname(), which wraps
+   * around the normal PHP dirname() function, which does not support stream
+   * wrappers.
+   *
+   * @param $uri
+   *   An optional URI.
+   *
+   * @return
+   *   A string containing the directory name, or FALSE if not applicable.
+   *
+   * @see drupal_dirname()
+   */
+  public function dirname($uri = NULL);
+}
+
+
+/**
+ * Drupal stream wrapper base class for local files.
+ *
+ * This class provides a complete stream wrapper implementation. URIs such as
+ * "public://example.txt" are expanded to a normal filesystem path such as
+ * "sites/default/files/example.txt" and then PHP filesystem functions are
+ * invoked.
+ *
+ * DrupalLocalStreamWrapper implementations need to implement at least the
+ * getDirectoryPath() and getExternalUrl() methods.
+ */
+abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface {
+  /**
+   * Stream context resource.
+   *
+   * @var Resource
+   */
+  public $context;
+
+  /**
+   * A generic resource handle.
+   *
+   * @var Resource
+   */
+  public $handle = NULL;
+
+  /**
+   * Instance URI (stream).
+   *
+   * A stream is referenced as "scheme://target".
+   *
+   * @var String
+   */
+  protected $uri;
+
+  /**
+   * Gets the path that the wrapper is responsible for.
+   * @TODO: Review this method name in D8 per http://drupal.org/node/701358
+   *
+   * @return
+   *   String specifying the path.
+   */
+  abstract function getDirectoryPath();
+
+  /**
+   * Base implementation of setUri().
+   */
+  function setUri($uri) {
+    $this->uri = $uri;
+  }
+
+  /**
+   * Base implementation of getUri().
+   */
+  function getUri() {
+    return $this->uri;
+  }
+
+  /**
+   * Returns the local writable target of the resource within the stream.
+   *
+   * This function should be used in place of calls to realpath() or similar
+   * functions when attempting to determine the location of a file. While
+   * functions like realpath() may return the location of a read-only file, this
+   * method may return a URI or path suitable for writing that is completely
+   * separate from the URI used for reading.
+   *
+   * @param $uri
+   *   Optional URI.
+   *
+   * @return
+   *   Returns a string representing a location suitable for writing of a file,
+   *   or FALSE if unable to write to the file such as with read-only streams.
+   */
+  protected function getTarget($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+
+    list($scheme, $target) = explode('://', $uri, 2);
+
+    // Remove erroneous leading or trailing, forward-slashes and backslashes.
+    return trim($target, '\/');
+  }
+
+  /**
+   * Base implementation of getMimeType().
+   */
+  static function getMimeType($uri, $mapping = NULL) {
+    if (!isset($mapping)) {
+      // The default file map, defined in file.mimetypes.inc is quite big.
+      // We only load it when necessary.
+      include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc';
+      $mapping = file_mimetype_mapping();
+    }
+
+    $extension = '';
+    $file_parts = explode('.', drupal_basename($uri));
+
+    // Remove the first part: a full filename should not match an extension.
+    array_shift($file_parts);
+
+    // Iterate over the file parts, trying to find a match.
+    // For my.awesome.image.jpeg, we try:
+    //   - jpeg
+    //   - image.jpeg, and
+    //   - awesome.image.jpeg
+    while ($additional_part = array_pop($file_parts)) {
+      $extension = strtolower($additional_part . ($extension ? '.' . $extension : ''));
+      if (isset($mapping['extensions'][$extension])) {
+        return $mapping['mimetypes'][$mapping['extensions'][$extension]];
+      }
+    }
+
+    return 'application/octet-stream';
+  }
+
+  /**
+   * Base implementation of chmod().
+   */
+  function chmod($mode) {
+    $output = @chmod($this->getLocalPath(), $mode);
+    // We are modifying the underlying file here, so we have to clear the stat
+    // cache so that PHP understands that URI has changed too.
+    clearstatcache();
+    return $output;
+  }
+
+  /**
+   * Base implementation of realpath().
+   */
+  function realpath() {
+    return $this->getLocalPath();
+  }
+
+  /**
+   * Returns the canonical absolute path of the URI, if possible.
+   *
+   * @param string $uri
+   *   (optional) The stream wrapper URI to be converted to a canonical
+   *   absolute path. This may point to a directory or another type of file.
+   *
+   * @return string|false
+   *   If $uri is not set, returns the canonical absolute path of the URI
+   *   previously set by the DrupalStreamWrapperInterface::setUri() function.
+   *   If $uri is set and valid for this class, returns its canonical absolute
+   *   path, as determined by the realpath() function. If $uri is set but not
+   *   valid, returns FALSE.
+   */
+  protected function getLocalPath($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+    $path = $this->getDirectoryPath() . '/' . $this->getTarget($uri);
+    $realpath = realpath($path);
+    if (!$realpath) {
+      // This file does not yet exist.
+      $realpath = realpath(dirname($path)) . '/' . drupal_basename($path);
+    }
+    $directory = realpath($this->getDirectoryPath());
+    if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) {
+      return FALSE;
+    }
+    return $realpath;
+  }
+
+  /**
+   * Support for fopen(), file_get_contents(), file_put_contents() etc.
+   *
+   * @param $uri
+   *   A string containing the URI to the file to open.
+   * @param $mode
+   *   The file mode ("r", "wb" etc.).
+   * @param $options
+   *   A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
+   * @param $opened_path
+   *   A string containing the path actually opened.
+   *
+   * @return
+   *   Returns TRUE if file was opened successfully.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-open.php
+   */
+  public function stream_open($uri, $mode, $options, &$opened_path) {
+    $this->uri = $uri;
+    $path = $this->getLocalPath();
+    $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode);
+
+    if ((bool) $this->handle && $options & STREAM_USE_PATH) {
+      $opened_path = $path;
+    }
+
+    return (bool) $this->handle;
+  }
+
+  /**
+   * Support for flock().
+   *
+   * @param $operation
+   *   One of the following:
+   *   - LOCK_SH to acquire a shared lock (reader).
+   *   - LOCK_EX to acquire an exclusive lock (writer).
+   *   - LOCK_UN to release a lock (shared or exclusive).
+   *   - LOCK_NB if you don't want flock() to block while locking (not
+   *     supported on Windows).
+   *
+   * @return
+   *   Always returns TRUE at the present time.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-lock.php
+   */
+  public function stream_lock($operation) {
+    if (in_array($operation, array(LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB))) {
+      return flock($this->handle, $operation);
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Support for fread(), file_get_contents() etc.
+   *
+   * @param $count
+   *   Maximum number of bytes to be read.
+   *
+   * @return
+   *   The string that was read, or FALSE in case of an error.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-read.php
+   */
+  public function stream_read($count) {
+    return fread($this->handle, $count);
+  }
+
+  /**
+   * Support for fwrite(), file_put_contents() etc.
+   *
+   * @param $data
+   *   The string to be written.
+   *
+   * @return
+   *   The number of bytes written (integer).
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-write.php
+   */
+  public function stream_write($data) {
+    return fwrite($this->handle, $data);
+  }
+
+  /**
+   * Support for feof().
+   *
+   * @return
+   *   TRUE if end-of-file has been reached.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-eof.php
+   */
+  public function stream_eof() {
+    return feof($this->handle);
+  }
+
+  /**
+   * Support for fseek().
+   *
+   * @param $offset
+   *   The byte offset to got to.
+   * @param $whence
+   *   SEEK_SET, SEEK_CUR, or SEEK_END.
+   *
+   * @return
+   *   TRUE on success.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-seek.php
+   */
+  public function stream_seek($offset, $whence) {
+    // fseek returns 0 on success and -1 on a failure.
+    // stream_seek   1 on success and  0 on a failure.
+    return !fseek($this->handle, $offset, $whence);
+  }
+
+  /**
+   * Support for fflush().
+   *
+   * @return
+   *   TRUE if data was successfully stored (or there was no data to store).
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-flush.php
+   */
+  public function stream_flush() {
+    return fflush($this->handle);
+  }
+
+  /**
+   * Support for ftell().
+   *
+   * @return
+   *   The current offset in bytes from the beginning of file.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-tell.php
+   */
+  public function stream_tell() {
+    return ftell($this->handle);
+  }
+
+  /**
+   * Support for fstat().
+   *
+   * @return
+   *   An array with file status, or FALSE in case of an error - see fstat()
+   *   for a description of this array.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-stat.php
+   */
+  public function stream_stat() {
+    return fstat($this->handle);
+  }
+
+  /**
+   * Support for fclose().
+   *
+   * @return
+   *   TRUE if stream was successfully closed.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-close.php
+   */
+  public function stream_close() {
+    return fclose($this->handle);
+  }
+
+  /**
+   * Support for unlink().
+   *
+   * @param $uri
+   *   A string containing the URI to the resource to delete.
+   *
+   * @return
+   *   TRUE if resource was successfully deleted.
+   *
+   * @see http://php.net/manual/en/streamwrapper.unlink.php
+   */
+  public function unlink($uri) {
+    $this->uri = $uri;
+    return drupal_unlink($this->getLocalPath());
+  }
+
+  /**
+   * Support for rename().
+   *
+   * @param $from_uri,
+   *   The URI to the file to rename.
+   * @param $to_uri
+   *   The new URI for file.
+   *
+   * @return
+   *   TRUE if file was successfully renamed.
+   *
+   * @see http://php.net/manual/en/streamwrapper.rename.php
+   */
+  public function rename($from_uri, $to_uri) {
+    return rename($this->getLocalPath($from_uri), $this->getLocalPath($to_uri));
+  }
+
+  /**
+   * Gets the name of the directory from a given path.
+   *
+   * This method is usually accessed through drupal_dirname(), which wraps
+   * around the PHP dirname() function because it does not support stream
+   * wrappers.
+   *
+   * @param $uri
+   *   A URI or path.
+   *
+   * @return
+   *   A string containing the directory name.
+   *
+   * @see drupal_dirname()
+   */
+  public function dirname($uri = NULL) {
+    list($scheme, $target) = explode('://', $uri, 2);
+    $target  = $this->getTarget($uri);
+    $dirname = dirname($target);
+
+    if ($dirname == '.') {
+      $dirname = '';
+    }
+
+    return $scheme . '://' . $dirname;
+  }
+
+  /**
+   * Support for mkdir().
+   *
+   * @param $uri
+   *   A string containing the URI to the directory to create.
+   * @param $mode
+   *   Permission flags - see mkdir().
+   * @param $options
+   *   A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
+   *
+   * @return
+   *   TRUE if directory was successfully created.
+   *
+   * @see http://php.net/manual/en/streamwrapper.mkdir.php
+   */
+  public function mkdir($uri, $mode, $options) {
+    $this->uri = $uri;
+    $recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE);
+    if ($recursive) {
+      // $this->getLocalPath() fails if $uri has multiple levels of directories
+      // that do not yet exist.
+      $localpath = $this->getDirectoryPath() . '/' . $this->getTarget($uri);
+    }
+    else {
+      $localpath = $this->getLocalPath($uri);
+    }
+    if ($options & STREAM_REPORT_ERRORS) {
+      return mkdir($localpath, $mode, $recursive);
+    }
+    else {
+      return @mkdir($localpath, $mode, $recursive);
+    }
+  }
+
+  /**
+   * Support for rmdir().
+   *
+   * @param $uri
+   *   A string containing the URI to the directory to delete.
+   * @param $options
+   *   A bit mask of STREAM_REPORT_ERRORS.
+   *
+   * @return
+   *   TRUE if directory was successfully removed.
+   *
+   * @see http://php.net/manual/en/streamwrapper.rmdir.php
+   */
+  public function rmdir($uri, $options) {
+    $this->uri = $uri;
+    if ($options & STREAM_REPORT_ERRORS) {
+      return drupal_rmdir($this->getLocalPath());
+    }
+    else {
+      return @drupal_rmdir($this->getLocalPath());
+    }
+  }
+
+  /**
+   * Support for stat().
+   *
+   * @param $uri
+   *   A string containing the URI to get information about.
+   * @param $flags
+   *   A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
+   *
+   * @return
+   *   An array with file status, or FALSE in case of an error - see fstat()
+   *   for a description of this array.
+   *
+   * @see http://php.net/manual/en/streamwrapper.url-stat.php
+   */
+  public function url_stat($uri, $flags) {
+    $this->uri = $uri;
+    $path = $this->getLocalPath();
+    // Suppress warnings if requested or if the file or directory does not
+    // exist. This is consistent with PHP's plain filesystem stream wrapper.
+    if ($flags & STREAM_URL_STAT_QUIET || !file_exists($path)) {
+      return @stat($path);
+    }
+    else {
+      return stat($path);
+    }
+  }
+
+  /**
+   * Support for opendir().
+   *
+   * @param $uri
+   *   A string containing the URI to the directory to open.
+   * @param $options
+   *   Unknown (parameter is not documented in PHP Manual).
+   *
+   * @return
+   *   TRUE on success.
+   *
+   * @see http://php.net/manual/en/streamwrapper.dir-opendir.php
+   */
+  public function dir_opendir($uri, $options) {
+    $this->uri = $uri;
+    $this->handle = opendir($this->getLocalPath());
+
+    return (bool) $this->handle;
+  }
+
+  /**
+   * Support for readdir().
+   *
+   * @return
+   *   The next filename, or FALSE if there are no more files in the directory.
+   *
+   * @see http://php.net/manual/en/streamwrapper.dir-readdir.php
+   */
+  public function dir_readdir() {
+    return readdir($this->handle);
+  }
+
+  /**
+   * Support for rewinddir().
+   *
+   * @return
+   *   TRUE on success.
+   *
+   * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php
+   */
+  public function dir_rewinddir() {
+    rewinddir($this->handle);
+    // We do not really have a way to signal a failure as rewinddir() does not
+    // have a return value and there is no way to read a directory handler
+    // without advancing to the next file.
+    return TRUE;
+  }
+
+  /**
+   * Support for closedir().
+   *
+   * @return
+   *   TRUE on success.
+   *
+   * @see http://php.net/manual/en/streamwrapper.dir-closedir.php
+   */
+  public function dir_closedir() {
+    closedir($this->handle);
+    // We do not really have a way to signal a failure as closedir() does not
+    // have a return value.
+    return TRUE;
+  }
+}
+
+/**
+ * Drupal public (public://) stream wrapper class.
+ *
+ * Provides support for storing publicly accessible files with the Drupal file
+ * interface.
+ */
+class DrupalPublicStreamWrapper extends DrupalLocalStreamWrapper {
+  /**
+   * Implements abstract public function getDirectoryPath()
+   */
+  public function getDirectoryPath() {
+    return variable_get('file_public_path', conf_path() . '/files');
+  }
+
+  /**
+   * Overrides getExternalUrl().
+   *
+   * Return the HTML URI of a public file.
+   */
+  function getExternalUrl() {
+    $path = str_replace('\\', '/', $this->getTarget());
+    return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . drupal_encode_path($path);
+  }
+}
+
+
+/**
+ * Drupal private (private://) stream wrapper class.
+ *
+ * Provides support for storing privately accessible files with the Drupal file
+ * interface.
+ *
+ * Extends DrupalPublicStreamWrapper.
+ */
+class DrupalPrivateStreamWrapper extends DrupalLocalStreamWrapper {
+  /**
+   * Implements abstract public function getDirectoryPath()
+   */
+  public function getDirectoryPath() {
+    return variable_get('file_private_path', '');
+  }
+
+  /**
+   * Overrides getExternalUrl().
+   *
+   * Return the HTML URI of a private file.
+   */
+  function getExternalUrl() {
+    $path = str_replace('\\', '/', $this->getTarget());
+    return url('system/files/' . $path, array('absolute' => TRUE));
+  }
+}
+
+/**
+ * Drupal temporary (temporary://) stream wrapper class.
+ *
+ * Provides support for storing temporarily accessible files with the Drupal
+ * file interface.
+ *
+ * Extends DrupalPublicStreamWrapper.
+ */
+class DrupalTemporaryStreamWrapper extends DrupalLocalStreamWrapper {
+  /**
+   * Implements abstract public function getDirectoryPath()
+   */
+  public function getDirectoryPath() {
+    return variable_get('file_temporary_path', file_directory_temp());
+  }
+
+  /**
+   * Overrides getExternalUrl().
+   */
+  public function getExternalUrl() {
+    $path = str_replace('\\', '/', $this->getTarget());
+    return url('system/temporary/' . $path, array('absolute' => TRUE));
+  }
+}

+ 256 - 0
includes/tablesort.inc

@@ -0,0 +1,256 @@
+<?php
+
+/**
+ * @file
+ * Functions to aid in the creation of sortable tables.
+ *
+ * All tables created with a call to theme('table') have the option of having
+ * column headers that the user can click on to sort the table by that column.
+ */
+
+
+/**
+ * Query extender class for tablesort queries.
+ */
+class TableSort extends SelectQueryExtender {
+
+  /**
+   * The array of fields that can be sorted by.
+   *
+   * @var array
+   */
+  protected $header = array();
+
+  public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
+    parent::__construct($query, $connection);
+
+    // Add convenience tag to mark that this is an extended query. We have to
+    // do this in the constructor to ensure that it is set before preExecute()
+    // gets called.
+    $this->addTag('tablesort');
+  }
+
+  /**
+   * Order the query based on a header array.
+   *
+   * @see theme_table()
+   * @param $header
+   *   Table header array.
+   * @return SelectQueryInterface
+   *   The called object.
+   */
+  public function orderByHeader(Array $header) {
+    $this->header = $header;
+    $ts = $this->init();
+    if (!empty($ts['sql'])) {
+      // Based on code from db_escape_table(), but this can also contain a dot.
+      $field = preg_replace('/[^A-Za-z0-9_.]+/', '', $ts['sql']);
+
+      // Sort order can only be ASC or DESC.
+      $sort = drupal_strtoupper($ts['sort']);
+      $sort = in_array($sort, array('ASC', 'DESC')) ? $sort : '';
+      $this->orderBy($field, $sort);
+    }
+    return $this;
+  }
+
+  /**
+   * Initializes the table sort context.
+   */
+  protected function init() {
+    $ts = $this->order();
+    $ts['sort'] = $this->getSort();
+    $ts['query'] = $this->getQueryParameters();
+    return $ts;
+  }
+
+  /**
+   * Determine the current sort direction.
+   *
+   * @param $headers
+   *   An array of column headers in the format described in theme_table().
+   * @return
+   *   The current sort direction ("asc" or "desc").
+   */
+  protected function getSort() {
+    return tablesort_get_sort($this->header);
+  }
+
+  /**
+   * Compose a URL query parameter array to append to table sorting requests.
+   *
+   * @return
+   *   A URL query parameter array that consists of all components of the current
+   *   page request except for those pertaining to table sorting.
+   *
+   * @see tablesort_get_query_parameters()
+   */
+  protected function getQueryParameters() {
+    return tablesort_get_query_parameters();
+  }
+
+  /**
+   * Determine the current sort criterion.
+   *
+   * @param $headers
+   *   An array of column headers in the format described in theme_table().
+   * @return
+   *   An associative array describing the criterion, containing the keys:
+   *   - "name": The localized title of the table column.
+   *   - "sql": The name of the database field to sort on.
+   */
+  protected function order() {
+    return tablesort_get_order($this->header);
+  }
+}
+
+/**
+ * Initialize the table sort context.
+ */
+function tablesort_init($header) {
+  $ts = tablesort_get_order($header);
+  $ts['sort'] = tablesort_get_sort($header);
+  $ts['query'] = tablesort_get_query_parameters();
+  return $ts;
+}
+
+/**
+ * Formats a column header.
+ *
+ * If the cell in question is the column header for the current sort criterion,
+ * it gets special formatting. All possible sort criteria become links.
+ *
+ * @param $cell
+ *   The cell to format.
+ * @param $header
+ *   An array of column headers in the format described in theme_table().
+ * @param $ts
+ *   The current table sort context as returned from tablesort_init().
+ *
+ * @return
+ *   A properly formatted cell, ready for _theme_table_cell().
+ */
+function tablesort_header($cell, $header, $ts) {
+  // Special formatting for the currently sorted column header.
+  if (is_array($cell) && isset($cell['field'])) {
+    $title = t('sort by @s', array('@s' => $cell['data']));
+    if ($cell['data'] == $ts['name']) {
+      $ts['sort'] = (($ts['sort'] == 'asc') ? 'desc' : 'asc');
+      $cell['class'][] = 'active';
+      $image = theme('tablesort_indicator', array('style' => $ts['sort']));
+    }
+    else {
+      // If the user clicks a different header, we want to sort ascending initially.
+      $ts['sort'] = 'asc';
+      $image = '';
+    }
+    $cell['data'] = l($cell['data'] . $image, $_GET['q'], array('attributes' => array('title' => $title), 'query' => array_merge($ts['query'], array('sort' => $ts['sort'], 'order' => $cell['data'])), 'html' => TRUE));
+
+    unset($cell['field'], $cell['sort']);
+  }
+  return $cell;
+}
+
+/**
+ * Formats a table cell.
+ *
+ * Adds a class attribute to all cells in the currently active column.
+ *
+ * @param $cell
+ *   The cell to format.
+ * @param $header
+ *   An array of column headers in the format described in theme_table().
+ * @param $ts
+ *   The current table sort context as returned from tablesort_init().
+ * @param $i
+ *   The index of the cell's table column.
+ *
+ * @return
+ *   A properly formatted cell, ready for _theme_table_cell().
+ */
+function tablesort_cell($cell, $header, $ts, $i) {
+  if (isset($header[$i]['data']) && $header[$i]['data'] == $ts['name'] && !empty($header[$i]['field'])) {
+    if (is_array($cell)) {
+      $cell['class'][] = 'active';
+    }
+    else {
+      $cell = array('data' => $cell, 'class' => array('active'));
+    }
+  }
+  return $cell;
+}
+
+/**
+ * Composes a URL query parameter array for table sorting links.
+ *
+ * @return
+ *   A URL query parameter array that consists of all components of the current
+ *   page request except for those pertaining to table sorting.
+ */
+function tablesort_get_query_parameters() {
+  return drupal_get_query_parameters($_GET, array('q', 'sort', 'order'));
+}
+
+/**
+ * Determines the current sort criterion.
+ *
+ * @param $headers
+ *   An array of column headers in the format described in theme_table().
+ *
+ * @return
+ *   An associative array describing the criterion, containing the keys:
+ *   - "name": The localized title of the table column.
+ *   - "sql": The name of the database field to sort on.
+ */
+function tablesort_get_order($headers) {
+  $order = isset($_GET['order']) ? $_GET['order'] : '';
+  foreach ($headers as $header) {
+    if (is_array($header)) {
+      if (isset($header['data']) && $order == $header['data']) {
+        $default = $header;
+        break;
+      }
+
+      if (empty($default) && isset($header['sort']) && ($header['sort'] == 'asc' || $header['sort'] == 'desc')) {
+        $default = $header;
+      }
+    }
+  }
+
+  if (!isset($default)) {
+    $default = reset($headers);
+    if (!is_array($default)) {
+      $default = array('data' => $default);
+    }
+  }
+
+  $default += array('data' => NULL, 'field' => NULL);
+  return array('name' => $default['data'], 'sql' => $default['field']);
+}
+
+/**
+ * Determines the current sort direction.
+ *
+ * @param $headers
+ *   An array of column headers in the format described in theme_table().
+ *
+ * @return
+ *   The current sort direction ("asc" or "desc").
+ */
+function tablesort_get_sort($headers) {
+  if (isset($_GET['sort'])) {
+    return (strtolower($_GET['sort']) == 'desc') ? 'desc' : 'asc';
+  }
+  // The user has not specified a sort. Use the default for the currently sorted
+  // header if specified; otherwise use "asc".
+  else {
+    // Find out which header is currently being sorted.
+    $ts = tablesort_get_order($headers);
+    foreach ($headers as $header) {
+      if (is_array($header) && isset($header['data']) && $header['data'] == $ts['name'] && isset($header['sort'])) {
+        return $header['sort'];
+      }
+    }
+  }
+  return 'asc';
+}

+ 2905 - 0
includes/theme.inc

@@ -0,0 +1,2905 @@
+<?php
+
+/**
+ * @file
+ * The theme system, which controls the output of Drupal.
+ *
+ * The theme system allows for nearly all output of the Drupal system to be
+ * customized by user themes.
+ */
+
+/**
+ * @defgroup content_flags Content markers
+ * @{
+ * Markers used by theme_mark() and node_mark() to designate content.
+ * @see theme_mark(), node_mark()
+ */
+
+/**
+ * Mark content as read.
+ */
+define('MARK_READ', 0);
+
+/**
+ * Mark content as being new.
+ */
+define('MARK_NEW', 1);
+
+/**
+ * Mark content as being updated.
+ */
+define('MARK_UPDATED', 2);
+
+/**
+ * @} End of "Content markers".
+ */
+
+/**
+ * Determines if a theme is available to use.
+ *
+ * @param $theme
+ *   Either the name of a theme or a full theme object.
+ *
+ * @return
+ *   Boolean TRUE if the theme is enabled or is the site administration theme;
+ *   FALSE otherwise.
+ */
+function drupal_theme_access($theme) {
+  if (is_object($theme)) {
+    return _drupal_theme_access($theme);
+  }
+  else {
+    $themes = list_themes();
+    return isset($themes[$theme]) && _drupal_theme_access($themes[$theme]);
+  }
+}
+
+/**
+ * Helper function for determining access to a theme.
+ *
+ * @see drupal_theme_access()
+ */
+function _drupal_theme_access($theme) {
+  $admin_theme = variable_get('admin_theme');
+  return !empty($theme->status) || ($admin_theme && $theme->name == $admin_theme);
+}
+
+/**
+ * Initializes the theme system by loading the theme.
+ */
+function drupal_theme_initialize() {
+  global $theme, $user, $theme_key;
+
+  // If $theme is already set, assume the others are set, too, and do nothing
+  if (isset($theme)) {
+    return;
+  }
+
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
+  $themes = list_themes();
+
+  // Only select the user selected theme if it is available in the
+  // list of themes that can be accessed.
+  $theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : variable_get('theme_default', 'bartik');
+
+  // Allow modules to override the theme. Validation has already been performed
+  // inside menu_get_custom_theme(), so we do not need to check it again here.
+  $custom_theme = menu_get_custom_theme();
+  $theme = !empty($custom_theme) ? $custom_theme : $theme;
+
+  // Store the identifier for retrieving theme settings with.
+  $theme_key = $theme;
+
+  // Find all our ancestor themes and put them in an array.
+  $base_theme = array();
+  $ancestor = $theme;
+  while ($ancestor && isset($themes[$ancestor]->base_theme)) {
+    $ancestor = $themes[$ancestor]->base_theme;
+    $base_theme[] = $themes[$ancestor];
+  }
+  _drupal_theme_initialize($themes[$theme], array_reverse($base_theme));
+
+  // Themes can have alter functions, so reset the drupal_alter() cache.
+  drupal_static_reset('drupal_alter');
+
+  // Provide the page with information about the theme that's used, so that a
+  // later Ajax request can be rendered using the same theme.
+  // @see ajax_base_page_theme()
+  $setting['ajaxPageState'] = array(
+    'theme' => $theme_key,
+    'theme_token' => drupal_get_token($theme_key),
+  );
+  drupal_add_js($setting, 'setting');
+}
+
+/**
+ * Initializes the theme system given already loaded information.
+ *
+ * This function is useful to initialize a theme when no database is present.
+ *
+ * @param $theme
+ *   An object with the following information:
+ *     filename
+ *       The .info file for this theme. The 'path' to
+ *       the theme will be in this file's directory. (Required)
+ *     owner
+ *       The path to the .theme file or the .engine file to load for
+ *       the theme. (Required)
+ *     stylesheet
+ *       The primary stylesheet for the theme. (Optional)
+ *     engine
+ *       The name of theme engine to use. (Optional)
+ * @param $base_theme
+ *    An optional array of objects that represent the 'base theme' if the
+ *    theme is meant to be derivative of another theme. It requires
+ *    the same information as the $theme object. It should be in
+ *    'oldest first' order, meaning the top level of the chain will
+ *    be first.
+ * @param $registry_callback
+ *   The callback to invoke to set the theme registry.
+ */
+function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') {
+  global $theme_info, $base_theme_info, $theme_engine, $theme_path;
+  $theme_info = $theme;
+  $base_theme_info = $base_theme;
+
+  $theme_path = dirname($theme->filename);
+
+  // Prepare stylesheets from this theme as well as all ancestor themes.
+  // We work it this way so that we can have child themes override parent
+  // theme stylesheets easily.
+  $final_stylesheets = array();
+
+  // Grab stylesheets from base theme
+  foreach ($base_theme as $base) {
+    if (!empty($base->stylesheets)) {
+      foreach ($base->stylesheets as $media => $stylesheets) {
+        foreach ($stylesheets as $name => $stylesheet) {
+          $final_stylesheets[$media][$name] = $stylesheet;
+        }
+      }
+    }
+  }
+
+  // Add stylesheets used by this theme.
+  if (!empty($theme->stylesheets)) {
+    foreach ($theme->stylesheets as $media => $stylesheets) {
+      foreach ($stylesheets as $name => $stylesheet) {
+        $final_stylesheets[$media][$name] = $stylesheet;
+      }
+    }
+  }
+
+  // And now add the stylesheets properly
+  foreach ($final_stylesheets as $media => $stylesheets) {
+    foreach ($stylesheets as $stylesheet) {
+      drupal_add_css($stylesheet, array('group' => CSS_THEME, 'every_page' => TRUE, 'media' => $media));
+    }
+  }
+
+  // Do basically the same as the above for scripts
+  $final_scripts = array();
+
+  // Grab scripts from base theme
+  foreach ($base_theme as $base) {
+    if (!empty($base->scripts)) {
+      foreach ($base->scripts as $name => $script) {
+        $final_scripts[$name] = $script;
+      }
+    }
+  }
+
+  // Add scripts used by this theme.
+  if (!empty($theme->scripts)) {
+    foreach ($theme->scripts as $name => $script) {
+      $final_scripts[$name] = $script;
+    }
+  }
+
+  // Add scripts used by this theme.
+  foreach ($final_scripts as $script) {
+    drupal_add_js($script, array('group' => JS_THEME, 'every_page' => TRUE));
+  }
+
+  $theme_engine = NULL;
+
+  // Initialize the theme.
+  if (isset($theme->engine)) {
+    // Include the engine.
+    include_once DRUPAL_ROOT . '/' . $theme->owner;
+
+    $theme_engine = $theme->engine;
+    if (function_exists($theme_engine . '_init')) {
+      foreach ($base_theme as $base) {
+        call_user_func($theme_engine . '_init', $base);
+      }
+      call_user_func($theme_engine . '_init', $theme);
+    }
+  }
+  else {
+    // include non-engine theme files
+    foreach ($base_theme as $base) {
+      // Include the theme file or the engine.
+      if (!empty($base->owner)) {
+        include_once DRUPAL_ROOT . '/' . $base->owner;
+      }
+    }
+    // and our theme gets one too.
+    if (!empty($theme->owner)) {
+      include_once DRUPAL_ROOT . '/' . $theme->owner;
+    }
+  }
+
+  if (isset($registry_callback)) {
+    _theme_registry_callback($registry_callback, array($theme, $base_theme, $theme_engine));
+  }
+}
+
+/**
+ * Gets the theme registry.
+ *
+ * @param $complete
+ *   Optional boolean to indicate whether to return the complete theme registry
+ *   array or an instance of the ThemeRegistry class. If TRUE, the complete
+ *   theme registry array will be returned. This is useful if you want to
+ *   foreach over the whole registry, use array_* functions or inspect it in a
+ *   debugger. If FALSE, an instance of the ThemeRegistry class will be
+ *   returned, this provides an ArrayObject which allows it to be accessed
+ *   with array syntax and  isset(), and should be more lightweight
+ *   than the full registry. Defaults to TRUE.
+ *
+ * @return
+ *   The complete theme registry array, or an instance of the ThemeRegistry
+ *   class.
+ */
+function theme_get_registry($complete = TRUE) {
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['registry'] = &drupal_static('theme_get_registry');
+  }
+  $theme_registry = &$drupal_static_fast['registry'];
+
+  // Initialize the theme, if this is called early in the bootstrap, or after
+  // static variables have been reset.
+  if (!is_array($theme_registry)) {
+    drupal_theme_initialize();
+    $theme_registry = array();
+  }
+
+  $key = (int) $complete;
+
+  if (!isset($theme_registry[$key])) {
+    list($callback, $arguments) = _theme_registry_callback();
+    if (!$complete) {
+      $arguments[] = FALSE;
+    }
+    $theme_registry[$key] = call_user_func_array($callback, $arguments);
+  }
+
+  return $theme_registry[$key];
+}
+
+/**
+ * Sets the callback that will be used by theme_get_registry().
+ *
+ * @param $callback
+ *   The name of the callback function.
+ * @param $arguments
+ *   The arguments to pass to the function.
+ */
+function _theme_registry_callback($callback = NULL, array $arguments = array()) {
+  static $stored;
+  if (isset($callback)) {
+    $stored = array($callback, $arguments);
+  }
+  return $stored;
+}
+
+/**
+ * Gets the theme_registry cache; if it doesn't exist, builds it.
+ *
+ * @param $theme
+ *   The loaded $theme object as returned by list_themes().
+ * @param $base_theme
+ *   An array of loaded $theme objects representing the ancestor themes in
+ *   oldest first order.
+ * @param $theme_engine
+ *   The name of the theme engine.
+ * @param $complete
+ *   Whether to load the complete theme registry or an instance of the
+ *   ThemeRegistry class.
+ *
+ * @return
+ *   The theme registry array, or an instance of the ThemeRegistry class.
+ */
+function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL, $complete = TRUE) {
+  if ($complete) {
+    // Check the theme registry cache; if it exists, use it.
+    $cached = cache_get("theme_registry:$theme->name");
+    if (isset($cached->data)) {
+      $registry = $cached->data;
+    }
+    else {
+      // If not, build one and cache it.
+      $registry = _theme_build_registry($theme, $base_theme, $theme_engine);
+      // Only persist this registry if all modules are loaded. This assures a
+      // complete set of theme hooks.
+      if (module_load_all(NULL)) {
+        _theme_save_registry($theme, $registry);
+      }
+    }
+    return $registry;
+  }
+  else {
+    return new ThemeRegistry('theme_registry:runtime:' . $theme->name, 'cache');
+  }
+}
+
+/**
+ * Writes the theme_registry cache into the database.
+ */
+function _theme_save_registry($theme, $registry) {
+  cache_set("theme_registry:$theme->name", $registry);
+}
+
+/**
+ * Forces the system to rebuild the theme registry.
+ *
+ * This function should be called when modules are added to the system, or when
+ * a dynamic system needs to add more theme hooks.
+ */
+function drupal_theme_rebuild() {
+  drupal_static_reset('theme_get_registry');
+  cache_clear_all('theme_registry', 'cache', TRUE);
+}
+
+/**
+ * Builds the run-time theme registry.
+ *
+ * Extends DrupalCacheArray to allow the theme registry to be accessed as a
+ * complete registry, while internally caching only the parts of the registry
+ * that are actually in use on the site. On cache misses the complete
+ * theme registry is loaded and used to update the run-time cache.
+ */
+class ThemeRegistry Extends DrupalCacheArray {
+
+  /**
+   * Whether the partial registry can be persisted to the cache.
+   *
+   * This is only allowed if all modules and the request method is GET. theme()
+   * should be very rarely called on POST requests and this avoids polluting
+   * the runtime cache.
+   */
+  protected $persistable;
+
+  /**
+   * The complete theme registry array.
+   */
+  protected $completeRegistry;
+
+  function __construct($cid, $bin) {
+    $this->cid = $cid;
+    $this->bin = $bin;
+    $this->persistable = module_load_all(NULL) && $_SERVER['REQUEST_METHOD'] == 'GET';
+
+    $data = array();
+    if ($this->persistable && $cached = cache_get($this->cid, $this->bin)) {
+      $data = $cached->data;
+    }
+    else {
+      // If there is no runtime cache stored, fetch the full theme registry,
+      // but then initialize each value to NULL. This allows offsetExists()
+      // to function correctly on non-registered theme hooks without triggering
+      // a call to resolveCacheMiss().
+      $data = $this->initializeRegistry();
+      if ($this->persistable) {
+        $this->set($data);
+      }
+    }
+    $this->storage = $data;
+  }
+
+  /**
+   * Initializes the full theme registry.
+   *
+   * @return
+   *   An array with the keys of the full theme registry, but the values
+   *   initialized to NULL.
+   */
+  function initializeRegistry() {
+    $this->completeRegistry = theme_get_registry();
+
+    return array_fill_keys(array_keys($this->completeRegistry), NULL);
+  }
+
+  public function offsetExists($offset) {
+    // Since the theme registry allows for theme hooks to be requested that
+    // are not registered, just check the existence of the key in the registry.
+    // Use array_key_exists() here since a NULL value indicates that the theme
+    // hook exists but has not yet been requested.
+    return array_key_exists($offset, $this->storage);
+  }
+
+  public function offsetGet($offset) {
+    // If the offset is set but empty, it is a registered theme hook that has
+    // not yet been requested. Offsets that do not exist at all were not
+    // registered in hook_theme().
+    if (isset($this->storage[$offset])) {
+      return $this->storage[$offset];
+    }
+    elseif (array_key_exists($offset, $this->storage)) {
+      return $this->resolveCacheMiss($offset);
+    }
+  }
+
+  public function resolveCacheMiss($offset) {
+    if (!isset($this->completeRegistry)) {
+      $this->completeRegistry = theme_get_registry();
+    }
+    $this->storage[$offset] = $this->completeRegistry[$offset];
+    if ($this->persistable) {
+      $this->persist($offset);
+    }
+    return $this->storage[$offset];
+  }
+
+  public function set($data, $lock = TRUE) {
+    $lock_name = $this->cid . ':' . $this->bin;
+    if (!$lock || lock_acquire($lock_name)) {
+      if ($cached = cache_get($this->cid, $this->bin)) {
+        // Use array merge instead of union so that filled in values in $data
+        // overwrite empty values in the current cache.
+        $data = array_merge($cached->data, $data);
+      }
+      else {
+        $registry = $this->initializeRegistry();
+        $data = array_merge($registry, $data);
+      }
+      cache_set($this->cid, $data, $this->bin);
+      if ($lock) {
+        lock_release($lock_name);
+      }
+    }
+  }
+}
+
+/**
+ * Process a single implementation of hook_theme().
+ *
+ * @param $cache
+ *   The theme registry that will eventually be cached; It is an associative
+ *   array keyed by theme hooks, whose values are associative arrays describing
+ *   the hook:
+ *   - 'type': The passed-in $type.
+ *   - 'theme path': The passed-in $path.
+ *   - 'function': The name of the function generating output for this theme
+ *     hook. Either defined explicitly in hook_theme() or, if neither 'function'
+ *     nor 'template' is defined, then the default theme function name is used.
+ *     The default theme function name is the theme hook prefixed by either
+ *     'theme_' for modules or '$name_' for everything else. If 'function' is
+ *     defined, 'template' is not used.
+ *   - 'template': The filename of the template generating output for this
+ *     theme hook. The template is in the directory defined by the 'path' key of
+ *     hook_theme() or defaults to $path.
+ *   - 'variables': The variables for this theme hook as defined in
+ *     hook_theme(). If there is more than one implementation and 'variables' is
+ *     not specified in a later one, then the previous definition is kept.
+ *   - 'render element': The renderable element for this theme hook as defined
+ *     in hook_theme(). If there is more than one implementation and
+ *     'render element' is not specified in a later one, then the previous
+ *     definition is kept.
+ *   - 'preprocess functions': See theme() for detailed documentation.
+ *   - 'process functions': See theme() for detailed documentation.
+ * @param $name
+ *   The name of the module, theme engine, base theme engine, theme or base
+ *   theme implementing hook_theme().
+ * @param $type
+ *   One of 'module', 'theme_engine', 'base_theme_engine', 'theme', or
+ *   'base_theme'. Unlike regular hooks that can only be implemented by modules,
+ *   each of these can implement hook_theme(). _theme_process_registry() is
+ *   called in aforementioned order and new entries override older ones. For
+ *   example, if a theme hook is both defined by a module and a theme, then the
+ *   definition in the theme will be used.
+ * @param $theme
+ *   The loaded $theme object as returned from list_themes().
+ * @param $path
+ *   The directory where $name is. For example, modules/system or
+ *   themes/bartik.
+ *
+ * @see theme()
+ * @see _theme_process_registry()
+ * @see hook_theme()
+ * @see list_themes()
+ */
+function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
+  $result = array();
+
+  // Processor functions work in two distinct phases with the process
+  // functions always being executed after the preprocess functions.
+  $variable_process_phases = array(
+    'preprocess functions' => 'preprocess',
+    'process functions'    => 'process',
+  );
+
+  $hook_defaults = array(
+    'variables' => TRUE,
+    'render element' => TRUE,
+    'pattern' => TRUE,
+    'base hook' => TRUE,
+  );
+
+  // Invoke the hook_theme() implementation, process what is returned, and
+  // merge it into $cache.
+  $function = $name . '_theme';
+  if (function_exists($function)) {
+    $result = $function($cache, $type, $theme, $path);
+    foreach ($result as $hook => $info) {
+      // When a theme or engine overrides a module's theme function
+      // $result[$hook] will only contain key/value pairs for information being
+      // overridden.  Pull the rest of the information from what was defined by
+      // an earlier hook.
+
+      // Fill in the type and path of the module, theme, or engine that
+      // implements this theme function.
+      $result[$hook]['type'] = $type;
+      $result[$hook]['theme path'] = $path;
+
+      // If function and file are omitted, default to standard naming
+      // conventions.
+      if (!isset($info['template']) && !isset($info['function'])) {
+        $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook;
+      }
+
+      if (isset($cache[$hook]['includes'])) {
+        $result[$hook]['includes'] = $cache[$hook]['includes'];
+      }
+
+      // If the theme implementation defines a file, then also use the path
+      // that it defined. Otherwise use the default path. This allows
+      // system.module to declare theme functions on behalf of core .include
+      // files.
+      if (isset($info['file'])) {
+        $include_file = isset($info['path']) ? $info['path'] : $path;
+        $include_file .= '/' . $info['file'];
+        include_once DRUPAL_ROOT . '/' . $include_file;
+        $result[$hook]['includes'][] = $include_file;
+      }
+
+      // If the default keys are not set, use the default values registered
+      // by the module.
+      if (isset($cache[$hook])) {
+        $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults);
+      }
+
+      // The following apply only to theming hooks implemented as templates.
+      if (isset($info['template'])) {
+        // Prepend the current theming path when none is set.
+        if (!isset($info['path'])) {
+          $result[$hook]['template'] = $path . '/' . $info['template'];
+        }
+      }
+
+      // Allow variable processors for all theming hooks, whether the hook is
+      // implemented as a template or as a function.
+      foreach ($variable_process_phases as $phase_key => $phase) {
+        // Check for existing variable processors. Ensure arrayness.
+        if (!isset($info[$phase_key]) || !is_array($info[$phase_key])) {
+          $info[$phase_key] = array();
+          $prefixes = array();
+          if ($type == 'module') {
+            // Default variable processor prefix.
+            $prefixes[] = 'template';
+            // Add all modules so they can intervene with their own variable
+            // processors. This allows them to provide variable processors even
+            // if they are not the owner of the current hook.
+            $prefixes += module_list();
+          }
+          elseif ($type == 'theme_engine' || $type == 'base_theme_engine') {
+            // Theme engines get an extra set that come before the normally
+            // named variable processors.
+            $prefixes[] = $name . '_engine';
+            // The theme engine registers on behalf of the theme using the
+            // theme's name.
+            $prefixes[] = $theme;
+          }
+          else {
+            // This applies when the theme manually registers their own variable
+            // processors.
+            $prefixes[] = $name;
+          }
+          foreach ($prefixes as $prefix) {
+            // Only use non-hook-specific variable processors for theming hooks
+            // implemented as templates. See theme().
+            if (isset($info['template']) && function_exists($prefix . '_' . $phase)) {
+              $info[$phase_key][] = $prefix . '_' . $phase;
+            }
+            if (function_exists($prefix . '_' . $phase . '_' . $hook)) {
+              $info[$phase_key][] = $prefix . '_' . $phase . '_' . $hook;
+            }
+          }
+        }
+        // Check for the override flag and prevent the cached variable
+        // processors from being used. This allows themes or theme engines to
+        // remove variable processors set earlier in the registry build.
+        if (!empty($info['override ' . $phase_key])) {
+          // Flag not needed inside the registry.
+          unset($result[$hook]['override ' . $phase_key]);
+        }
+        elseif (isset($cache[$hook][$phase_key]) && is_array($cache[$hook][$phase_key])) {
+          $info[$phase_key] = array_merge($cache[$hook][$phase_key], $info[$phase_key]);
+        }
+        $result[$hook][$phase_key] = $info[$phase_key];
+      }
+    }
+
+    // Merge the newly created theme hooks into the existing cache.
+    $cache = $result + $cache;
+  }
+
+  // Let themes have variable processors even if they didn't register a
+  // template.
+  if ($type == 'theme' || $type == 'base_theme') {
+    foreach ($cache as $hook => $info) {
+      // Check only if not registered by the theme or engine.
+      if (empty($result[$hook])) {
+        foreach ($variable_process_phases as $phase_key => $phase) {
+          if (!isset($info[$phase_key])) {
+            $cache[$hook][$phase_key] = array();
+          }
+          // Only use non-hook-specific variable processors for theming hooks
+          // implemented as templates. See theme().
+          if (isset($info['template']) && function_exists($name . '_' . $phase)) {
+            $cache[$hook][$phase_key][] = $name . '_' . $phase;
+          }
+          if (function_exists($name . '_' . $phase . '_' . $hook)) {
+            $cache[$hook][$phase_key][] = $name . '_' . $phase . '_' . $hook;
+            $cache[$hook]['theme path'] = $path;
+          }
+          // Ensure uniqueness.
+          $cache[$hook][$phase_key] = array_unique($cache[$hook][$phase_key]);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Builds the theme registry cache.
+ *
+ * @param $theme
+ *   The loaded $theme object as returned by list_themes().
+ * @param $base_theme
+ *   An array of loaded $theme objects representing the ancestor themes in
+ *   oldest first order.
+ * @param $theme_engine
+ *   The name of the theme engine.
+ */
+function _theme_build_registry($theme, $base_theme, $theme_engine) {
+  $cache = array();
+  // First, process the theme hooks advertised by modules. This will
+  // serve as the basic registry. Since the list of enabled modules is the same
+  // regardless of the theme used, this is cached in its own entry to save
+  // building it for every theme.
+  if ($cached = cache_get('theme_registry:build:modules')) {
+    $cache = $cached->data;
+  }
+  else {
+    foreach (module_implements('theme') as $module) {
+      _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
+    }
+    // Only cache this registry if all modules are loaded.
+    if (module_load_all(NULL)) {
+      cache_set('theme_registry:build:modules', $cache);
+    }
+  }
+
+  // Process each base theme.
+  foreach ($base_theme as $base) {
+    // If the base theme uses a theme engine, process its hooks.
+    $base_path = dirname($base->filename);
+    if ($theme_engine) {
+      _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);
+    }
+    _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);
+  }
+
+  // And then the same thing, but for the theme.
+  if ($theme_engine) {
+    _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));
+  }
+
+  // Finally, hooks provided by the theme itself.
+  _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));
+
+  // Let modules alter the registry.
+  drupal_alter('theme_registry', $cache);
+
+  // Optimize the registry to not have empty arrays for functions.
+  foreach ($cache as $hook => $info) {
+    foreach (array('preprocess functions', 'process functions') as $phase) {
+      if (empty($info[$phase])) {
+        unset($cache[$hook][$phase]);
+      }
+    }
+  }
+  return $cache;
+}
+
+/**
+ * Returns a list of all currently available themes.
+ *
+ * Retrieved from the database, if available and the site is not in maintenance
+ * mode; otherwise compiled freshly from the filesystem.
+ *
+ * @param $refresh
+ *   Whether to reload the list of themes from the database. Defaults to FALSE.
+ *
+ * @return
+ *   An associative array of the currently available themes. The keys are the
+ *   themes' machine names and the values are objects having the following
+ *   properties:
+ *   - filename: The filepath and name of the .info file.
+ *   - name: The machine name of the theme.
+ *   - status: 1 for enabled, 0 for disabled themes.
+ *   - info: The contents of the .info file.
+ *   - stylesheets: A two dimensional array, using the first key for the
+ *     media attribute (e.g. 'all'), the second for the name of the file
+ *     (e.g. style.css). The value is a complete filepath (e.g.
+ *     themes/bartik/style.css). Not set if no stylesheets are defined in the
+ *     .info file.
+ *   - scripts: An associative array of JavaScripts, using the filename as key
+ *     and the complete filepath as value. Not set if no scripts are defined in
+ *     the .info file.
+ *   - prefix: The base theme engine prefix.
+ *   - engine: The machine name of the theme engine.
+ *   - base_theme: If this is a sub-theme, the machine name of the base theme
+ *     defined in the .info file. Otherwise, the element is not set.
+ *   - base_themes: If this is a sub-theme, an associative array of the
+ *     base-theme ancestors of this theme, starting with this theme's base
+ *     theme, then the base theme's own base theme, etc. Each entry has an
+ *     array key equal to the theme's machine name, and a value equal to the
+ *     human-readable theme name; if a theme with matching machine name does
+ *     not exist in the system, the value will instead be NULL (and since the
+ *     system would not know whether that theme itself has a base theme, that
+ *     will end the array of base themes). This is not set if the theme is not
+ *     a sub-theme.
+ *   - sub_themes: An associative array of themes on the system that are
+ *     either direct sub-themes (that is, they declare this theme to be
+ *     their base theme), direct sub-themes of sub-themes, etc. The keys are
+ *     the themes' machine names, and the values are the themes' human-readable
+ *     names. This element is not set if there are no themes on the system that
+ *     declare this theme as their base theme.
+*/
+function list_themes($refresh = FALSE) {
+  $list = &drupal_static(__FUNCTION__, array());
+
+  if ($refresh) {
+    $list = array();
+    system_list_reset();
+  }
+
+  if (empty($list)) {
+    $list = array();
+    $themes = array();
+    // Extract from the database only when it is available.
+    // Also check that the site is not in the middle of an install or update.
+    if (!defined('MAINTENANCE_MODE')) {
+      try {
+        $themes = system_list('theme');
+      }
+      catch (Exception $e) {
+        // If the database is not available, rebuild the theme data.
+        $themes = _system_rebuild_theme_data();
+      }
+    }
+    else {
+      // Scan the installation when the database should not be read.
+      $themes = _system_rebuild_theme_data();
+    }
+
+    foreach ($themes as $theme) {
+      foreach ($theme->info['stylesheets'] as $media => $stylesheets) {
+        foreach ($stylesheets as $stylesheet => $path) {
+          $theme->stylesheets[$media][$stylesheet] = $path;
+        }
+      }
+      foreach ($theme->info['scripts'] as $script => $path) {
+        $theme->scripts[$script] = $path;
+      }
+      if (isset($theme->info['engine'])) {
+        $theme->engine = $theme->info['engine'];
+      }
+      if (isset($theme->info['base theme'])) {
+        $theme->base_theme = $theme->info['base theme'];
+      }
+      // Status is normally retrieved from the database. Add zero values when
+      // read from the installation directory to prevent notices.
+      if (!isset($theme->status)) {
+        $theme->status = 0;
+      }
+      $list[$theme->name] = $theme;
+    }
+  }
+
+  return $list;
+}
+
+/**
+ * Find all the base themes for the specified theme.
+ *
+ * Themes can inherit templates and function implementations from earlier themes.
+ *
+ * @param $themes
+ *   An array of available themes.
+ * @param $key
+ *   The name of the theme whose base we are looking for.
+ * @param $used_keys
+ *   A recursion parameter preventing endless loops.
+ * @return
+ *   Returns an array of all of the theme's ancestors; the first element's value
+ *   will be NULL if an error occurred.
+ */
+function drupal_find_base_themes($themes, $key, $used_keys = array()) {
+  $base_key = $themes[$key]->info['base theme'];
+  // Does the base theme exist?
+  if (!isset($themes[$base_key])) {
+    return array($base_key => NULL);
+  }
+
+  $current_base_theme = array($base_key => $themes[$base_key]->info['name']);
+
+  // Is the base theme itself a child of another theme?
+  if (isset($themes[$base_key]->info['base theme'])) {
+    // Do we already know the base themes of this theme?
+    if (isset($themes[$base_key]->base_themes)) {
+      return $themes[$base_key]->base_themes + $current_base_theme;
+    }
+    // Prevent loops.
+    if (!empty($used_keys[$base_key])) {
+      return array($base_key => NULL);
+    }
+    $used_keys[$base_key] = TRUE;
+    return drupal_find_base_themes($themes, $base_key, $used_keys) + $current_base_theme;
+  }
+  // If we get here, then this is our parent theme.
+  return $current_base_theme;
+}
+
+/**
+ * Generates themed output.
+ *
+ * All requests for themed output must go through this function. It examines
+ * the request and routes it to the appropriate
+ * @link themeable theme function or template @endlink, by checking the theme
+ * registry.
+ *
+ * Most commonly, the first argument to this function is the name of the theme
+ * hook. For instance, to theme a taxonomy term, the theme hook name is
+ * 'taxonomy_term'. Modules register theme hooks within a hook_theme()
+ * implementation and provide a default implementation via a function named
+ * theme_HOOK() (e.g., theme_taxonomy_term()) or via a template file named
+ * according to the value of the 'template' key registered with the theme hook
+ * (see hook_theme() for details). Default templates are implemented with the
+ * PHPTemplate rendering engine and are named the same as the theme hook, with
+ * underscores changed to hyphens, so for the 'taxonomy_term' theme hook, the
+ * default template is 'taxonomy-term.tpl.php'.
+ *
+ * Themes may also register new theme hooks within a hook_theme()
+ * implementation, but it is more common for themes to override default
+ * implementations provided by modules than to register entirely new theme
+ * hooks. Themes can override a default implementation by implementing a
+ * function named THEME_HOOK() (for example, the 'bartik' theme overrides the
+ * default implementation of the 'menu_tree' theme hook by implementing a
+ * bartik_menu_tree() function), or by adding a template file within its folder
+ * structure that follows the template naming structure used by the theme's
+ * rendering engine (for example, since the Bartik theme uses the PHPTemplate
+ * rendering engine, it overrides the default implementation of the 'page' theme
+ * hook by containing a 'page.tpl.php' file within its folder structure).
+ *
+ * If the implementation is a template file, several functions are called
+ * before the template file is invoked, to modify the $variables array. These
+ * fall into the "preprocessing" phase and the "processing" phase, and are
+ * executed (if they exist), in the following order (note that in the following
+ * list, HOOK indicates the theme hook name, MODULE indicates a module name,
+ * THEME indicates a theme name, and ENGINE indicates a theme engine name):
+ * - template_preprocess(&$variables, $hook): Creates a default set of
+ *   variables for all theme hooks with template implementations.
+ * - template_preprocess_HOOK(&$variables): Should be implemented by the module
+ *   that registers the theme hook, to set up default variables.
+ * - MODULE_preprocess(&$variables, $hook): hook_preprocess() is invoked on all
+ *   implementing modules.
+ * - MODULE_preprocess_HOOK(&$variables): hook_preprocess_HOOK() is invoked on
+ *   all implementing modules, so that modules that didn't define the theme
+ *   hook can alter the variables.
+ * - ENGINE_engine_preprocess(&$variables, $hook): Allows the theme engine to
+ *   set necessary variables for all theme hooks with template implementations.
+ * - ENGINE_engine_preprocess_HOOK(&$variables): Allows the theme engine to set
+ *   necessary variables for the particular theme hook.
+ * - THEME_preprocess(&$variables, $hook): Allows the theme to set necessary
+ *   variables for all theme hooks with template implementations.
+ * - THEME_preprocess_HOOK(&$variables): Allows the theme to set necessary
+ *   variables specific to the particular theme hook.
+ * - template_process(&$variables, $hook): Creates an additional set of default
+ *   variables for all theme hooks with template implementations. The variables
+ *   created in this function are derived from ones created by
+ *   template_preprocess(), but potentially altered by the other preprocess
+ *   functions listed above. For example, any preprocess function can add to or
+ *   modify the $variables['attributes_array'] variable, and after all of them
+ *   have finished executing, template_process() flattens it into a
+ *   $variables['attributes'] string for convenient use by templates.
+ * - template_process_HOOK(&$variables): Should be implemented by the module
+ *   that registers the theme hook, if it needs to perform additional variable
+ *   processing after all preprocess functions have finished.
+ * - MODULE_process(&$variables, $hook): hook_process() is invoked on all
+ *   implementing modules.
+ * - MODULE_process_HOOK(&$variables): hook_process_HOOK() is invoked on
+ *   on all implementing modules, so that modules that didn't define the theme
+ *   hook can alter the variables.
+ * - ENGINE_engine_process(&$variables, $hook): Allows the theme engine to
+ *   process variables for all theme hooks with template implementations.
+ * - ENGINE_engine_process_HOOK(&$variables): Allows the theme engine to process
+ *   the variables specific to the theme hook.
+ * - THEME_process(&$variables, $hook):  Allows the theme to process the
+ *   variables for all theme hooks with template implementations.
+ * - THEME_process_HOOK(&$variables):  Allows the theme to process the
+ *   variables specific to the theme hook.
+ *
+ * If the implementation is a function, only the theme-hook-specific preprocess
+ * and process functions (the ones ending in _HOOK) are called from the
+ * list above. This is because theme hooks with function implementations
+ * need to be fast, and calling the non-theme-hook-specific preprocess and
+ * process functions for them would incur a noticeable performance penalty.
+ *
+ * There are two special variables that these preprocess and process functions
+ * can set: 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be
+ * merged together to form a list of 'suggested' alternate theme hooks to use,
+ * in reverse order of priority. theme_hook_suggestion will always be a higher
+ * priority than items in theme_hook_suggestions. theme() will use the
+ * highest priority implementation that exists. If none exists, theme() will
+ * use the implementation for the theme hook it was called with. These
+ * suggestions are similar to and are used for similar reasons as calling
+ * theme() with an array as the $hook parameter (see below). The difference
+ * is whether the suggestions are determined by the code that calls theme() or
+ * by a preprocess or process function.
+ *
+ * @param $hook
+ *   The name of the theme hook to call. If the name contains a
+ *   double-underscore ('__') and there isn't an implementation for the full
+ *   name, the part before the '__' is checked. This allows a fallback to a
+ *   more generic implementation. For example, if theme('links__node', ...) is
+ *   called, but there is no implementation of that theme hook, then the
+ *   'links' implementation is used. This process is iterative, so if
+ *   theme('links__contextual__node', ...) is called, theme() checks for the
+ *   following implementations, and uses the first one that exists:
+ *   - links__contextual__node
+ *   - links__contextual
+ *   - links
+ *   This allows themes to create specific theme implementations for named
+ *   objects and contexts of otherwise generic theme hooks. The $hook parameter
+ *   may also be an array, in which case the first theme hook that has an
+ *   implementation is used. This allows for the code that calls theme() to
+ *   explicitly specify the fallback order in a situation where using the '__'
+ *   convention is not desired or is insufficient.
+ * @param $variables
+ *   An associative array of variables to merge with defaults from the theme
+ *   registry, pass to preprocess and process functions for modification, and
+ *   finally, pass to the function or template implementing the theme hook.
+ *   Alternatively, this can be a renderable array, in which case, its
+ *   properties are mapped to variables expected by the theme hook
+ *   implementations.
+ *
+ * @return
+ *   An HTML string representing the themed output.
+ *
+ * @see themeable
+ * @see hook_theme()
+ * @see template_preprocess()
+ * @see template_process()
+ */
+function theme($hook, $variables = array()) {
+  // If called before all modules are loaded, we do not necessarily have a full
+  // theme registry to work with, and therefore cannot process the theme
+  // request properly. See also _theme_load_registry().
+  if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) {
+    throw new Exception(t('theme() may not be called until all modules are loaded.'));
+  }
+
+  $hooks = theme_get_registry(FALSE);
+
+  // If an array of hook candidates were passed, use the first one that has an
+  // implementation.
+  if (is_array($hook)) {
+    foreach ($hook as $candidate) {
+      if (isset($hooks[$candidate])) {
+        break;
+      }
+    }
+    $hook = $candidate;
+  }
+
+  // If there's no implementation, check for more generic fallbacks. If there's
+  // still no implementation, log an error and return an empty string.
+  if (!isset($hooks[$hook])) {
+    // Iteratively strip everything after the last '__' delimiter, until an
+    // implementation is found.
+    while ($pos = strrpos($hook, '__')) {
+      $hook = substr($hook, 0, $pos);
+      if (isset($hooks[$hook])) {
+        break;
+      }
+    }
+    if (!isset($hooks[$hook])) {
+      // Only log a message when not trying theme suggestions ($hook being an
+      // array).
+      if (!isset($candidate)) {
+        watchdog('theme', 'Theme hook %hook not found.', array('%hook' => $hook), WATCHDOG_WARNING);
+      }
+      return '';
+    }
+  }
+
+  $info = $hooks[$hook];
+  global $theme_path;
+  $temp = $theme_path;
+  // point path_to_theme() to the currently used theme path:
+  $theme_path = $info['theme path'];
+
+  // Include a file if the theme function or variable processor is held
+  // elsewhere.
+  if (!empty($info['includes'])) {
+    foreach ($info['includes'] as $include_file) {
+      include_once DRUPAL_ROOT . '/' . $include_file;
+    }
+  }
+
+  // If a renderable array is passed as $variables, then set $variables to
+  // the arguments expected by the theme function.
+  if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) {
+    $element = $variables;
+    $variables = array();
+    if (isset($info['variables'])) {
+      foreach (array_keys($info['variables']) as $name) {
+        if (isset($element["#$name"])) {
+          $variables[$name] = $element["#$name"];
+        }
+      }
+    }
+    else {
+      $variables[$info['render element']] = $element;
+    }
+  }
+
+  // Merge in argument defaults.
+  if (!empty($info['variables'])) {
+    $variables += $info['variables'];
+  }
+  elseif (!empty($info['render element'])) {
+    $variables += array($info['render element'] => array());
+  }
+
+  // Invoke the variable processors, if any. The processors may specify
+  // alternate suggestions for which hook's template/function to use. If the
+  // hook is a suggestion of a base hook, invoke the variable processors of
+  // the base hook, but retain the suggestion as a high priority suggestion to
+  // be used unless overridden by a variable processor function.
+  if (isset($info['base hook'])) {
+    $base_hook = $info['base hook'];
+    $base_hook_info = $hooks[$base_hook];
+    // Include files required by the base hook, since its variable processors
+    // might reside there.
+    if (!empty($base_hook_info['includes'])) {
+      foreach ($base_hook_info['includes'] as $include_file) {
+        include_once DRUPAL_ROOT . '/' . $include_file;
+      }
+    }
+    if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) {
+      $variables['theme_hook_suggestion'] = $hook;
+      $hook = $base_hook;
+      $info = $base_hook_info;
+    }
+  }
+  if (isset($info['preprocess functions']) || isset($info['process functions'])) {
+    $variables['theme_hook_suggestions'] = array();
+    foreach (array('preprocess functions', 'process functions') as $phase) {
+      if (!empty($info[$phase])) {
+        foreach ($info[$phase] as $processor_function) {
+          if (function_exists($processor_function)) {
+            // We don't want a poorly behaved process function changing $hook.
+            $hook_clone = $hook;
+            $processor_function($variables, $hook_clone);
+          }
+        }
+      }
+    }
+    // If the preprocess/process functions specified hook suggestions, and the
+    // suggestion exists in the theme registry, use it instead of the hook that
+    // theme() was called with. This allows the preprocess/process step to
+    // route to a more specific theme hook. For example, a function may call
+    // theme('node', ...), but a preprocess function can add 'node__article' as
+    // a suggestion, enabling a theme to have an alternate template file for
+    // article nodes. Suggestions are checked in the following order:
+    // - The 'theme_hook_suggestion' variable is checked first. It overrides
+    //   all others.
+    // - The 'theme_hook_suggestions' variable is checked in FILO order, so the
+    //   last suggestion added to the array takes precedence over suggestions
+    //   added earlier.
+    $suggestions = array();
+    if (!empty($variables['theme_hook_suggestions'])) {
+      $suggestions = $variables['theme_hook_suggestions'];
+    }
+    if (!empty($variables['theme_hook_suggestion'])) {
+      $suggestions[] = $variables['theme_hook_suggestion'];
+    }
+    foreach (array_reverse($suggestions) as $suggestion) {
+      if (isset($hooks[$suggestion])) {
+        $info = $hooks[$suggestion];
+        break;
+      }
+    }
+  }
+
+  // Generate the output using either a function or a template.
+  $output = '';
+  if (isset($info['function'])) {
+    if (function_exists($info['function'])) {
+      $output = $info['function']($variables);
+    }
+  }
+  else {
+    // Default render function and extension.
+    $render_function = 'theme_render_template';
+    $extension = '.tpl.php';
+
+    // The theme engine may use a different extension and a different renderer.
+    global $theme_engine;
+    if (isset($theme_engine)) {
+      if ($info['type'] != 'module') {
+        if (function_exists($theme_engine . '_render_template')) {
+          $render_function = $theme_engine . '_render_template';
+        }
+        $extension_function = $theme_engine . '_extension';
+        if (function_exists($extension_function)) {
+          $extension = $extension_function();
+        }
+      }
+    }
+
+    // In some cases, a template implementation may not have had
+    // template_preprocess() run (for example, if the default implementation is
+    // a function, but a template overrides that default implementation). In
+    // these cases, a template should still be able to expect to have access to
+    // the variables provided by template_preprocess(), so we add them here if
+    // they don't already exist. We don't want to run template_preprocess()
+    // twice (it would be inefficient and mess up zebra striping), so we use the
+    // 'directory' variable to determine if it has already run, which while not
+    // completely intuitive, is reasonably safe, and allows us to save on the
+    // overhead of adding some new variable to track that.
+    if (!isset($variables['directory'])) {
+      $default_template_variables = array();
+      template_preprocess($default_template_variables, $hook);
+      $variables += $default_template_variables;
+    }
+
+    // Render the output using the template file.
+    $template_file = $info['template'] . $extension;
+    if (isset($info['path'])) {
+      $template_file = $info['path'] . '/' . $template_file;
+    }
+    $output = $render_function($template_file, $variables);
+  }
+
+  // restore path_to_theme()
+  $theme_path = $temp;
+  return $output;
+}
+
+/**
+ * Returns the path to the current themed element.
+ *
+ * It can point to the active theme or the module handling a themed
+ * implementation. For example, when invoked within the scope of a theming call
+ * it will depend on where the theming function is handled. If implemented from
+ * a module, it will point to the module. If implemented from the active theme,
+ * it will point to the active theme. When called outside the scope of a
+ * theming call, it will always point to the active theme.
+ */
+function path_to_theme() {
+  global $theme_path;
+
+  if (!isset($theme_path)) {
+    drupal_theme_initialize();
+  }
+
+  return $theme_path;
+}
+
+/**
+ * Allows themes and/or theme engines to discover overridden theme functions.
+ *
+ * @param $cache
+ *   The existing cache of theme hooks to test against.
+ * @param $prefixes
+ *   An array of prefixes to test, in reverse order of importance.
+ *
+ * @return $implementations
+ *   The functions found, suitable for returning from hook_theme;
+ */
+function drupal_find_theme_functions($cache, $prefixes) {
+  $implementations = array();
+  $functions = get_defined_functions();
+
+  foreach ($cache as $hook => $info) {
+    foreach ($prefixes as $prefix) {
+      // Find theme functions that implement possible "suggestion" variants of
+      // registered theme hooks and add those as new registered theme hooks.
+      // The 'pattern' key defines a common prefix that all suggestions must
+      // start with. The default is the name of the hook followed by '__'. An
+      // 'base hook' key is added to each entry made for a found suggestion,
+      // so that common functionality can be implemented for all suggestions of
+      // the same base hook. To keep things simple, deep hierarchy of
+      // suggestions is not supported: each suggestion's 'base hook' key
+      // refers to a base hook, not to another suggestion, and all suggestions
+      // are found using the base hook's pattern, not a pattern from an
+      // intermediary suggestion.
+      $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
+      if (!isset($info['base hook']) && !empty($pattern)) {
+        $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $functions['user']);
+        if ($matches) {
+          foreach ($matches as $match) {
+            $new_hook = substr($match, strlen($prefix) + 1);
+            $arg_name = isset($info['variables']) ? 'variables' : 'render element';
+            $implementations[$new_hook] = array(
+              'function' => $match,
+              $arg_name => $info[$arg_name],
+              'base hook' => $hook,
+            );
+          }
+        }
+      }
+      // Find theme functions that implement registered theme hooks and include
+      // that in what is returned so that the registry knows that the theme has
+      // this implementation.
+      if (function_exists($prefix . '_' . $hook)) {
+        $implementations[$hook] = array(
+          'function' => $prefix . '_' . $hook,
+        );
+      }
+    }
+  }
+
+  return $implementations;
+}
+
+/**
+ * Allows themes and/or theme engines to easily discover overridden templates.
+ *
+ * @param $cache
+ *   The existing cache of theme hooks to test against.
+ * @param $extension
+ *   The extension that these templates will have.
+ * @param $path
+ *   The path to search.
+ */
+function drupal_find_theme_templates($cache, $extension, $path) {
+  $implementations = array();
+
+  // Collect paths to all sub-themes grouped by base themes. These will be
+  // used for filtering. This allows base themes to have sub-themes in its
+  // folder hierarchy without affecting the base themes template discovery.
+  $theme_paths = array();
+  foreach (list_themes() as $theme_info) {
+    if (!empty($theme_info->base_theme)) {
+      $theme_paths[$theme_info->base_theme][$theme_info->name] = dirname($theme_info->filename);
+    }
+  }
+  foreach ($theme_paths as $basetheme => $subthemes) {
+    foreach ($subthemes as $subtheme => $subtheme_path) {
+      if (isset($theme_paths[$subtheme])) {
+        $theme_paths[$basetheme] = array_merge($theme_paths[$basetheme], $theme_paths[$subtheme]);
+      }
+    }
+  }
+  global $theme;
+  $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array();
+
+  // Escape the periods in the extension.
+  $regex = '/' . str_replace('.', '\.', $extension) . '$/';
+  // Get a listing of all template files in the path to search.
+  $files = drupal_system_listing($regex, $path, 'name', 0);
+
+  // Find templates that implement registered theme hooks and include that in
+  // what is returned so that the registry knows that the theme has this
+  // implementation.
+  foreach ($files as $template => $file) {
+    // Ignore sub-theme templates for the current theme.
+    if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) {
+      continue;
+    }
+    // Chop off the remaining extensions if there are any. $template already
+    // has the rightmost extension removed, but there might still be more,
+    // such as with .tpl.php, which still has .tpl in $template at this point.
+    if (($pos = strpos($template, '.')) !== FALSE) {
+      $template = substr($template, 0, $pos);
+    }
+    // Transform - in filenames to _ to match function naming scheme
+    // for the purposes of searching.
+    $hook = strtr($template, '-', '_');
+    if (isset($cache[$hook])) {
+      $implementations[$hook] = array(
+        'template' => $template,
+        'path' => dirname($file->uri),
+      );
+    }
+  }
+
+  // Find templates that implement possible "suggestion" variants of registered
+  // theme hooks and add those as new registered theme hooks. See
+  // drupal_find_theme_functions() for more information about suggestions and
+  // the use of 'pattern' and 'base hook'.
+  $patterns = array_keys($files);
+  foreach ($cache as $hook => $info) {
+    $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
+    if (!isset($info['base hook']) && !empty($pattern)) {
+      // Transform _ in pattern to - to match file naming scheme
+      // for the purposes of searching.
+      $pattern = strtr($pattern, '_', '-');
+
+      $matches = preg_grep('/^' . $pattern . '/', $patterns);
+      if ($matches) {
+        foreach ($matches as $match) {
+          $file = substr($match, 0, strpos($match, '.'));
+          // Put the underscores back in for the hook name and register this
+          // pattern.
+          $arg_name = isset($info['variables']) ? 'variables' : 'render element';
+          $implementations[strtr($file, '-', '_')] = array(
+            'template' => $file,
+            'path' => dirname($files[$match]->uri),
+            $arg_name => $info[$arg_name],
+            'base hook' => $hook,
+          );
+        }
+      }
+    }
+  }
+  return $implementations;
+}
+
+/**
+ * Retrieves a setting for the current theme or for a given theme.
+ *
+ * The final setting is obtained from the last value found in the following
+ * sources:
+ * - the default global settings specified in this function
+ * - the default theme-specific settings defined in any base theme's .info file
+ * - the default theme-specific settings defined in the theme's .info file
+ * - the saved values from the global theme settings form
+ * - the saved values from the theme's settings form
+ * To only retrieve the default global theme setting, an empty string should be
+ * given for $theme.
+ *
+ * @param $setting_name
+ *   The name of the setting to be retrieved.
+ * @param $theme
+ *   The name of a given theme; defaults to the current theme.
+ *
+ * @return
+ *   The value of the requested setting, NULL if the setting does not exist.
+ */
+function theme_get_setting($setting_name, $theme = NULL) {
+  $cache = &drupal_static(__FUNCTION__, array());
+
+  // If no key is given, use the current theme if we can determine it.
+  if (!isset($theme)) {
+    $theme = !empty($GLOBALS['theme_key']) ? $GLOBALS['theme_key'] : '';
+  }
+
+  if (empty($cache[$theme])) {
+    // Set the default values for each global setting.
+    // To add new global settings, add their default values below, and then
+    // add form elements to system_theme_settings() in system.admin.inc.
+    $cache[$theme] = array(
+      'default_logo'                     =>  1,
+      'logo_path'                        =>  '',
+      'default_favicon'                  =>  1,
+      'favicon_path'                     =>  '',
+      // Use the IANA-registered MIME type for ICO files as default.
+      'favicon_mimetype'                 =>  'image/vnd.microsoft.icon',
+    );
+    // Turn on all default features.
+    $features = _system_default_theme_features();
+    foreach ($features as $feature) {
+      $cache[$theme]['toggle_' . $feature] = 1;
+    }
+
+    // Get the values for the theme-specific settings from the .info files of
+    // the theme and all its base themes.
+    if ($theme) {
+      $themes = list_themes();
+      $theme_object = $themes[$theme];
+
+      // Create a list which includes the current theme and all its base themes.
+      if (isset($theme_object->base_themes)) {
+        $theme_keys = array_keys($theme_object->base_themes);
+        $theme_keys[] = $theme;
+      }
+      else {
+        $theme_keys = array($theme);
+      }
+      foreach ($theme_keys as $theme_key) {
+        if (!empty($themes[$theme_key]->info['settings'])) {
+          $cache[$theme] = array_merge($cache[$theme], $themes[$theme_key]->info['settings']);
+        }
+      }
+    }
+
+    // Get the saved global settings from the database.
+    $cache[$theme] = array_merge($cache[$theme], variable_get('theme_settings', array()));
+
+    if ($theme) {
+      // Get the saved theme-specific settings from the database.
+      $cache[$theme] = array_merge($cache[$theme], variable_get('theme_' . $theme . '_settings', array()));
+
+      // If the theme does not support a particular feature, override the global
+      // setting and set the value to NULL.
+      if (!empty($theme_object->info['features'])) {
+        foreach ($features as $feature) {
+          if (!in_array($feature, $theme_object->info['features'])) {
+            $cache[$theme]['toggle_' . $feature] = NULL;
+          }
+        }
+      }
+
+      // Generate the path to the logo image.
+      if ($cache[$theme]['toggle_logo']) {
+        if ($cache[$theme]['default_logo']) {
+          $cache[$theme]['logo'] = file_create_url(dirname($theme_object->filename) . '/logo.png');
+        }
+        elseif ($cache[$theme]['logo_path']) {
+          $cache[$theme]['logo'] = file_create_url($cache[$theme]['logo_path']);
+        }
+      }
+
+      // Generate the path to the favicon.
+      if ($cache[$theme]['toggle_favicon']) {
+        if ($cache[$theme]['default_favicon']) {
+          if (file_exists($favicon = dirname($theme_object->filename) . '/favicon.ico')) {
+            $cache[$theme]['favicon'] = file_create_url($favicon);
+          }
+          else {
+            $cache[$theme]['favicon'] = file_create_url('misc/favicon.ico');
+          }
+        }
+        elseif ($cache[$theme]['favicon_path']) {
+          $cache[$theme]['favicon'] = file_create_url($cache[$theme]['favicon_path']);
+        }
+        else {
+          $cache[$theme]['toggle_favicon'] = FALSE;
+        }
+      }
+    }
+  }
+
+  return isset($cache[$theme][$setting_name]) ? $cache[$theme][$setting_name] : NULL;
+}
+
+/**
+ * Renders a system default template, which is essentially a PHP template.
+ *
+ * @param $template_file
+ *   The filename of the template to render.
+ * @param $variables
+ *   A keyed array of variables that will appear in the output.
+ *
+ * @return
+ *   The output generated by the template.
+ */
+function theme_render_template($template_file, $variables) {
+  // Extract the variables to a local namespace
+  extract($variables, EXTR_SKIP);
+
+  // Start output buffering
+  ob_start();
+
+  // Include the template file
+  include DRUPAL_ROOT . '/' . $template_file;
+
+  // End buffering and return its contents
+  return ob_get_clean();
+}
+
+/**
+ * Enables a given list of themes.
+ *
+ * @param $theme_list
+ *   An array of theme names.
+ */
+function theme_enable($theme_list) {
+  drupal_clear_css_cache();
+
+  foreach ($theme_list as $key) {
+    db_update('system')
+      ->fields(array('status' => 1))
+      ->condition('type', 'theme')
+      ->condition('name', $key)
+      ->execute();
+  }
+
+  list_themes(TRUE);
+  menu_rebuild();
+  drupal_theme_rebuild();
+
+  // Invoke hook_themes_enabled() after the themes have been enabled.
+  module_invoke_all('themes_enabled', $theme_list);
+}
+
+/**
+ * Disables a given list of themes.
+ *
+ * @param $theme_list
+ *   An array of theme names.
+ */
+function theme_disable($theme_list) {
+  // Don't disable the default theme.
+  if ($pos = array_search(variable_get('theme_default', 'bartik'), $theme_list) !== FALSE) {
+    unset($theme_list[$pos]);
+    if (empty($theme_list)) {
+      return;
+    }
+  }
+
+  drupal_clear_css_cache();
+
+  foreach ($theme_list as $key) {
+    db_update('system')
+      ->fields(array('status' => 0))
+      ->condition('type', 'theme')
+      ->condition('name', $key)
+      ->execute();
+  }
+
+  list_themes(TRUE);
+  menu_rebuild();
+  drupal_theme_rebuild();
+
+  // Invoke hook_themes_disabled after the themes have been disabled.
+  module_invoke_all('themes_disabled', $theme_list);
+}
+
+/**
+ * @addtogroup themeable
+ * @{
+ */
+
+/**
+ * Returns HTML for status and/or error messages, grouped by type.
+ *
+ * An invisible heading identifies the messages for assistive technology.
+ * Sighted users see a colored box. See http://www.w3.org/TR/WCAG-TECHS/H69.html
+ * for info.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - display: (optional) Set to 'status' or 'error' to display only messages
+ *     of that type.
+ */
+function theme_status_messages($variables) {
+  $display = $variables['display'];
+  $output = '';
+
+  $status_heading = array(
+    'status' => t('Status message'),
+    'error' => t('Error message'),
+    'warning' => t('Warning message'),
+  );
+  foreach (drupal_get_messages($display) as $type => $messages) {
+    $output .= "<div class=\"messages $type\">\n";
+    if (!empty($status_heading[$type])) {
+      $output .= '<h2 class="element-invisible">' . $status_heading[$type] . "</h2>\n";
+    }
+    if (count($messages) > 1) {
+      $output .= " <ul>\n";
+      foreach ($messages as $message) {
+        $output .= '  <li>' . $message . "</li>\n";
+      }
+      $output .= " </ul>\n";
+    }
+    else {
+      $output .= $messages[0];
+    }
+    $output .= "</div>\n";
+  }
+  return $output;
+}
+
+/**
+ * Returns HTML for a link.
+ *
+ * All Drupal code that outputs a link should call the l() function. That
+ * function performs some initial preprocessing, and then, if necessary, calls
+ * theme('link') for rendering the anchor tag.
+ *
+ * To optimize performance for sites that don't need custom theming of links,
+ * the l() function includes an inline copy of this function, and uses that
+ * copy if none of the enabled modules or the active theme implement any
+ * preprocess or process functions or override this theme implementation.
+ *
+ * @param $variables
+ *   An associative array containing the keys 'text', 'path', and 'options'.
+ *   See the l() function for information about these variables.
+ *
+ * @see l()
+ */
+function theme_link($variables) {
+  return '<a href="' . check_plain(url($variables['path'], $variables['options'])) . '"' . drupal_attributes($variables['options']['attributes']) . '>' . ($variables['options']['html'] ? $variables['text'] : check_plain($variables['text'])) . '</a>';
+}
+
+/**
+ * Returns HTML for a set of links.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - links: An associative array of links to be themed. The key for each link
+ *     is used as its CSS class. Each link should be itself an array, with the
+ *     following elements:
+ *     - title: The link text.
+ *     - href: The link URL. If omitted, the 'title' is shown as a plain text
+ *       item in the links list.
+ *     - html: (optional) Whether or not 'title' is HTML. If set, the title
+ *       will not be passed through check_plain().
+ *     - attributes: (optional) Attributes for the anchor, or for the <span>
+ *       tag used in its place if no 'href' is supplied. If element 'class' is
+ *       included, it must be an array of one or more class names.
+ *     If the 'href' element is supplied, the entire link array is passed to
+ *     l() as its $options parameter.
+ *   - attributes: A keyed array of attributes for the UL containing the
+ *     list of links.
+ *   - heading: (optional) A heading to precede the links. May be an
+ *     associative array or a string. If it's an array, it can have the
+ *     following elements:
+ *     - text: The heading text.
+ *     - level: The heading level (e.g. 'h2', 'h3').
+ *     - class: (optional) An array of the CSS classes for the heading.
+ *     When using a string it will be used as the text of the heading and the
+ *     level will default to 'h2'. Headings should be used on navigation menus
+ *     and any list of links that consistently appears on multiple pages. To
+ *     make the heading invisible use the 'element-invisible' CSS class. Do not
+ *     use 'display:none', which removes it from screen-readers and assistive
+ *     technology. Headings allow screen-reader and keyboard only users to
+ *     navigate to or skip the links. See
+ *     http://juicystudio.com/article/screen-readers-display-none.php and
+ *     http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
+ */
+function theme_links($variables) {
+  $links = $variables['links'];
+  $attributes = $variables['attributes'];
+  $heading = $variables['heading'];
+  global $language_url;
+  $output = '';
+
+  if (count($links) > 0) {
+    $output = '';
+
+    // Treat the heading first if it is present to prepend it to the
+    // list of links.
+    if (!empty($heading)) {
+      if (is_string($heading)) {
+        // Prepare the array that will be used when the passed heading
+        // is a string.
+        $heading = array(
+          'text' => $heading,
+          // Set the default level of the heading.
+          'level' => 'h2',
+        );
+      }
+      $output .= '<' . $heading['level'];
+      if (!empty($heading['class'])) {
+        $output .= drupal_attributes(array('class' => $heading['class']));
+      }
+      $output .= '>' . check_plain($heading['text']) . '</' . $heading['level'] . '>';
+    }
+
+    $output .= '<ul' . drupal_attributes($attributes) . '>';
+
+    $num_links = count($links);
+    $i = 1;
+
+    foreach ($links as $key => $link) {
+      $class = array($key);
+
+      // Add first, last and active classes to the list of links to help out themers.
+      if ($i == 1) {
+        $class[] = 'first';
+      }
+      if ($i == $num_links) {
+        $class[] = 'last';
+      }
+      if (isset($link['href']) && ($link['href'] == $_GET['q'] || ($link['href'] == '<front>' && drupal_is_front_page()))
+          && (empty($link['language']) || $link['language']->language == $language_url->language)) {
+        $class[] = 'active';
+      }
+      $output .= '<li' . drupal_attributes(array('class' => $class)) . '>';
+
+      if (isset($link['href'])) {
+        // Pass in $link as $options, they share the same keys.
+        $output .= l($link['title'], $link['href'], $link);
+      }
+      elseif (!empty($link['title'])) {
+        // Some links are actually not links, but we wrap these in <span> for adding title and class attributes.
+        if (empty($link['html'])) {
+          $link['title'] = check_plain($link['title']);
+        }
+        $span_attributes = '';
+        if (isset($link['attributes'])) {
+          $span_attributes = drupal_attributes($link['attributes']);
+        }
+        $output .= '<span' . $span_attributes . '>' . $link['title'] . '</span>';
+      }
+
+      $i++;
+      $output .= "</li>\n";
+    }
+
+    $output .= '</ul>';
+  }
+
+  return $output;
+}
+
+/**
+ * Returns HTML for an image.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - path: Either the path of the image file (relative to base_path()) or a
+ *     full URL.
+ *   - width: The width of the image (if known).
+ *   - height: The height of the image (if known).
+ *   - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0
+ *     always require an alt attribute. The HTML 5 draft allows the alt
+ *     attribute to be omitted in some cases. Therefore, this variable defaults
+ *     to an empty string, but can be set to NULL for the attribute to be
+ *     omitted. Usually, neither omission nor an empty string satisfies
+ *     accessibility requirements, so it is strongly encouraged for code
+ *     calling theme('image') to pass a meaningful value for this variable.
+ *     - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
+ *     - http://www.w3.org/TR/xhtml1/dtds.html
+ *     - http://dev.w3.org/html5/spec/Overview.html#alt
+ *   - title: The title text is displayed when the image is hovered in some
+ *     popular browsers.
+ *   - attributes: Associative array of attributes to be placed in the img tag.
+ */
+function theme_image($variables) {
+  $attributes = $variables['attributes'];
+  $attributes['src'] = file_create_url($variables['path']);
+
+  foreach (array('width', 'height', 'alt', 'title') as $key) {
+
+    if (isset($variables[$key])) {
+      $attributes[$key] = $variables[$key];
+    }
+  }
+
+  return '<img' . drupal_attributes($attributes) . ' />';
+}
+
+/**
+ * Returns HTML for a breadcrumb trail.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - breadcrumb: An array containing the breadcrumb links.
+ */
+function theme_breadcrumb($variables) {
+  $breadcrumb = $variables['breadcrumb'];
+
+  if (!empty($breadcrumb)) {
+    // Provide a navigational heading to give context for breadcrumb links to
+    // screen-reader users. Make the heading invisible with .element-invisible.
+    $output = '<h2 class="element-invisible">' . t('You are here') . '</h2>';
+
+    $output .= '<div class="breadcrumb">' . implode(' » ', $breadcrumb) . '</div>';
+    return $output;
+  }
+}
+
+/**
+ * Returns HTML for a table.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - header: An array containing the table headers. Each element of the array
+ *     can be either a localized string or an associative array with the
+ *     following keys:
+ *     - "data": The localized title of the table column.
+ *     - "field": The database field represented in the table column (required
+ *       if user is to be able to sort on this column).
+ *     - "sort": A default sort order for this column ("asc" or "desc").
+ *     - Any HTML attributes, such as "colspan", to apply to the column header
+ *       cell.
+ *   - rows: An array of table rows. Every row is an array of cells, or an
+ *     associative array with the following keys:
+ *     - "data": an array of cells
+ *     - Any HTML attributes, such as "class", to apply to the table row.
+ *     - "no_striping": a boolean indicating that the row should receive no
+ *       'even / odd' styling. Defaults to FALSE.
+ *     Each cell can be either a string or an associative array with the
+ *     following keys:
+ *     - "data": The string to display in the table cell.
+ *     - "header": Indicates this cell is a header.
+ *     - Any HTML attributes, such as "colspan", to apply to the table cell.
+ *     Here's an example for $rows:
+ *     @code
+ *     $rows = array(
+ *       // Simple row
+ *       array(
+ *         'Cell 1', 'Cell 2', 'Cell 3'
+ *       ),
+ *       // Row with attributes on the row and some of its cells.
+ *       array(
+ *         'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky')
+ *       )
+ *     );
+ *     @endcode
+ *   - attributes: An array of HTML attributes to apply to the table tag.
+ *   - caption: A localized string to use for the <caption> tag.
+ *   - colgroups: An array of column groups. Each element of the array can be
+ *     either:
+ *     - An array of columns, each of which is an associative array of HTML
+ *       attributes applied to the COL element.
+ *     - An array of attributes applied to the COLGROUP element, which must
+ *       include a "data" attribute. To add attributes to COL elements, set the
+ *       "data" attribute with an array of columns, each of which is an
+ *       associative array of HTML attributes.
+ *     Here's an example for $colgroup:
+ *     @code
+ *     $colgroup = array(
+ *       // COLGROUP with one COL element.
+ *       array(
+ *         array(
+ *           'class' => array('funky'), // Attribute for the COL element.
+ *         ),
+ *       ),
+ *       // Colgroup with attributes and inner COL elements.
+ *       array(
+ *         'data' => array(
+ *           array(
+ *             'class' => array('funky'), // Attribute for the COL element.
+ *           ),
+ *         ),
+ *         'class' => array('jazzy'), // Attribute for the COLGROUP element.
+ *       ),
+ *     );
+ *     @endcode
+ *     These optional tags are used to group and set properties on columns
+ *     within a table. For example, one may easily group three columns and
+ *     apply same background style to all.
+ *   - sticky: Use a "sticky" table header.
+ *   - empty: The message to display in an extra row if table does not have any
+ *     rows.
+ */
+function theme_table($variables) {
+  $header = $variables['header'];
+  $rows = $variables['rows'];
+  $attributes = $variables['attributes'];
+  $caption = $variables['caption'];
+  $colgroups = $variables['colgroups'];
+  $sticky = $variables['sticky'];
+  $empty = $variables['empty'];
+
+  // Add sticky headers, if applicable.
+  if (count($header) && $sticky) {
+    drupal_add_js('misc/tableheader.js');
+    // Add 'sticky-enabled' class to the table to identify it for JS.
+    // This is needed to target tables constructed by this function.
+    $attributes['class'][] = 'sticky-enabled';
+  }
+
+  $output = '<table' . drupal_attributes($attributes) . ">\n";
+
+  if (isset($caption)) {
+    $output .= '<caption>' . $caption . "</caption>\n";
+  }
+
+  // Format the table columns:
+  if (count($colgroups)) {
+    foreach ($colgroups as $number => $colgroup) {
+      $attributes = array();
+
+      // Check if we're dealing with a simple or complex column
+      if (isset($colgroup['data'])) {
+        foreach ($colgroup as $key => $value) {
+          if ($key == 'data') {
+            $cols = $value;
+          }
+          else {
+            $attributes[$key] = $value;
+          }
+        }
+      }
+      else {
+        $cols = $colgroup;
+      }
+
+      // Build colgroup
+      if (is_array($cols) && count($cols)) {
+        $output .= ' <colgroup' . drupal_attributes($attributes) . '>';
+        $i = 0;
+        foreach ($cols as $col) {
+          $output .= ' <col' . drupal_attributes($col) . ' />';
+        }
+        $output .= " </colgroup>\n";
+      }
+      else {
+        $output .= ' <colgroup' . drupal_attributes($attributes) . " />\n";
+      }
+    }
+  }
+
+  // Add the 'empty' row message if available.
+  if (!count($rows) && $empty) {
+    $header_count = 0;
+    foreach ($header as $header_cell) {
+      if (is_array($header_cell)) {
+        $header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1;
+      }
+      else {
+        $header_count++;
+      }
+    }
+    $rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message')));
+  }
+
+  // Format the table header:
+  if (count($header)) {
+    $ts = tablesort_init($header);
+    // HTML requires that the thead tag has tr tags in it followed by tbody
+    // tags. Using ternary operator to check and see if we have any rows.
+    $output .= (count($rows) ? ' <thead><tr>' : ' <tr>');
+    foreach ($header as $cell) {
+      $cell = tablesort_header($cell, $header, $ts);
+      $output .= _theme_table_cell($cell, TRUE);
+    }
+    // Using ternary operator to close the tags based on whether or not there are rows
+    $output .= (count($rows) ? " </tr></thead>\n" : "</tr>\n");
+  }
+  else {
+    $ts = array();
+  }
+
+  // Format the table rows:
+  if (count($rows)) {
+    $output .= "<tbody>\n";
+    $flip = array('even' => 'odd', 'odd' => 'even');
+    $class = 'even';
+    foreach ($rows as $number => $row) {
+      $attributes = array();
+
+      // Check if we're dealing with a simple or complex row
+      if (isset($row['data'])) {
+        foreach ($row as $key => $value) {
+          if ($key == 'data') {
+            $cells = $value;
+          }
+          else {
+            $attributes[$key] = $value;
+          }
+        }
+      }
+      else {
+        $cells = $row;
+      }
+      if (count($cells)) {
+        // Add odd/even class
+        if (empty($row['no_striping'])) {
+          $class = $flip[$class];
+          $attributes['class'][] = $class;
+        }
+
+        // Build row
+        $output .= ' <tr' . drupal_attributes($attributes) . '>';
+        $i = 0;
+        foreach ($cells as $cell) {
+          $cell = tablesort_cell($cell, $header, $ts, $i++);
+          $output .= _theme_table_cell($cell);
+        }
+        $output .= " </tr>\n";
+      }
+    }
+    $output .= "</tbody>\n";
+  }
+
+  $output .= "</table>\n";
+  return $output;
+}
+
+/**
+ * Returns HTML for a sort icon.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - style: Set to either 'asc' or 'desc', this determines which icon to
+ *     show.
+ */
+function theme_tablesort_indicator($variables) {
+  if ($variables['style'] == "asc") {
+    return theme('image', array('path' => 'misc/arrow-asc.png', 'width' => 13, 'height' => 13, 'alt' => t('sort ascending'), 'title' => t('sort ascending')));
+  }
+  else {
+    return theme('image', array('path' => 'misc/arrow-desc.png', 'width' => 13, 'height' => 13, 'alt' => t('sort descending'), 'title' => t('sort descending')));
+  }
+}
+
+/**
+ * Returns HTML for a marker for new or updated content.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - type: Number representing the marker type to display. See MARK_NEW,
+ *     MARK_UPDATED, MARK_READ.
+ */
+function theme_mark($variables) {
+  $type = $variables['type'];
+  global $user;
+  if ($user->uid) {
+    if ($type == MARK_NEW) {
+      return ' <span class="marker">' . t('new') . '</span>';
+    }
+    elseif ($type == MARK_UPDATED) {
+      return ' <span class="marker">' . t('updated') . '</span>';
+    }
+  }
+}
+
+/**
+ * Returns HTML for a list or nested list of items.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - items: An array of items to be displayed in the list. If an item is a
+ *     string, then it is used as is. If an item is an array, then the "data"
+ *     element of the array is used as the contents of the list item. If an item
+ *     is an array with a "children" element, those children are displayed in a
+ *     nested list. All other elements are treated as attributes of the list
+ *     item element.
+ *   - title: The title of the list.
+ *   - type: The type of list to return (e.g. "ul", "ol").
+ *   - attributes: The attributes applied to the list element.
+ */
+function theme_item_list($variables) {
+  $items = $variables['items'];
+  $title = $variables['title'];
+  $type = $variables['type'];
+  $attributes = $variables['attributes'];
+
+  // Only output the list container and title, if there are any list items.
+  // Check to see whether the block title exists before adding a header.
+  // Empty headers are not semantic and present accessibility challenges.
+  $output = '<div class="item-list">';
+  if (isset($title) && $title !== '') {
+    $output .= '<h3>' . $title . '</h3>';
+  }
+
+  if (!empty($items)) {
+    $output .= "<$type" . drupal_attributes($attributes) . '>';
+    $num_items = count($items);
+    $i = 0;
+    foreach ($items as $item) {
+      $attributes = array();
+      $children = array();
+      $data = '';
+      $i++;
+      if (is_array($item)) {
+        foreach ($item as $key => $value) {
+          if ($key == 'data') {
+            $data = $value;
+          }
+          elseif ($key == 'children') {
+            $children = $value;
+          }
+          else {
+            $attributes[$key] = $value;
+          }
+        }
+      }
+      else {
+        $data = $item;
+      }
+      if (count($children) > 0) {
+        // Render nested list.
+        $data .= theme_item_list(array('items' => $children, 'title' => NULL, 'type' => $type, 'attributes' => $attributes));
+      }
+      if ($i == 1) {
+        $attributes['class'][] = 'first';
+      }
+      if ($i == $num_items) {
+        $attributes['class'][] = 'last';
+      }
+      $output .= '<li' . drupal_attributes($attributes) . '>' . $data . "</li>\n";
+    }
+    $output .= "</$type>";
+  }
+  $output .= '</div>';
+  return $output;
+}
+
+/**
+ * Returns HTML for a "more help" link.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - url: The URL for the link.
+ */
+function theme_more_help_link($variables) {
+  return '<div class="more-help-link">' . l(t('More help'), $variables['url']) . '</div>';
+}
+
+/**
+ * Returns HTML for a feed icon.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - url: An internal system path or a fully qualified external URL of the
+ *     feed.
+ *   - title: A descriptive title of the feed.
+ */
+function theme_feed_icon($variables) {
+  $text = t('Subscribe to !feed-title', array('!feed-title' => $variables['title']));
+  if ($image = theme('image', array('path' => 'misc/feed.png', 'width' => 16, 'height' => 16, 'alt' => $text))) {
+    return l($image, $variables['url'], array('html' => TRUE, 'attributes' => array('class' => array('feed-icon'), 'title' => $text)));
+  }
+}
+
+/**
+ * Returns HTML for a generic HTML tag with attributes.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array describing the tag:
+ *     - #tag: The tag name to output. Typical tags added to the HTML HEAD:
+ *       - meta: To provide meta information, such as a page refresh.
+ *       - link: To refer to stylesheets and other contextual information.
+ *       - script: To load JavaScript.
+ *     - #attributes: (optional) An array of HTML attributes to apply to the
+ *       tag.
+ *     - #value: (optional) A string containing tag content, such as inline
+ *       CSS.
+ *     - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA
+ *       wrapper prefix.
+ *     - #value_suffix: (optional) A string to append to #value, e.g. a CDATA
+ *       wrapper suffix.
+ */
+function theme_html_tag($variables) {
+  $element = $variables['element'];
+  $attributes = isset($element['#attributes']) ? drupal_attributes($element['#attributes']) : '';
+  if (!isset($element['#value'])) {
+    return '<' . $element['#tag'] . $attributes . " />\n";
+  }
+  else {
+    $output = '<' . $element['#tag'] . $attributes . '>';
+    if (isset($element['#value_prefix'])) {
+      $output .= $element['#value_prefix'];
+    }
+    $output .= $element['#value'];
+    if (isset($element['#value_suffix'])) {
+      $output .= $element['#value_suffix'];
+    }
+    $output .= '</' . $element['#tag'] . ">\n";
+    return $output;
+  }
+}
+
+/**
+ * Returns HTML for a "more" link, like those used in blocks.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - url: The URL of the main page.
+ *   - title: A descriptive verb for the link, like 'Read more'.
+ */
+function theme_more_link($variables) {
+  return '<div class="more-link">' . l(t('More'), $variables['url'], array('attributes' => array('title' => $variables['title']))) . '</div>';
+}
+
+/**
+ * Returns HTML for a username, potentially linked to the user's page.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - account: The user object to format.
+ *   - name: The user's name, sanitized.
+ *   - extra: Additional text to append to the user's name, sanitized.
+ *   - link_path: The path or URL of the user's profile page, home page, or
+ *     other desired page to link to for more information about the user.
+ *   - link_options: An array of options to pass to the l() function's $options
+ *     parameter if linking the user's name to the user's page.
+ *   - attributes_array: An array of attributes to pass to the
+ *     drupal_attributes() function if not linking to the user's page.
+ *
+ * @see template_preprocess_username()
+ * @see template_process_username()
+ */
+function theme_username($variables) {
+  if (isset($variables['link_path'])) {
+    // We have a link path, so we should generate a link using l().
+    // Additional classes may be added as array elements like
+    // $variables['link_options']['attributes']['class'][] = 'myclass';
+    $output = l($variables['name'] . $variables['extra'], $variables['link_path'], $variables['link_options']);
+  }
+  else {
+    // Modules may have added important attributes so they must be included
+    // in the output. Additional classes may be added as array elements like
+    // $variables['attributes_array']['class'][] = 'myclass';
+    $output = '<span' . drupal_attributes($variables['attributes_array']) . '>' . $variables['name'] . $variables['extra'] . '</span>';
+  }
+  return $output;
+}
+
+/**
+ * Returns HTML for a progress bar.
+ *
+ * Note that the core Batch API uses this only for non-JavaScript batch jobs.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - percent: The percentage of the progress.
+ *   - message: A string containing information to be displayed.
+ */
+function theme_progress_bar($variables) {
+  $output = '<div id="progress" class="progress">';
+  $output .= '<div class="bar"><div class="filled" style="width: ' . $variables['percent'] . '%"></div></div>';
+  $output .= '<div class="percentage">' . $variables['percent'] . '%</div>';
+  $output .= '<div class="message">' . $variables['message'] . '</div>';
+  $output .= '</div>';
+
+  return $output;
+}
+
+/**
+ * Returns HTML for an indentation div; used for drag and drop tables.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - size: Optional. The number of indentations to create.
+ */
+function theme_indentation($variables) {
+  $output = '';
+  for ($n = 0; $n < $variables['size']; $n++) {
+    $output .= '<div class="indentation">&nbsp;</div>';
+  }
+  return $output;
+}
+
+/**
+ * @} End of "addtogroup themeable".
+ */
+
+/**
+ * Returns HTML output for a single table cell for theme_table().
+ *
+ * @param $cell
+ *   Array of cell information, or string to display in cell.
+ * @param bool $header
+ *   TRUE if this cell is a table header cell, FALSE if it is an ordinary
+ *   table cell. If $cell is an array with element 'header' set to TRUE, that
+ *   will override the $header parameter.
+ *
+ * @return
+ *   HTML for the cell.
+ */
+function _theme_table_cell($cell, $header = FALSE) {
+  $attributes = '';
+
+  if (is_array($cell)) {
+    $data = isset($cell['data']) ? $cell['data'] : '';
+    // Cell's data property can be a string or a renderable array.
+    if (is_array($data)) {
+      $data = drupal_render($data);
+    }
+    $header |= isset($cell['header']);
+    unset($cell['data']);
+    unset($cell['header']);
+    $attributes = drupal_attributes($cell);
+  }
+  else {
+    $data = $cell;
+  }
+
+  if ($header) {
+    $output = "<th$attributes>$data</th>";
+  }
+  else {
+    $output = "<td$attributes>$data</td>";
+  }
+
+  return $output;
+}
+
+/**
+ * Adds a default set of helper variables for variable processors and templates.
+ *
+ * This function is called for theme hooks implemented as templates only, not
+ * for theme hooks implemented as functions. This preprocess function is the
+ * first in the sequence of preprocessing and processing functions that is
+ * called when preparing variables for a template. See theme() for more details
+ * about the full sequence.
+ *
+ * @see theme()
+ * @see template_process()
+ */
+function template_preprocess(&$variables, $hook) {
+  global $user;
+  static $count = array();
+
+  // Track run count for each hook to provide zebra striping. See
+  // "template_preprocess_block()" which provides the same feature specific to
+  // blocks.
+  $count[$hook] = isset($count[$hook]) && is_int($count[$hook]) ? $count[$hook] : 1;
+  $variables['zebra'] = ($count[$hook] % 2) ? 'odd' : 'even';
+  $variables['id'] = $count[$hook]++;
+
+  // Tell all templates where they are located.
+  $variables['directory'] = path_to_theme();
+
+  // Initialize html class attribute for the current hook.
+  $variables['classes_array'] = array(drupal_html_class($hook));
+
+  // Merge in variables that don't depend on hook and don't change during a
+  // single page request.
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['default_variables'] = &drupal_static(__FUNCTION__);
+  }
+  $default_variables = &$drupal_static_fast['default_variables'];
+  // Global $user object shouldn't change during a page request once rendering
+  // has started, but if there's an edge case where it does, re-fetch the
+  // variables appropriate for the new user.
+  if (!isset($default_variables) || ($user !== $default_variables['user'])) {
+    $default_variables = _template_preprocess_default_variables();
+  }
+  $variables += $default_variables;
+}
+
+/**
+ * Returns hook-independent variables to template_preprocess().
+ */
+function _template_preprocess_default_variables() {
+  global $user;
+
+  // Variables that don't depend on a database connection.
+  $variables = array(
+    'attributes_array' => array(),
+    'title_attributes_array' => array(),
+    'content_attributes_array' => array(),
+    'title_prefix' => array(),
+    'title_suffix' => array(),
+    'user' => $user,
+    'db_is_active' => !defined('MAINTENANCE_MODE'),
+    'is_admin' => FALSE,
+    'logged_in' => FALSE,
+  );
+
+  // The user object has no uid property when the database does not exist during
+  // install. The user_access() check deals with issues when in maintenance mode
+  // as uid is set but the user.module has not been included.
+  if (isset($user->uid) && function_exists('user_access')) {
+    $variables['is_admin'] = user_access('access administration pages');
+    $variables['logged_in'] = ($user->uid > 0);
+  }
+
+  // drupal_is_front_page() might throw an exception.
+  try {
+    $variables['is_front'] = drupal_is_front_page();
+  }
+  catch (Exception $e) {
+    // If the database is not yet available, set default values for these
+    // variables.
+    $variables['is_front'] = FALSE;
+    $variables['db_is_active'] = FALSE;
+  }
+
+  return $variables;
+}
+
+/**
+ * Adds helper variables derived from variables defined during preprocessing.
+ *
+ * When preparing variables for a theme hook implementation, all 'preprocess'
+ * functions run first, then all 'process' functions (see theme() for details
+ * about the full sequence).
+ *
+ * This function serializes array variables manipulated during the preprocessing
+ * phase into strings for convenient use by templates. As with
+ * template_preprocess(), this function does not get called for theme hooks
+ * implemented as functions.
+ *
+ * @see theme()
+ * @see template_preprocess()
+ */
+function template_process(&$variables, $hook) {
+  // Flatten out classes.
+  $variables['classes'] = implode(' ', $variables['classes_array']);
+
+  // Flatten out attributes, title_attributes, and content_attributes.
+  // Because this function can be called very often, and often with empty
+  // attributes, optimize performance by only calling drupal_attributes() if
+  // necessary.
+  $variables['attributes'] = $variables['attributes_array'] ? drupal_attributes($variables['attributes_array']) : '';
+  $variables['title_attributes'] = $variables['title_attributes_array'] ? drupal_attributes($variables['title_attributes_array']) : '';
+  $variables['content_attributes'] = $variables['content_attributes_array'] ? drupal_attributes($variables['content_attributes_array']) : '';
+}
+
+/**
+ * Preprocess variables for html.tpl.php
+ *
+ * @see system_elements()
+ * @see html.tpl.php
+ */
+function template_preprocess_html(&$variables) {
+  // Compile a list of classes that are going to be applied to the body element.
+  // This allows advanced theming based on context (home page, node of certain type, etc.).
+  // Add a class that tells us whether we're on the front page or not.
+  $variables['classes_array'][] = $variables['is_front'] ? 'front' : 'not-front';
+  // Add a class that tells us whether the page is viewed by an authenticated user or not.
+  $variables['classes_array'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in';
+
+  // Add information about the number of sidebars.
+  if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) {
+    $variables['classes_array'][] = 'two-sidebars';
+  }
+  elseif (!empty($variables['page']['sidebar_first'])) {
+    $variables['classes_array'][] = 'one-sidebar sidebar-first';
+  }
+  elseif (!empty($variables['page']['sidebar_second'])) {
+    $variables['classes_array'][] = 'one-sidebar sidebar-second';
+  }
+  else {
+    $variables['classes_array'][] = 'no-sidebars';
+  }
+
+  // Populate the body classes.
+  if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) {
+    foreach ($suggestions as $suggestion) {
+      if ($suggestion != 'page-front') {
+        // Add current suggestion to page classes to make it possible to theme
+        // the page depending on the current page type (e.g. node, admin, user,
+        // etc.) as well as more specific data like node-12 or node-edit.
+        $variables['classes_array'][] = drupal_html_class($suggestion);
+      }
+    }
+  }
+
+  // If on an individual node page, add the node type to body classes.
+  if ($node = menu_get_object()) {
+    $variables['classes_array'][] = drupal_html_class('node-type-' . $node->type);
+  }
+
+  // RDFa allows annotation of XHTML pages with RDF data, while GRDDL provides
+  // mechanisms for extraction of this RDF content via XSLT transformation
+  // using an associated GRDDL profile.
+  $variables['rdf_namespaces']    = drupal_get_rdf_namespaces();
+  $variables['grddl_profile']     = 'http://www.w3.org/1999/xhtml/vocab';
+  $variables['language']          = $GLOBALS['language'];
+  $variables['language']->dir     = $GLOBALS['language']->direction ? 'rtl' : 'ltr';
+
+  // Add favicon.
+  if (theme_get_setting('toggle_favicon')) {
+    $favicon = theme_get_setting('favicon');
+    $type = theme_get_setting('favicon_mimetype');
+    drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => drupal_strip_dangerous_protocols($favicon), 'type' => $type));
+  }
+
+  // Construct page title.
+  if (drupal_get_title()) {
+    $head_title = array(
+      'title' => strip_tags(drupal_get_title()),
+      'name' => check_plain(variable_get('site_name', 'Drupal')),
+    );
+  }
+  else {
+    $head_title = array('name' => check_plain(variable_get('site_name', 'Drupal')));
+    if (variable_get('site_slogan', '')) {
+      $head_title['slogan'] = filter_xss_admin(variable_get('site_slogan', ''));
+    }
+  }
+  $variables['head_title_array'] = $head_title;
+  $variables['head_title'] = implode(' | ', $head_title);
+
+  // Populate the page template suggestions.
+  if ($suggestions = theme_get_suggestions(arg(), 'html')) {
+    $variables['theme_hook_suggestions'] = $suggestions;
+  }
+}
+
+/**
+ * Preprocess variables for page.tpl.php
+ *
+ * Most themes utilize their own copy of page.tpl.php. The default is located
+ * inside "modules/system/page.tpl.php". Look in there for the full list of
+ * variables.
+ *
+ * Uses the arg() function to generate a series of page template suggestions
+ * based on the current path.
+ *
+ * Any changes to variables in this preprocessor should also be changed inside
+ * template_preprocess_maintenance_page() to keep all of them consistent.
+ *
+ * @see drupal_render_page()
+ * @see template_process_page()
+ * @see page.tpl.php
+ */
+function template_preprocess_page(&$variables) {
+  // Move some variables to the top level for themer convenience and template cleanliness.
+  $variables['show_messages'] = $variables['page']['#show_messages'];
+
+  foreach (system_region_list($GLOBALS['theme']) as $region_key => $region_name) {
+    if (!isset($variables['page'][$region_key])) {
+      $variables['page'][$region_key] = array();
+    }
+  }
+
+  // Set up layout variable.
+  $variables['layout'] = 'none';
+  if (!empty($variables['page']['sidebar_first'])) {
+    $variables['layout'] = 'first';
+  }
+  if (!empty($variables['page']['sidebar_second'])) {
+    $variables['layout'] = ($variables['layout'] == 'first') ? 'both' : 'second';
+  }
+
+  $variables['base_path']         = base_path();
+  $variables['front_page']        = url();
+  $variables['feed_icons']        = drupal_get_feeds();
+  $variables['language']          = $GLOBALS['language'];
+  $variables['language']->dir     = $GLOBALS['language']->direction ? 'rtl' : 'ltr';
+  $variables['logo']              = theme_get_setting('logo');
+  $variables['main_menu']         = theme_get_setting('toggle_main_menu') ? menu_main_menu() : array();
+  $variables['secondary_menu']    = theme_get_setting('toggle_secondary_menu') ? menu_secondary_menu() : array();
+  $variables['action_links']      = menu_local_actions();
+  $variables['site_name']         = (theme_get_setting('toggle_name') ? filter_xss_admin(variable_get('site_name', 'Drupal')) : '');
+  $variables['site_slogan']       = (theme_get_setting('toggle_slogan') ? filter_xss_admin(variable_get('site_slogan', '')) : '');
+  $variables['tabs']              = menu_local_tabs();
+
+  if ($node = menu_get_object()) {
+    $variables['node'] = $node;
+  }
+
+  // Populate the page template suggestions.
+  if ($suggestions = theme_get_suggestions(arg(), 'page')) {
+    $variables['theme_hook_suggestions'] = $suggestions;
+  }
+}
+
+/**
+ * Process variables for page.tpl.php
+ *
+ * Perform final addition of variables before passing them into the template.
+ * To customize these variables, simply set them in an earlier step.
+ *
+ * @see template_preprocess_page()
+ * @see page.tpl.php
+ */
+function template_process_page(&$variables) {
+  if (!isset($variables['breadcrumb'])) {
+    // Build the breadcrumb last, so as to increase the chance of being able to
+    // re-use the cache of an already rendered menu containing the active link
+    // for the current page.
+    // @see menu_tree_page_data()
+    $variables['breadcrumb'] = theme('breadcrumb', array('breadcrumb' => drupal_get_breadcrumb()));
+  }
+  if (!isset($variables['title'])) {
+    $variables['title'] = drupal_get_title();
+  }
+
+  // Generate messages last in order to capture as many as possible for the
+  // current page.
+  if (!isset($variables['messages'])) {
+    $variables['messages'] = $variables['show_messages'] ? theme('status_messages') : '';
+  }
+}
+
+/**
+ * Process variables for html.tpl.php
+ *
+ * Perform final addition and modification of variables before passing into
+ * the template. To customize these variables, call drupal_render() on elements
+ * in $variables['page'] during THEME_preprocess_page().
+ *
+ * @see template_preprocess_html()
+ * @see html.tpl.php
+ */
+function template_process_html(&$variables) {
+  // Render page_top and page_bottom into top level variables.
+  $variables['page_top'] = drupal_render($variables['page']['page_top']);
+  $variables['page_bottom'] = drupal_render($variables['page']['page_bottom']);
+  // Place the rendered HTML for the page body into a top level variable.
+  $variables['page']              = $variables['page']['#children'];
+  $variables['page_bottom'] .= drupal_get_js('footer');
+
+  $variables['head']    = drupal_get_html_head();
+  $variables['css']     = drupal_add_css();
+  $variables['styles']  = drupal_get_css();
+  $variables['scripts'] = drupal_get_js();
+}
+
+/**
+ * Generate an array of suggestions from path arguments.
+ *
+ * This is typically called for adding to the 'theme_hook_suggestions' or
+ * 'classes_array' variables from within preprocess functions, when wanting to
+ * base the additional suggestions on the path of the current page.
+ *
+ * @param $args
+ *   An array of path arguments, such as from function arg().
+ * @param $base
+ *   A string identifying the base 'thing' from which more specific suggestions
+ *   are derived. For example, 'page' or 'html'.
+ * @param $delimiter
+ *   The string used to delimit increasingly specific information. The default
+ *   of '__' is appropriate for theme hook suggestions. '-' is appropriate for
+ *   extra classes.
+ *
+ * @return
+ *   An array of suggestions, suitable for adding to
+ *   $variables['theme_hook_suggestions'] within a preprocess function or to
+ *   $variables['classes_array'] if the suggestions represent extra CSS classes.
+ */
+function theme_get_suggestions($args, $base, $delimiter = '__') {
+
+  // Build a list of suggested theme hooks or body classes in order of
+  // specificity. One suggestion is made for every element of the current path,
+  // though numeric elements are not carried to subsequent suggestions. For
+  // example, for $base='page', http://www.example.com/node/1/edit would result
+  // in the following suggestions and body classes:
+  //
+  // page__node              page-node
+  // page__node__%           page-node-%
+  // page__node__1           page-node-1
+  // page__node__edit        page-node-edit
+
+  $suggestions = array();
+  $prefix = $base;
+  foreach ($args as $arg) {
+    // Remove slashes or null per SA-CORE-2009-003 and change - (hyphen) to _
+    // (underscore).
+    //
+    // When we discover templates in @see drupal_find_theme_templates,
+    // hyphens (-) are converted to underscores (_) before the theme hook
+    // is registered. We do this because the hyphens used for delimiters
+    // in hook suggestions cannot be used in the function names of the
+    // associated preprocess functions. Any page templates designed to be used
+    // on paths that contain a hyphen are also registered with these hyphens
+    // converted to underscores so here we must convert any hyphens in path
+    // arguments to underscores here before fetching theme hook suggestions
+    // to ensure the templates are appropriately recognized.
+    $arg = str_replace(array("/", "\\", "\0", '-'), array('', '', '', '_'), $arg);
+    // The percent acts as a wildcard for numeric arguments since
+    // asterisks are not valid filename characters on many filesystems.
+    if (is_numeric($arg)) {
+      $suggestions[] = $prefix . $delimiter . '%';
+    }
+    $suggestions[] = $prefix . $delimiter . $arg;
+    if (!is_numeric($arg)) {
+      $prefix .= $delimiter . $arg;
+    }
+  }
+  if (drupal_is_front_page()) {
+    // Front templates should be based on root only, not prefixed arguments.
+    $suggestions[] = $base . $delimiter . 'front';
+  }
+
+  return $suggestions;
+}
+
+/**
+ * Process variables for maintenance-page.tpl.php.
+ *
+ * The variables array generated here is a mirror of
+ * template_preprocess_page(). This preprocessor will run its course when
+ * theme_maintenance_page() is invoked. An alternate template file of
+ * maintenance-page--offline.tpl.php can be used when the database is offline to
+ * hide errors and completely replace the content.
+ *
+ * The $variables array contains the following arguments:
+ * - $content
+ *
+ * @see maintenance-page.tpl.php
+ */
+function template_preprocess_maintenance_page(&$variables) {
+  // Add favicon
+  if (theme_get_setting('toggle_favicon')) {
+    $favicon = theme_get_setting('favicon');
+    $type = theme_get_setting('favicon_mimetype');
+    drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => drupal_strip_dangerous_protocols($favicon), 'type' => $type));
+  }
+
+  global $theme;
+  // Retrieve the theme data to list all available regions.
+  $theme_data = list_themes();
+  $regions = $theme_data[$theme]->info['regions'];
+
+  // Get all region content set with drupal_add_region_content().
+  foreach (array_keys($regions) as $region) {
+    // Assign region to a region variable.
+    $region_content = drupal_get_region_content($region);
+    isset($variables[$region]) ? $variables[$region] .= $region_content : $variables[$region] = $region_content;
+  }
+
+  // Setup layout variable.
+  $variables['layout'] = 'none';
+  if (!empty($variables['sidebar_first'])) {
+    $variables['layout'] = 'first';
+  }
+  if (!empty($variables['sidebar_second'])) {
+    $variables['layout'] = ($variables['layout'] == 'first') ? 'both' : 'second';
+  }
+
+  // Construct page title
+  if (drupal_get_title()) {
+    $head_title = array(
+      'title' => strip_tags(drupal_get_title()),
+      'name' => variable_get('site_name', 'Drupal'),
+    );
+  }
+  else {
+    $head_title = array('name' => variable_get('site_name', 'Drupal'));
+    if (variable_get('site_slogan', '')) {
+      $head_title['slogan'] = variable_get('site_slogan', '');
+    }
+  }
+
+  // set the default language if necessary
+  $language = isset($GLOBALS['language']) ? $GLOBALS['language'] : language_default();
+
+  $variables['head_title_array']  = $head_title;
+  $variables['head_title']        = implode(' | ', $head_title);
+  $variables['base_path']         = base_path();
+  $variables['front_page']        = url();
+  $variables['breadcrumb']        = '';
+  $variables['feed_icons']        = '';
+  $variables['help']              = '';
+  $variables['language']          = $language;
+  $variables['language']->dir     = $language->direction ? 'rtl' : 'ltr';
+  $variables['logo']              = theme_get_setting('logo');
+  $variables['messages']          = $variables['show_messages'] ? theme('status_messages') : '';
+  $variables['main_menu']         = array();
+  $variables['secondary_menu']    = array();
+  $variables['site_name']         = (theme_get_setting('toggle_name') ? variable_get('site_name', 'Drupal') : '');
+  $variables['site_slogan']       = (theme_get_setting('toggle_slogan') ? variable_get('site_slogan', '') : '');
+  $variables['tabs']              = '';
+  $variables['title']             = drupal_get_title();
+
+  // Compile a list of classes that are going to be applied to the body element.
+  $variables['classes_array'][] = 'in-maintenance';
+  if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
+    $variables['classes_array'][] = 'db-offline';
+  }
+  if ($variables['layout'] == 'both') {
+    $variables['classes_array'][] = 'two-sidebars';
+  }
+  elseif ($variables['layout'] == 'none') {
+    $variables['classes_array'][] = 'no-sidebars';
+  }
+  else {
+    $variables['classes_array'][] = 'one-sidebar sidebar-' . $variables['layout'];
+  }
+
+  // Dead databases will show error messages so supplying this template will
+  // allow themers to override the page and the content completely.
+  if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
+    $variables['theme_hook_suggestion'] = 'maintenance_page__offline';
+  }
+}
+
+/**
+ * Theme process function for theme_maintenance_field().
+ *
+ * The variables array generated here is a mirror of template_process_html().
+ * This processor will run its course when theme_maintenance_page() is invoked.
+ *
+ * @see maintenance-page.tpl.php
+ * @see template_process_html()
+ */
+function template_process_maintenance_page(&$variables) {
+  $variables['head']    = drupal_get_html_head();
+  $variables['css']     = drupal_add_css();
+  $variables['styles']  = drupal_get_css();
+  $variables['scripts'] = drupal_get_js();
+}
+
+/**
+ * Preprocess variables for region.tpl.php
+ *
+ * Prepares the values passed to the theme_region function to be passed into a
+ * pluggable template engine. Uses the region name to generate a template file
+ * suggestions. If none are found, the default region.tpl.php is used.
+ *
+ * @see drupal_region_class()
+ * @see region.tpl.php
+ */
+function template_preprocess_region(&$variables) {
+  // Create the $content variable that templates expect.
+  $variables['content'] = $variables['elements']['#children'];
+  $variables['region'] = $variables['elements']['#region'];
+
+  $variables['classes_array'][] = drupal_region_class($variables['region']);
+  $variables['theme_hook_suggestions'][] = 'region__' . $variables['region'];
+}
+
+/**
+ * Preprocesses variables for theme_username().
+ *
+ * Modules that make any changes to variables like 'name' or 'extra' must insure
+ * that the final string is safe to include directly in the output by using
+ * check_plain() or filter_xss().
+ *
+ * @see template_process_username()
+ */
+function template_preprocess_username(&$variables) {
+  $account = $variables['account'];
+
+  $variables['extra'] = '';
+  if (empty($account->uid)) {
+   $variables['uid'] = 0;
+   if (theme_get_setting('toggle_comment_user_verification')) {
+     $variables['extra'] = ' (' . t('not verified') . ')';
+   }
+  }
+  else {
+    $variables['uid'] = (int) $account->uid;
+  }
+
+  // Set the name to a formatted name that is safe for printing and
+  // that won't break tables by being too long. Keep an unshortened,
+  // unsanitized version, in case other preprocess functions want to implement
+  // their own shortening logic or add markup. If they do so, they must ensure
+  // that $variables['name'] is safe for printing.
+  $name = $variables['name_raw'] = format_username($account);
+  if (drupal_strlen($name) > 20) {
+    $name = drupal_substr($name, 0, 15) . '...';
+  }
+  $variables['name'] = check_plain($name);
+
+  $variables['profile_access'] = user_access('access user profiles');
+  $variables['link_attributes'] = array();
+  // Populate link path and attributes if appropriate.
+  if ($variables['uid'] && $variables['profile_access']) {
+    // We are linking to a local user.
+    $variables['link_attributes'] = array('title' => t('View user profile.'));
+    $variables['link_path'] = 'user/' . $variables['uid'];
+  }
+  elseif (!empty($account->homepage)) {
+    // Like the 'class' attribute, the 'rel' attribute can hold a
+    // space-separated set of values, so initialize it as an array to make it
+    // easier for other preprocess functions to append to it.
+    $variables['link_attributes'] = array('rel' => array('nofollow'));
+    $variables['link_path'] = $account->homepage;
+    $variables['homepage'] = $account->homepage;
+  }
+  // We do not want the l() function to check_plain() a second time.
+  $variables['link_options']['html'] = TRUE;
+  // Set a default class.
+  $variables['attributes_array'] = array('class' => array('username'));
+}
+
+/**
+ * Processes variables for theme_username().
+ *
+ * @see template_preprocess_username()
+ */
+function template_process_username(&$variables) {
+  // Finalize the link_options array for passing to the l() function.
+  // This is done in the process phase so that attributes may be added by
+  // modules or the theme during the preprocess phase.
+  if (isset($variables['link_path'])) {
+    // $variables['attributes_array'] contains attributes that should be applied
+    // regardless of whether a link is being rendered or not.
+    // $variables['link_attributes'] contains attributes that should only be
+    // applied if a link is being rendered. Preprocess functions are encouraged
+    // to use the former unless they want to add attributes on the link only.
+    // If a link is being rendered, these need to be merged. Some attributes are
+    // themselves arrays, so the merging needs to be recursive.
+    $variables['link_options']['attributes'] = array_merge_recursive($variables['link_attributes'], $variables['attributes_array']);
+  }
+}

+ 211 - 0
includes/theme.maintenance.inc

@@ -0,0 +1,211 @@
+<?php
+
+/**
+ * @file
+ * Theming for maintenance pages.
+ */
+
+/**
+ * Sets up the theming system for maintenance page.
+ *
+ * Used for site installs, updates and when the site is in maintenance mode.
+ * It also applies when the database is unavailable or bootstrap was not
+ * complete. Seven is always used for the initial install and update
+ * operations. In other cases, Bartik is used, but this can be overridden by
+ * setting a "maintenance_theme" key in the $conf variable in settings.php.
+ */
+function _drupal_maintenance_theme() {
+  global $theme, $theme_key, $conf;
+
+  // If $theme is already set, assume the others are set too, and do nothing.
+  if (isset($theme)) {
+    return;
+  }
+
+  require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
+  require_once DRUPAL_ROOT . '/includes/theme.inc';
+  require_once DRUPAL_ROOT . '/includes/common.inc';
+  require_once DRUPAL_ROOT . '/includes/unicode.inc';
+  require_once DRUPAL_ROOT . '/includes/file.inc';
+  require_once DRUPAL_ROOT . '/includes/module.inc';
+  unicode_check();
+
+  // Install and update pages are treated differently to prevent theming overrides.
+  if (defined('MAINTENANCE_MODE') && (MAINTENANCE_MODE == 'install' || MAINTENANCE_MODE == 'update')) {
+    $custom_theme = (isset($conf['maintenance_theme']) ? $conf['maintenance_theme'] : 'seven');
+  }
+  else {
+    // The bootstrap was not complete. So we are operating in a crippled
+    // environment, we need to bootstrap just enough to allow hook invocations
+    // to work. See _drupal_log_error().
+    if (!class_exists('Database', FALSE)) {
+      require_once DRUPAL_ROOT . '/includes/database/database.inc';
+    }
+
+    // We use the default theme as the maintenance theme. If a default theme
+    // isn't specified in the database or in settings.php, we use Bartik.
+    $custom_theme = variable_get('maintenance_theme', variable_get('theme_default', 'bartik'));
+  }
+
+  // Ensure that system.module is loaded.
+  if (!function_exists('_system_rebuild_theme_data')) {
+    $module_list['system']['filename'] = 'modules/system/system.module';
+    module_list(TRUE, FALSE, FALSE, $module_list);
+    drupal_load('module', 'system');
+  }
+
+  $themes = list_themes();
+
+  // list_themes() triggers a drupal_alter() in maintenance mode, but we can't
+  // let themes alter the .info data until we know a theme's base themes. So
+  // don't set global $theme until after list_themes() builds its cache.
+  $theme = $custom_theme;
+
+  // Store the identifier for retrieving theme settings with.
+  $theme_key = $theme;
+
+  // Find all our ancestor themes and put them in an array.
+  $base_theme = array();
+  $ancestor = $theme;
+  while ($ancestor && isset($themes[$ancestor]->base_theme)) {
+    $base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_theme];
+    $ancestor = $themes[$ancestor]->base_theme;
+  }
+  _drupal_theme_initialize($themes[$theme], array_reverse($base_theme), '_theme_load_offline_registry');
+
+  // These are usually added from system_init() -except maintenance.css.
+  // When the database is inactive it's not called so we add it here.
+  $path = drupal_get_path('module', 'system');
+  drupal_add_css($path . '/system.base.css');
+  drupal_add_css($path . '/system.admin.css');
+  drupal_add_css($path . '/system.menus.css');
+  drupal_add_css($path . '/system.messages.css');
+  drupal_add_css($path . '/system.theme.css');
+  drupal_add_css($path . '/system.maintenance.css');
+}
+
+/**
+ * Builds the registry when the site needs to bypass any database calls.
+ */
+function _theme_load_offline_registry($theme, $base_theme = NULL, $theme_engine = NULL) {
+  return _theme_build_registry($theme, $base_theme, $theme_engine);
+}
+
+/**
+ * Returns HTML for a list of maintenance tasks to perform.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - items: An associative array of maintenance tasks.
+ *   - active: The key for the currently active maintenance task.
+ *
+ * @ingroup themeable
+ */
+function theme_task_list($variables) {
+  $items = $variables['items'];
+  $active = $variables['active'];
+
+  $done = isset($items[$active]) || $active == NULL;
+  $output = '<h2 class="element-invisible">Installation tasks</h2>';
+  $output .= '<ol class="task-list">';
+
+  foreach ($items as $k => $item) {
+    if ($active == $k) {
+      $class = 'active';
+      $status = '(' . t('active') . ')';
+      $done = FALSE;
+    }
+    else {
+      $class = $done ? 'done' : '';
+      $status = $done ? '(' . t('done') . ')' : '';
+    }
+    $output .= '<li';
+    $output .= ($class ? ' class="' . $class . '"' : '') . '>';
+    $output .= $item;
+    $output .= ($status ? '<span class="element-invisible">' . $status . '</span>' : '');
+    $output .= '</li>';
+  }
+  $output .= '</ol>';
+  return $output;
+}
+
+/**
+ * Returns HTML for the installation page.
+ *
+ * Note: this function is not themeable.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - content: The page content to show.
+ */
+function theme_install_page($variables) {
+  drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
+  return theme('maintenance_page', $variables);
+}
+
+/**
+ * Returns HTML for the update page.
+ *
+ * Note: this function is not themeable.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - content: The page content to show.
+ *   - show_messages: Whether to output status and error messages.
+ *     FALSE can be useful to postpone the messages to a subsequent page.
+ */
+function theme_update_page($variables) {
+  drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
+  return theme('maintenance_page', $variables);
+}
+
+/**
+ * Returns HTML for a results report of an operation run by authorize.php.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - messages: An array of result messages.
+ *
+ * @ingroup themeable
+ */
+function theme_authorize_report($variables) {
+  $messages = $variables['messages'];
+  $output = '';
+  if (!empty($messages)) {
+    $output .= '<div id="authorize-results">';
+    foreach ($messages as $heading => $logs) {
+      $items = array();
+      foreach ($logs as $number => $log_message) {
+        if ($number === '#abort') {
+          continue;
+        }
+        $items[] = theme('authorize_message', array('message' => $log_message['message'], 'success' => $log_message['success']));
+      }
+      $output .= theme('item_list',  array('items' => $items, 'title' => $heading));
+    }
+    $output .= '</div>';
+  }
+  return $output;
+}
+
+/**
+ * Returns HTML for a single log message from the authorize.php batch operation.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - message: The log message.
+ *   - success: A boolean indicating failure or success.
+ *
+ * @ingroup themeable
+ */
+function theme_authorize_message($variables) {
+  $message = $variables['message'];
+  $success = $variables['success'];
+  if ($success) {
+    $item = array('data' => $message, 'class' => array('success'));
+  }
+  else {
+    $item = array('data' => '<strong>' . $message . '</strong>', 'class' => array('failure'));
+  }
+  return $item;
+}

+ 264 - 0
includes/token.inc

@@ -0,0 +1,264 @@
+<?php
+
+/**
+ * @file
+ * Drupal placeholder/token replacement system.
+ *
+ * API functions for replacing placeholders in text with meaningful values.
+ *
+ * For example: When configuring automated emails, an administrator enters
+ * standard text for the email. Variables like the title of a node and the date
+ * the email was sent can be entered as placeholders like [node:title] and
+ * [date:short]. When a Drupal module prepares to send the email, it can call
+ * the token_replace() function, passing in the text. The token system will
+ * scan the text for placeholder tokens, give other modules an opportunity to
+ * replace them with meaningful text, then return the final product to the
+ * original module.
+ *
+ * Tokens follow the form: [$type:$name], where $type is a general class of
+ * tokens like 'node', 'user', or 'comment' and $name is the name of a given
+ * placeholder. For example, [node:title] or [node:created:since].
+ *
+ * In addition to raw text containing placeholders, modules may pass in an array
+ * of objects to be used when performing the replacement. The objects should be
+ * keyed by the token type they correspond to. For example:
+ *
+ * @code
+ * // Load a node and a user, then replace tokens in the text.
+ * $text = 'On [date:short], [user:name] read [node:title].';
+ * $node = node_load(1);
+ * $user = user_load(1);
+ *
+ * // [date:...] tokens use the current date automatically.
+ * $data = array('node' => $node, 'user' => $user);
+ * return token_replace($text, $data);
+ * @endcode
+ *
+ * Some tokens may be chained in the form of [$type:$pointer:$name], where $type
+ * is a normal token type, $pointer is a reference to another token type, and
+ * $name is the name of a given placeholder. For example, [node:author:mail]. In
+ * that example, 'author' is a pointer to the 'user' account that created the
+ * node, and 'mail' is a placeholder available for any 'user'.
+ *
+ * @see token_replace()
+ * @see hook_tokens()
+ * @see hook_token_info()
+ */
+
+/**
+ * Replaces all tokens in a given string with appropriate values.
+ *
+ * @param $text
+ *   A string potentially containing replaceable tokens.
+ * @param $data
+ *   (optional) An array of keyed objects. For simple replacement scenarios
+ *   'node', 'user', and others are common keys, with an accompanying node or
+ *   user object being the value. Some token types, like 'site', do not require
+ *   any explicit information from $data and can be replaced even if it is
+ *   empty.
+ * @param $options
+ *   (optional) A keyed array of settings and flags to control the token
+ *   replacement process. Supported options are:
+ *   - language: A language object to be used when generating locale-sensitive
+ *     tokens.
+ *   - callback: A callback function that will be used to post-process the array
+ *     of token replacements after they are generated. For example, a module
+ *     using tokens in a text-only email might provide a callback to strip HTML
+ *     entities from token values before they are inserted into the final text.
+ *   - clear: A boolean flag indicating that tokens should be removed from the
+ *     final text if no replacement value can be generated.
+ *   - sanitize: A boolean flag indicating that tokens should be sanitized for
+ *     display to a web browser. Defaults to TRUE. Developers who set this
+ *     option to FALSE assume responsibility for running filter_xss(),
+ *     check_plain() or other appropriate scrubbing functions before displaying
+ *     data to users.
+ *
+ * @return
+ *   Text with tokens replaced.
+ */
+function token_replace($text, array $data = array(), array $options = array()) {
+  $text_tokens = token_scan($text);
+  if (empty($text_tokens)) {
+    return $text;
+  }
+
+  $replacements = array();
+  foreach ($text_tokens as $type => $tokens) {
+    $replacements += token_generate($type, $tokens, $data, $options);
+    if (!empty($options['clear'])) {
+      $replacements += array_fill_keys($tokens, '');
+    }
+  }
+
+  // Optionally alter the list of replacement values.
+  if (!empty($options['callback']) && function_exists($options['callback'])) {
+    $function = $options['callback'];
+    $function($replacements, $data, $options);
+  }
+
+  $tokens = array_keys($replacements);
+  $values = array_values($replacements);
+
+  return str_replace($tokens, $values, $text);
+}
+
+/**
+ * Builds a list of all token-like patterns that appear in the text.
+ *
+ * @param $text
+ *   The text to be scanned for possible tokens.
+ *
+ * @return
+ *   An associative array of discovered tokens, grouped by type.
+ */
+function token_scan($text) {
+  // Matches tokens with the following pattern: [$type:$name]
+  // $type and $name may not contain  [ ] characters.
+  // $type may not contain : or whitespace characters, but $name may.
+  preg_match_all('/
+    \[             # [ - pattern start
+    ([^\s\[\]:]*)  # match $type not containing whitespace : [ or ]
+    :              # : - separator
+    ([^\[\]]*)     # match $name not containing [ or ]
+    \]             # ] - pattern end
+    /x', $text, $matches);
+
+  $types = $matches[1];
+  $tokens = $matches[2];
+
+  // Iterate through the matches, building an associative array containing
+  // $tokens grouped by $types, pointing to the version of the token found in
+  // the source text. For example, $results['node']['title'] = '[node:title]';
+  $results = array();
+  for ($i = 0; $i < count($tokens); $i++) {
+    $results[$types[$i]][$tokens[$i]] = $matches[0][$i];
+  }
+
+  return $results;
+}
+
+/**
+ * Generates replacement values for a list of tokens.
+ *
+ * @param $type
+ *   The type of token being replaced. 'node', 'user', and 'date' are common.
+ * @param $tokens
+ *   An array of tokens to be replaced, keyed by the literal text of the token
+ *   as it appeared in the source text.
+ * @param $data
+ *   (optional) An array of keyed objects. For simple replacement scenarios
+ *   'node', 'user', and others are common keys, with an accompanying node or
+ *   user object being the value. Some token types, like 'site', do not require
+ *   any explicit information from $data and can be replaced even if it is
+ *   empty.
+ * @param $options
+ *   (optional) A keyed array of settings and flags to control the token
+ *   replacement process. Supported options are:
+ *   - language: A language object to be used when generating locale-sensitive
+ *     tokens.
+ *   - callback: A callback function that will be used to post-process the
+ *     array of token replacements after they are generated. Can be used when
+ *     modules require special formatting of token text, for example URL
+ *     encoding or truncation to a specific length.
+ *   - sanitize: A boolean flag indicating that tokens should be sanitized for
+ *     display to a web browser. Developers who set this option to FALSE assume
+ *     responsibility for running filter_xss(), check_plain() or other
+ *     appropriate scrubbing functions before displaying data to users.
+ *
+ * @return
+ *   An associative array of replacement values, keyed by the original 'raw'
+ *   tokens that were found in the source text. For example:
+ *   $results['[node:title]'] = 'My new node';
+ *
+ * @see hook_tokens()
+ * @see hook_tokens_alter()
+ */
+function token_generate($type, array $tokens, array $data = array(), array $options = array()) {
+  $options += array('sanitize' => TRUE);
+  $replacements = module_invoke_all('tokens', $type, $tokens, $data, $options);
+
+  // Allow other modules to alter the replacements.
+  $context = array(
+    'type' => $type,
+    'tokens' => $tokens,
+    'data' => $data,
+    'options' => $options,
+  );
+  drupal_alter('tokens', $replacements, $context);
+
+  return $replacements;
+}
+
+/**
+ * Returns a list of tokens that begin with a specific prefix.
+ *
+ * Used to extract a group of 'chained' tokens (such as [node:author:name])
+ * from the full list of tokens found in text. For example:
+ * @code
+ *   $data = array(
+ *     'author:name' => '[node:author:name]',
+ *     'title'       => '[node:title]',
+ *     'created'     => '[node:created]',
+ *   );
+ *   $results = token_find_with_prefix($data, 'author');
+ *   $results == array('name' => '[node:author:name]');
+ * @endcode
+ *
+ * @param $tokens
+ *   A keyed array of tokens, and their original raw form in the source text.
+ * @param $prefix
+ *   A textual string to be matched at the beginning of the token.
+ * @param $delimiter
+ *   An optional string containing the character that separates the prefix from
+ *   the rest of the token. Defaults to ':'.
+ *
+ * @return
+ *   An associative array of discovered tokens, with the prefix and delimiter
+ *   stripped from the key.
+ */
+function token_find_with_prefix(array $tokens, $prefix, $delimiter = ':') {
+  $results = array();
+  foreach ($tokens as $token => $raw) {
+    $parts = explode($delimiter, $token, 2);
+    if (count($parts) == 2 && $parts[0] == $prefix) {
+      $results[$parts[1]] = $raw;
+    }
+  }
+  return $results;
+}
+
+/**
+ * Returns metadata describing supported tokens.
+ *
+ * The metadata array contains token type, name, and description data as well
+ * as an optional pointer indicating that the token chains to another set of
+ * tokens.
+ *
+ * For example:
+ * @code
+ *   $data['types']['node'] = array(
+ *     'name' => t('Nodes'),
+ *     'description' => t('Tokens related to node objects.'),
+ *   );
+ *   $data['tokens']['node']['title'] = array(
+ *     'name' => t('Title'),
+ *     'description' => t('The title of the current node.'),
+ *   );
+ *   $data['tokens']['node']['author'] = array(
+ *     'name' => t('Author'),
+ *     'description' => t('The author of the current node.'),
+ *     'type' => 'user',
+ *   );
+ * @endcode
+ *
+ * @return
+ *   An associative array of token information, grouped by token type.
+ */
+function token_info() {
+  $data = &drupal_static(__FUNCTION__);
+  if (!isset($data)) {
+    $data = module_invoke_all('token_info');
+    drupal_alter('token_info', $data);
+  }
+  return $data;
+}

+ 265 - 0
includes/unicode.entities.inc

@@ -0,0 +1,265 @@
+<?php
+
+/**
+ * @file
+ * (X)HTML entities, as defined in HTML 4.01.
+ *
+ * @see http://www.w3.org/TR/html401/sgml/entities.html
+ */
+
+$html_entities = array(
+  '&Aacute;' => 'Á',
+  '&aacute;' => 'á',
+  '&Acirc;' => 'Â',
+  '&acirc;' => 'â',
+  '&acute;' => '´',
+  '&AElig;' => 'Æ',
+  '&aelig;' => 'æ',
+  '&Agrave;' => 'À',
+  '&agrave;' => 'à',
+  '&alefsym;' => 'ℵ',
+  '&Alpha;' => 'Α',
+  '&alpha;' => 'α',
+  '&amp;' => '&',
+  '&and;' => '∧',
+  '&ang;' => '∠',
+  '&Aring;' => 'Å',
+  '&aring;' => 'å',
+  '&asymp;' => '≈',
+  '&Atilde;' => 'Ã',
+  '&atilde;' => 'ã',
+  '&Auml;' => 'Ä',
+  '&auml;' => 'ä',
+  '&bdquo;' => '„',
+  '&Beta;' => 'Β',
+  '&beta;' => 'β',
+  '&brvbar;' => '¦',
+  '&bull;' => '•',
+  '&cap;' => '∩',
+  '&Ccedil;' => 'Ç',
+  '&ccedil;' => 'ç',
+  '&cedil;' => '¸',
+  '&cent;' => '¢',
+  '&Chi;' => 'Χ',
+  '&chi;' => 'χ',
+  '&circ;' => 'ˆ',
+  '&clubs;' => '♣',
+  '&cong;' => '≅',
+  '&copy;' => '©',
+  '&crarr;' => '↵',
+  '&cup;' => '∪',
+  '&curren;' => '¤',
+  '&dagger;' => '†',
+  '&Dagger;' => '‡',
+  '&darr;' => '↓',
+  '&dArr;' => '⇓',
+  '&deg;' => '°',
+  '&Delta;' => 'Δ',
+  '&delta;' => 'δ',
+  '&diams;' => '♦',
+  '&divide;' => '÷',
+  '&Eacute;' => 'É',
+  '&eacute;' => 'é',
+  '&Ecirc;' => 'Ê',
+  '&ecirc;' => 'ê',
+  '&Egrave;' => 'È',
+  '&egrave;' => 'è',
+  '&empty;' => '∅',
+  '&emsp;' => ' ',
+  '&ensp;' => ' ',
+  '&Epsilon;' => 'Ε',
+  '&epsilon;' => 'ε',
+  '&equiv;' => '≡',
+  '&Eta;' => 'Η',
+  '&eta;' => 'η',
+  '&ETH;' => 'Ð',
+  '&eth;' => 'ð',
+  '&Euml;' => 'Ë',
+  '&euml;' => 'ë',
+  '&euro;' => '€',
+  '&exist;' => '∃',
+  '&fnof;' => 'ƒ',
+  '&forall;' => '∀',
+  '&frac12;' => '½',
+  '&frac14;' => '¼',
+  '&frac34;' => '¾',
+  '&frasl;' => '⁄',
+  '&Gamma;' => 'Γ',
+  '&gamma;' => 'γ',
+  '&ge;' => '≥',
+  '&harr;' => '↔',
+  '&hArr;' => '⇔',
+  '&hearts;' => '♥',
+  '&hellip;' => '…',
+  '&Iacute;' => 'Í',
+  '&iacute;' => 'í',
+  '&Icirc;' => 'Î',
+  '&icirc;' => 'î',
+  '&iexcl;' => '¡',
+  '&Igrave;' => 'Ì',
+  '&igrave;' => 'ì',
+  '&image;' => 'ℑ',
+  '&infin;' => '∞',
+  '&int;' => '∫',
+  '&Iota;' => 'Ι',
+  '&iota;' => 'ι',
+  '&iquest;' => '¿',
+  '&isin;' => '∈',
+  '&Iuml;' => 'Ï',
+  '&iuml;' => 'ï',
+  '&Kappa;' => 'Κ',
+  '&kappa;' => 'κ',
+  '&Lambda;' => 'Λ',
+  '&lambda;' => 'λ',
+  '&lang;' => '〈',
+  '&laquo;' => '«',
+  '&larr;' => '←',
+  '&lArr;' => '⇐',
+  '&lceil;' => '⌈',
+  '&ldquo;' => '“',
+  '&le;' => '≤',
+  '&lfloor;' => '⌊',
+  '&lowast;' => '∗',
+  '&loz;' => '◊',
+  '&lrm;' => '‎',
+  '&lsaquo;' => '‹',
+  '&lsquo;' => '‘',
+  '&macr;' => '¯',
+  '&mdash;' => '—',
+  '&micro;' => 'µ',
+  '&middot;' => '·',
+  '&minus;' => '−',
+  '&Mu;' => 'Μ',
+  '&mu;' => 'μ',
+  '&nabla;' => '∇',
+  '&nbsp;' => ' ',
+  '&ndash;' => '–',
+  '&ne;' => '≠',
+  '&ni;' => '∋',
+  '&not;' => '¬',
+  '&notin;' => '∉',
+  '&nsub;' => '⊄',
+  '&Ntilde;' => 'Ñ',
+  '&ntilde;' => 'ñ',
+  '&Nu;' => 'Ν',
+  '&nu;' => 'ν',
+  '&Oacute;' => 'Ó',
+  '&oacute;' => 'ó',
+  '&Ocirc;' => 'Ô',
+  '&ocirc;' => 'ô',
+  '&OElig;' => 'Œ',
+  '&oelig;' => 'œ',
+  '&Ograve;' => 'Ò',
+  '&ograve;' => 'ò',
+  '&oline;' => '‾',
+  '&Omega;' => 'Ω',
+  '&omega;' => 'ω',
+  '&Omicron;' => 'Ο',
+  '&omicron;' => 'ο',
+  '&oplus;' => '⊕',
+  '&or;' => '∨',
+  '&ordf;' => 'ª',
+  '&ordm;' => 'º',
+  '&Oslash;' => 'Ø',
+  '&oslash;' => 'ø',
+  '&Otilde;' => 'Õ',
+  '&otilde;' => 'õ',
+  '&otimes;' => '⊗',
+  '&Ouml;' => 'Ö',
+  '&ouml;' => 'ö',
+  '&para;' => '¶',
+  '&part;' => '∂',
+  '&permil;' => '‰',
+  '&perp;' => '⊥',
+  '&Phi;' => 'Φ',
+  '&phi;' => 'φ',
+  '&Pi;' => 'Π',
+  '&pi;' => 'π',
+  '&piv;' => 'ϖ',
+  '&plusmn;' => '±',
+  '&pound;' => '£',
+  '&prime;' => '′',
+  '&Prime;' => '″',
+  '&prod;' => '∏',
+  '&prop;' => '∝',
+  '&Psi;' => 'Ψ',
+  '&psi;' => 'ψ',
+  '&radic;' => '√',
+  '&rang;' => '〉',
+  '&raquo;' => '»',
+  '&rarr;' => '→',
+  '&rArr;' => '⇒',
+  '&rceil;' => '⌉',
+  '&rdquo;' => '”',
+  '&real;' => 'ℜ',
+  '&reg;' => '®',
+  '&rfloor;' => '⌋',
+  '&Rho;' => 'Ρ',
+  '&rho;' => 'ρ',
+  '&rlm;' => '‏',
+  '&rsaquo;' => '›',
+  '&rsquo;' => '’',
+  '&sbquo;' => '‚',
+  '&Scaron;' => 'Š',
+  '&scaron;' => 'š',
+  '&sdot;' => '⋅',
+  '&sect;' => '§',
+  '&shy;' => '­',
+  '&Sigma;' => 'Σ',
+  '&sigma;' => 'σ',
+  '&sigmaf;' => 'ς',
+  '&sim;' => '∼',
+  '&spades;' => '♠',
+  '&sub;' => '⊂',
+  '&sube;' => '⊆',
+  '&sum;' => '∑',
+  '&sup1;' => '¹',
+  '&sup2;' => '²',
+  '&sup3;' => '³',
+  '&sup;' => '⊃',
+  '&supe;' => '⊇',
+  '&szlig;' => 'ß',
+  '&Tau;' => 'Τ',
+  '&tau;' => 'τ',
+  '&there4;' => '∴',
+  '&Theta;' => 'Θ',
+  '&theta;' => 'θ',
+  '&thetasym;' => 'ϑ',
+  '&thinsp;' => ' ',
+  '&THORN;' => 'Þ',
+  '&thorn;' => 'þ',
+  '&tilde;' => '˜',
+  '&times;' => '×',
+  '&trade;' => '™',
+  '&Uacute;' => 'Ú',
+  '&uacute;' => 'ú',
+  '&uarr;' => '↑',
+  '&uArr;' => '⇑',
+  '&Ucirc;' => 'Û',
+  '&ucirc;' => 'û',
+  '&Ugrave;' => 'Ù',
+  '&ugrave;' => 'ù',
+  '&uml;' => '¨',
+  '&upsih;' => 'ϒ',
+  '&Upsilon;' => 'Υ',
+  '&upsilon;' => 'υ',
+  '&Uuml;' => 'Ü',
+  '&uuml;' => 'ü',
+  '&weierp;' => '℘',
+  '&Xi;' => 'Ξ',
+  '&xi;' => 'ξ',
+  '&Yacute;' => 'Ý',
+  '&yacute;' => 'ý',
+  '&yen;' => '¥',
+  '&yuml;' => 'ÿ',
+  '&Yuml;' => 'Ÿ',
+  '&Zeta;' => 'Ζ',
+  '&zeta;' => 'ζ',
+  '&zwj;' => '‍',
+  '&zwnj;' => '‌',
+  '&gt;' => '>',
+  '&lt;' => '<',
+  '&quot;' => '"',
+  // Add apostrophe (XML).
+  '&apos;' => "'",
+);

+ 672 - 0
includes/unicode.inc

@@ -0,0 +1,672 @@
+<?php
+
+/**
+* @file
+* Provides Unicode-related conversions and operations.
+*/
+
+/**
+ * Indicates an error during check for PHP unicode support.
+ */
+define('UNICODE_ERROR', -1);
+
+/**
+ * Indicates that standard PHP (emulated) unicode support is being used.
+ */
+define('UNICODE_SINGLEBYTE', 0);
+
+/**
+ * Indicates that full unicode support with the PHP mbstring extension is being
+ * used.
+ */
+define('UNICODE_MULTIBYTE', 1);
+
+/**
+ * Matches Unicode characters that are word boundaries.
+ *
+ * Characters with the following General_category (gc) property values are used
+ * as word boundaries. While this does not fully conform to the Word Boundaries
+ * algorithm described in http://unicode.org/reports/tr29, as PCRE does not
+ * contain the Word_Break property table, this simpler algorithm has to do.
+ * - Cc, Cf, Cn, Co, Cs: Other.
+ * - Pc, Pd, Pe, Pf, Pi, Po, Ps: Punctuation.
+ * - Sc, Sk, Sm, So: Symbols.
+ * - Zl, Zp, Zs: Separators.
+ *
+ * Non-boundary characters include the following General_category (gc) property
+ * values:
+ * - Ll, Lm, Lo, Lt, Lu: Letters.
+ * - Mc, Me, Mn: Combining Marks.
+ * - Nd, Nl, No: Numbers.
+ *
+ * Note that the PCRE property matcher is not used because we wanted to be
+ * compatible with Unicode 5.2.0 regardless of the PCRE version used (and any
+ * bugs in PCRE property tables).
+ *
+ * @see http://unicode.org/glossary
+ */
+define('PREG_CLASS_UNICODE_WORD_BOUNDARY',
+  '\x{0}-\x{2F}\x{3A}-\x{40}\x{5B}-\x{60}\x{7B}-\x{A9}\x{AB}-\x{B1}\x{B4}' .
+  '\x{B6}-\x{B8}\x{BB}\x{BF}\x{D7}\x{F7}\x{2C2}-\x{2C5}\x{2D2}-\x{2DF}' .
+  '\x{2E5}-\x{2EB}\x{2ED}\x{2EF}-\x{2FF}\x{375}\x{37E}-\x{385}\x{387}\x{3F6}' .
+  '\x{482}\x{55A}-\x{55F}\x{589}-\x{58A}\x{5BE}\x{5C0}\x{5C3}\x{5C6}' .
+  '\x{5F3}-\x{60F}\x{61B}-\x{61F}\x{66A}-\x{66D}\x{6D4}\x{6DD}\x{6E9}' .
+  '\x{6FD}-\x{6FE}\x{700}-\x{70F}\x{7F6}-\x{7F9}\x{830}-\x{83E}' .
+  '\x{964}-\x{965}\x{970}\x{9F2}-\x{9F3}\x{9FA}-\x{9FB}\x{AF1}\x{B70}' .
+  '\x{BF3}-\x{BFA}\x{C7F}\x{CF1}-\x{CF2}\x{D79}\x{DF4}\x{E3F}\x{E4F}' .
+  '\x{E5A}-\x{E5B}\x{F01}-\x{F17}\x{F1A}-\x{F1F}\x{F34}\x{F36}\x{F38}' .
+  '\x{F3A}-\x{F3D}\x{F85}\x{FBE}-\x{FC5}\x{FC7}-\x{FD8}\x{104A}-\x{104F}' .
+  '\x{109E}-\x{109F}\x{10FB}\x{1360}-\x{1368}\x{1390}-\x{1399}\x{1400}' .
+  '\x{166D}-\x{166E}\x{1680}\x{169B}-\x{169C}\x{16EB}-\x{16ED}' .
+  '\x{1735}-\x{1736}\x{17B4}-\x{17B5}\x{17D4}-\x{17D6}\x{17D8}-\x{17DB}' .
+  '\x{1800}-\x{180A}\x{180E}\x{1940}-\x{1945}\x{19DE}-\x{19FF}' .
+  '\x{1A1E}-\x{1A1F}\x{1AA0}-\x{1AA6}\x{1AA8}-\x{1AAD}\x{1B5A}-\x{1B6A}' .
+  '\x{1B74}-\x{1B7C}\x{1C3B}-\x{1C3F}\x{1C7E}-\x{1C7F}\x{1CD3}\x{1FBD}' .
+  '\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}' .
+  '\x{1FFD}-\x{206F}\x{207A}-\x{207E}\x{208A}-\x{208E}\x{20A0}-\x{20B8}' .
+  '\x{2100}-\x{2101}\x{2103}-\x{2106}\x{2108}-\x{2109}\x{2114}' .
+  '\x{2116}-\x{2118}\x{211E}-\x{2123}\x{2125}\x{2127}\x{2129}\x{212E}' .
+  '\x{213A}-\x{213B}\x{2140}-\x{2144}\x{214A}-\x{214D}\x{214F}' .
+  '\x{2190}-\x{244A}\x{249C}-\x{24E9}\x{2500}-\x{2775}\x{2794}-\x{2B59}' .
+  '\x{2CE5}-\x{2CEA}\x{2CF9}-\x{2CFC}\x{2CFE}-\x{2CFF}\x{2E00}-\x{2E2E}' .
+  '\x{2E30}-\x{3004}\x{3008}-\x{3020}\x{3030}\x{3036}-\x{3037}' .
+  '\x{303D}-\x{303F}\x{309B}-\x{309C}\x{30A0}\x{30FB}\x{3190}-\x{3191}' .
+  '\x{3196}-\x{319F}\x{31C0}-\x{31E3}\x{3200}-\x{321E}\x{322A}-\x{3250}' .
+  '\x{3260}-\x{327F}\x{328A}-\x{32B0}\x{32C0}-\x{33FF}\x{4DC0}-\x{4DFF}' .
+  '\x{A490}-\x{A4C6}\x{A4FE}-\x{A4FF}\x{A60D}-\x{A60F}\x{A673}\x{A67E}' .
+  '\x{A6F2}-\x{A716}\x{A720}-\x{A721}\x{A789}-\x{A78A}\x{A828}-\x{A82B}' .
+  '\x{A836}-\x{A839}\x{A874}-\x{A877}\x{A8CE}-\x{A8CF}\x{A8F8}-\x{A8FA}' .
+  '\x{A92E}-\x{A92F}\x{A95F}\x{A9C1}-\x{A9CD}\x{A9DE}-\x{A9DF}' .
+  '\x{AA5C}-\x{AA5F}\x{AA77}-\x{AA79}\x{AADE}-\x{AADF}\x{ABEB}' .
+  '\x{E000}-\x{F8FF}\x{FB29}\x{FD3E}-\x{FD3F}\x{FDFC}-\x{FDFD}' .
+  '\x{FE10}-\x{FE19}\x{FE30}-\x{FE6B}\x{FEFF}-\x{FF0F}\x{FF1A}-\x{FF20}' .
+  '\x{FF3B}-\x{FF40}\x{FF5B}-\x{FF65}\x{FFE0}-\x{FFFD}');
+
+/**
+ * Wrapper around _unicode_check().
+ */
+function unicode_check() {
+  list($GLOBALS['multibyte']) = _unicode_check();
+}
+
+/**
+ * Perform checks about Unicode support in PHP, and set the right settings if
+ * needed.
+ *
+ * Because Drupal needs to be able to handle text in various encodings, we do
+ * not support mbstring function overloading. HTTP input/output conversion must
+ * be disabled for similar reasons.
+ *
+ * @param $errors
+ *   Whether to report any fatal errors with form_set_error().
+ */
+function _unicode_check() {
+  // Ensure translations don't break during installation.
+  $t = get_t();
+
+  // Check for mbstring extension
+  if (!function_exists('mb_strlen')) {
+    return array(UNICODE_SINGLEBYTE, $t('Operations on Unicode strings are emulated on a best-effort basis. Install the <a href="@url">PHP mbstring extension</a> for improved Unicode support.', array('@url' => 'http://www.php.net/mbstring')));
+  }
+
+  // Check mbstring configuration
+  if (ini_get('mbstring.func_overload') != 0) {
+    return array(UNICODE_ERROR, $t('Multibyte string function overloading in PHP is active and must be disabled. Check the php.ini <em>mbstring.func_overload</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
+  }
+  if (ini_get('mbstring.encoding_translation') != 0) {
+    return array(UNICODE_ERROR, $t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.encoding_translation</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
+  }
+  if (ini_get('mbstring.http_input') != 'pass') {
+    return array(UNICODE_ERROR, $t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.http_input</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
+  }
+  if (ini_get('mbstring.http_output') != 'pass') {
+    return array(UNICODE_ERROR, $t('Multibyte string output conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.http_output</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
+  }
+
+  // Set appropriate configuration
+  mb_internal_encoding('utf-8');
+  mb_language('uni');
+  return array(UNICODE_MULTIBYTE, '');
+}
+
+/**
+ * Returns Unicode library status and errors.
+ */
+function unicode_requirements() {
+  // Ensure translations don't break during installation.
+  $t = get_t();
+
+  $libraries = array(
+    UNICODE_SINGLEBYTE => $t('Standard PHP'),
+    UNICODE_MULTIBYTE => $t('PHP Mbstring Extension'),
+    UNICODE_ERROR => $t('Error'),
+  );
+  $severities = array(
+    UNICODE_SINGLEBYTE => REQUIREMENT_WARNING,
+    UNICODE_MULTIBYTE => REQUIREMENT_OK,
+    UNICODE_ERROR => REQUIREMENT_ERROR,
+  );
+  list($library, $description) = _unicode_check();
+
+  $requirements['unicode'] = array(
+    'title' => $t('Unicode library'),
+    'value' => $libraries[$library],
+  );
+  if ($description) {
+    $requirements['unicode']['description'] = $description;
+  }
+
+  $requirements['unicode']['severity'] = $severities[$library];
+
+  return $requirements;
+}
+
+/**
+ * Prepares a new XML parser.
+ *
+ * This is a wrapper around xml_parser_create() which extracts the encoding
+ * from the XML data first and sets the output encoding to UTF-8. This function
+ * should be used instead of xml_parser_create(), because PHP 4's XML parser
+ * doesn't check the input encoding itself. "Starting from PHP 5, the input
+ * encoding is automatically detected, so that the encoding parameter specifies
+ * only the output encoding."
+ *
+ * This is also where unsupported encodings will be converted. Callers should
+ * take this into account: $data might have been changed after the call.
+ *
+ * @param $data
+ *   The XML data which will be parsed later.
+ *
+ * @return
+ *   An XML parser object or FALSE on error.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_xml_parser_create(&$data) {
+  // Default XML encoding is UTF-8
+  $encoding = 'utf-8';
+  $bom = FALSE;
+
+  // Check for UTF-8 byte order mark (PHP5's XML parser doesn't handle it).
+  if (!strncmp($data, "\xEF\xBB\xBF", 3)) {
+    $bom = TRUE;
+    $data = substr($data, 3);
+  }
+
+  // Check for an encoding declaration in the XML prolog if no BOM was found.
+  if (!$bom && preg_match('/^<\?xml[^>]+encoding="(.+?)"/', $data, $match)) {
+    $encoding = $match[1];
+  }
+
+  // Unsupported encodings are converted here into UTF-8.
+  $php_supported = array('utf-8', 'iso-8859-1', 'us-ascii');
+  if (!in_array(strtolower($encoding), $php_supported)) {
+    $out = drupal_convert_to_utf8($data, $encoding);
+    if ($out !== FALSE) {
+      $encoding = 'utf-8';
+      $data = preg_replace('/^(<\?xml[^>]+encoding)="(.+?)"/', '\\1="utf-8"', $out);
+    }
+    else {
+      watchdog('php', 'Could not convert XML encoding %s to UTF-8.', array('%s' => $encoding), WATCHDOG_WARNING);
+      return FALSE;
+    }
+  }
+
+  $xml_parser = xml_parser_create($encoding);
+  xml_parser_set_option($xml_parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
+  return $xml_parser;
+}
+
+/**
+ * Converts data to UTF-8.
+ *
+ * Requires the iconv, GNU recode or mbstring PHP extension.
+ *
+ * @param $data
+ *   The data to be converted.
+ * @param $encoding
+ *   The encoding that the data is in.
+ *
+ * @return
+ *   Converted data or FALSE.
+ */
+function drupal_convert_to_utf8($data, $encoding) {
+  if (function_exists('iconv')) {
+    $out = @iconv($encoding, 'utf-8', $data);
+  }
+  elseif (function_exists('mb_convert_encoding')) {
+    $out = @mb_convert_encoding($data, 'utf-8', $encoding);
+  }
+  elseif (function_exists('recode_string')) {
+    $out = @recode_string($encoding . '..utf-8', $data);
+  }
+  else {
+    watchdog('php', 'Unsupported encoding %s. Please install iconv, GNU recode or mbstring for PHP.', array('%s' => $encoding), WATCHDOG_ERROR);
+    return FALSE;
+  }
+
+  return $out;
+}
+
+/**
+ * Truncates a UTF-8-encoded string safely to a number of bytes.
+ *
+ * If the end position is in the middle of a UTF-8 sequence, it scans backwards
+ * until the beginning of the byte sequence.
+ *
+ * Use this function whenever you want to chop off a string at an unsure
+ * location. On the other hand, if you're sure that you're splitting on a
+ * character boundary (e.g. after using strpos() or similar), you can safely
+ * use substr() instead.
+ *
+ * @param $string
+ *   The string to truncate.
+ * @param $len
+ *   An upper limit on the returned string length.
+ *
+ * @return
+ *   The truncated string.
+ */
+function drupal_truncate_bytes($string, $len) {
+  if (strlen($string) <= $len) {
+    return $string;
+  }
+  if ((ord($string[$len]) < 0x80) || (ord($string[$len]) >= 0xC0)) {
+    return substr($string, 0, $len);
+  }
+  // Scan backwards to beginning of the byte sequence.
+  while (--$len >= 0 && ord($string[$len]) >= 0x80 && ord($string[$len]) < 0xC0);
+
+  return substr($string, 0, $len);
+}
+
+/**
+ * Truncates a UTF-8-encoded string safely to a number of characters.
+ *
+ * @param $string
+ *   The string to truncate.
+ * @param $max_length
+ *   An upper limit on the returned string length, including trailing ellipsis
+ *   if $add_ellipsis is TRUE.
+ * @param $wordsafe
+ *   If TRUE, attempt to truncate on a word boundary. Word boundaries are
+ *   spaces, punctuation, and Unicode characters used as word boundaries in
+ *   non-Latin languages; see PREG_CLASS_UNICODE_WORD_BOUNDARY for more
+ *   information. If a word boundary cannot be found that would make the length
+ *   of the returned string fall within length guidelines (see parameters
+ *   $max_length and $min_wordsafe_length), word boundaries are ignored.
+ * @param $add_ellipsis
+ *   If TRUE, add t('...') to the end of the truncated string (defaults to
+ *   FALSE). The string length will still fall within $max_length.
+ * @param $min_wordsafe_length
+ *   If $wordsafe is TRUE, the minimum acceptable length for truncation (before
+ *   adding an ellipsis, if $add_ellipsis is TRUE). Has no effect if $wordsafe
+ *   is FALSE. This can be used to prevent having a very short resulting string
+ *   that will not be understandable. For instance, if you are truncating the
+ *   string "See myverylongurlexample.com for more information" to a word-safe
+ *   return length of 20, the only available word boundary within 20 characters
+ *   is after the word "See", which wouldn't leave a very informative string. If
+ *   you had set $min_wordsafe_length to 10, though, the function would realise
+ *   that "See" alone is too short, and would then just truncate ignoring word
+ *   boundaries, giving you "See myverylongurl..." (assuming you had set
+ *   $add_ellipses to TRUE).
+ *
+ * @return string
+ *   The truncated string.
+ */
+function truncate_utf8($string, $max_length, $wordsafe = FALSE, $add_ellipsis = FALSE, $min_wordsafe_length = 1) {
+  $ellipsis = '';
+  $max_length = max($max_length, 0);
+  $min_wordsafe_length = max($min_wordsafe_length, 0);
+
+  if (drupal_strlen($string) <= $max_length) {
+    // No truncation needed, so don't add ellipsis, just return.
+    return $string;
+  }
+
+  if ($add_ellipsis) {
+    // Truncate ellipsis in case $max_length is small.
+    $ellipsis = drupal_substr(t('...'), 0, $max_length);
+    $max_length -= drupal_strlen($ellipsis);
+    $max_length = max($max_length, 0);
+  }
+
+  if ($max_length <= $min_wordsafe_length) {
+    // Do not attempt word-safe if lengths are bad.
+    $wordsafe = FALSE;
+  }
+
+  if ($wordsafe) {
+    $matches = array();
+    // Find the last word boundary, if there is one within $min_wordsafe_length
+    // to $max_length characters. preg_match() is always greedy, so it will
+    // find the longest string possible.
+    $found = preg_match('/^(.{' . $min_wordsafe_length . ',' . $max_length . '})[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']/u', $string, $matches);
+    if ($found) {
+      $string = $matches[1];
+    }
+    else {
+      $string = drupal_substr($string, 0, $max_length);
+    }
+  }
+  else {
+    $string = drupal_substr($string, 0, $max_length);
+  }
+
+  if ($add_ellipsis) {
+    $string .= $ellipsis;
+  }
+
+  return $string;
+}
+
+/**
+ * Encodes MIME/HTTP header values that contain incorrectly encoded characters.
+ *
+ * For example, mime_header_encode('tést.txt') returns "=?UTF-8?B?dMOpc3QudHh0?=".
+ *
+ * See http://www.rfc-editor.org/rfc/rfc2047.txt for more information.
+ *
+ * Notes:
+ * - Only encode strings that contain non-ASCII characters.
+ * - We progressively cut-off a chunk with truncate_utf8(). This is to ensure
+ *   each chunk starts and ends on a character boundary.
+ * - Using \n as the chunk separator may cause problems on some systems and may
+ *   have to be changed to \r\n or \r.
+ *
+ * @param $string
+ *   The header to encode.
+ *
+ * @return string
+ *   The mime-encoded header.
+ *
+ * @see mime_header_decode()
+ */
+function mime_header_encode($string) {
+  if (preg_match('/[^\x20-\x7E]/', $string)) {
+    $chunk_size = 47; // floor((75 - strlen("=?UTF-8?B??=")) * 0.75);
+    $len = strlen($string);
+    $output = '';
+    while ($len > 0) {
+      $chunk = drupal_truncate_bytes($string, $chunk_size);
+      $output .= ' =?UTF-8?B?' . base64_encode($chunk) . "?=\n";
+      $c = strlen($chunk);
+      $string = substr($string, $c);
+      $len -= $c;
+    }
+    return trim($output);
+  }
+  return $string;
+}
+
+/**
+ * Decodes MIME/HTTP encoded header values.
+ *
+ * @param $header
+ *   The header to decode.
+ *
+ * @return string
+ *   The mime-decoded header.
+ *
+ * @see mime_header_encode()
+ */
+function mime_header_decode($header) {
+  // First step: encoded chunks followed by other encoded chunks (need to collapse whitespace)
+  $header = preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=\s+(?==\?)/', '_mime_header_decode', $header);
+  // Second step: remaining chunks (do not collapse whitespace)
+  return preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=/', '_mime_header_decode', $header);
+}
+
+/**
+ * Decodes encoded header data passed from mime_header_decode().
+ *
+ * Callback for preg_replace_callback() within mime_header_decode().
+ *
+ * @param $matches
+ *   The array of matches from preg_replace_callback().
+ *
+ * @return string
+ *   The mime-decoded string.
+ *
+ * @see mime_header_decode()
+ */
+function _mime_header_decode($matches) {
+  // Regexp groups:
+  // 1: Character set name
+  // 2: Escaping method (Q or B)
+  // 3: Encoded data
+  $data = ($matches[2] == 'B') ? base64_decode($matches[3]) : str_replace('_', ' ', quoted_printable_decode($matches[3]));
+  if (strtolower($matches[1]) != 'utf-8') {
+    $data = drupal_convert_to_utf8($data, $matches[1]);
+  }
+  return $data;
+}
+
+/**
+ * Decodes all HTML entities (including numerical ones) to regular UTF-8 bytes.
+ *
+ * Double-escaped entities will only be decoded once ("&amp;lt;" becomes "&lt;"
+ * , not "<"). Be careful when using this function, as decode_entities can
+ * revert previous sanitization efforts (&lt;script&gt; will become <script>).
+ *
+ * @param $text
+ *   The text to decode entities in.
+ *
+ * @return
+ *   The input $text, with all HTML entities decoded once.
+ */
+function decode_entities($text) {
+  return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
+}
+
+/**
+ * Counts the number of characters in a UTF-8 string.
+ *
+ * This is less than or equal to the byte count.
+ *
+ * @param $text
+ *   The string to run the operation on.
+ *
+ * @return integer
+ *   The length of the string.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_strlen($text) {
+  global $multibyte;
+  if ($multibyte == UNICODE_MULTIBYTE) {
+    return mb_strlen($text);
+  }
+  else {
+    // Do not count UTF-8 continuation bytes.
+    return strlen(preg_replace("/[\x80-\xBF]/", '', $text));
+  }
+}
+
+/**
+ * Uppercase a UTF-8 string.
+ *
+ * @param $text
+ *   The string to run the operation on.
+ *
+ * @return string
+ *   The string in uppercase.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_strtoupper($text) {
+  global $multibyte;
+  if ($multibyte == UNICODE_MULTIBYTE) {
+    return mb_strtoupper($text);
+  }
+  else {
+    // Use C-locale for ASCII-only uppercase
+    $text = strtoupper($text);
+    // Case flip Latin-1 accented letters
+    $text = preg_replace_callback('/\xC3[\xA0-\xB6\xB8-\xBE]/', '_unicode_caseflip', $text);
+    return $text;
+  }
+}
+
+/**
+ * Lowercase a UTF-8 string.
+ *
+ * @param $text
+ *   The string to run the operation on.
+ *
+ * @return string
+ *   The string in lowercase.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_strtolower($text) {
+  global $multibyte;
+  if ($multibyte == UNICODE_MULTIBYTE) {
+    return mb_strtolower($text);
+  }
+  else {
+    // Use C-locale for ASCII-only lowercase
+    $text = strtolower($text);
+    // Case flip Latin-1 accented letters
+    $text = preg_replace_callback('/\xC3[\x80-\x96\x98-\x9E]/', '_unicode_caseflip', $text);
+    return $text;
+  }
+}
+
+/**
+ * Flips U+C0-U+DE to U+E0-U+FD and back.
+ *
+ * @param $matches
+ *   An array of matches.
+ *
+ * @return array
+ *   The Latin-1 version of the array of matches.
+ *
+ * @see drupal_strtolower()
+ */
+function _unicode_caseflip($matches) {
+  return $matches[0][0] . chr(ord($matches[0][1]) ^ 32);
+}
+
+/**
+ * Capitalizes the first letter of a UTF-8 string.
+ *
+ * @param $text
+ *   The string to convert.
+ *
+ * @return
+ *   The string with the first letter as uppercase.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_ucfirst($text) {
+  // Note: no mbstring equivalent!
+  return drupal_strtoupper(drupal_substr($text, 0, 1)) . drupal_substr($text, 1);
+}
+
+/**
+ * Cuts off a piece of a string based on character indices and counts.
+ *
+ * Follows the same behavior as PHP's own substr() function. Note that for
+ * cutting off a string at a known character/substring location, the usage of
+ * PHP's normal strpos/substr is safe and much faster.
+ *
+ * @param $text
+ *   The input string.
+ * @param $start
+ *   The position at which to start reading.
+ * @param $length
+ *   The number of characters to read.
+ *
+ * @return
+ *   The shortened string.
+ *
+ * @ingroup php_wrappers
+ */
+function drupal_substr($text, $start, $length = NULL) {
+  global $multibyte;
+  if ($multibyte == UNICODE_MULTIBYTE) {
+    return $length === NULL ? mb_substr($text, $start) : mb_substr($text, $start, $length);
+  }
+  else {
+    $strlen = strlen($text);
+    // Find the starting byte offset.
+    $bytes = 0;
+    if ($start > 0) {
+      // Count all the continuation bytes from the start until we have found
+      // $start characters or the end of the string.
+      $bytes = -1; $chars = -1;
+      while ($bytes < $strlen - 1 && $chars < $start) {
+        $bytes++;
+        $c = ord($text[$bytes]);
+        if ($c < 0x80 || $c >= 0xC0) {
+          $chars++;
+        }
+      }
+    }
+    elseif ($start < 0) {
+      // Count all the continuation bytes from the end until we have found
+      // abs($start) characters.
+      $start = abs($start);
+      $bytes = $strlen; $chars = 0;
+      while ($bytes > 0 && $chars < $start) {
+        $bytes--;
+        $c = ord($text[$bytes]);
+        if ($c < 0x80 || $c >= 0xC0) {
+          $chars++;
+        }
+      }
+    }
+    $istart = $bytes;
+
+    // Find the ending byte offset.
+    if ($length === NULL) {
+      $iend = $strlen;
+    }
+    elseif ($length > 0) {
+      // Count all the continuation bytes from the starting index until we have
+      // found $length characters or reached the end of the string, then
+      // backtrace one byte.
+      $iend = $istart - 1;
+      $chars = -1;
+      $last_real = FALSE;
+      while ($iend < $strlen - 1 && $chars < $length) {
+        $iend++;
+        $c = ord($text[$iend]);
+        $last_real = FALSE;
+        if ($c < 0x80 || $c >= 0xC0) {
+          $chars++;
+          $last_real = TRUE;
+        }
+      }
+      // Backtrace one byte if the last character we found was a real character
+      // and we don't need it.
+      if ($last_real && $chars >= $length) {
+        $iend--;
+      }
+    }
+    elseif ($length < 0) {
+      // Count all the continuation bytes from the end until we have found
+      // abs($start) characters, then backtrace one byte.
+      $length = abs($length);
+      $iend = $strlen; $chars = 0;
+      while ($iend > 0 && $chars < $length) {
+        $iend--;
+        $c = ord($text[$iend]);
+        if ($c < 0x80 || $c >= 0xC0) {
+          $chars++;
+        }
+      }
+      // Backtrace one byte if we are not at the beginning of the string.
+      if ($iend > 0) {
+        $iend--;
+      }
+    }
+    else {
+      // $length == 0, return an empty string.
+      return '';
+    }
+
+    return substr($text, $istart, max(0, $iend - $istart + 1));
+  }
+}

+ 1477 - 0
includes/update.inc

@@ -0,0 +1,1477 @@
+<?php
+
+/**
+ * @file
+ * Drupal database update API.
+ *
+ * This file contains functions to perform database updates for a Drupal
+ * installation. It is included and used extensively by update.php.
+ */
+
+/**
+ * Minimum schema version of Drupal 6 required for upgrade to Drupal 7.
+ *
+ * Upgrades from Drupal 6 to Drupal 7 require that Drupal 6 be running
+ * the most recent version, or the upgrade could fail. We can't easily
+ * check the Drupal 6 version once the update process has begun, so instead
+ * we check the schema version of system.module in the system table.
+ */
+define('REQUIRED_D6_SCHEMA_VERSION', '6055');
+
+/**
+ * Disable any items in the {system} table that are not core compatible.
+ */
+function update_fix_compatibility() {
+  $incompatible = array();
+  $result = db_query("SELECT name, type, status FROM {system} WHERE status = 1 AND type IN ('module','theme')");
+  foreach ($result as $row) {
+    if (update_check_incompatibility($row->name, $row->type)) {
+      $incompatible[] = $row->name;
+    }
+  }
+  if (!empty($incompatible)) {
+    db_update('system')
+      ->fields(array('status' => 0))
+      ->condition('name', $incompatible, 'IN')
+      ->execute();
+  }
+}
+
+/**
+ * Tests the compatibility of a module or theme.
+ */
+function update_check_incompatibility($name, $type = 'module') {
+  static $themes, $modules;
+
+  // Store values of expensive functions for future use.
+  if (empty($themes) || empty($modules)) {
+    // We need to do a full rebuild here to make sure the database reflects any
+    // code changes that were made in the filesystem before the update script
+    // was initiated.
+    $themes = system_rebuild_theme_data();
+    $modules = system_rebuild_module_data();
+  }
+
+  if ($type == 'module' && isset($modules[$name])) {
+    $file = $modules[$name];
+  }
+  elseif ($type == 'theme' && isset($themes[$name])) {
+    $file = $themes[$name];
+  }
+  if (!isset($file)
+      || !isset($file->info['core'])
+      || $file->info['core'] != DRUPAL_CORE_COMPATIBILITY
+      || version_compare(phpversion(), $file->info['php']) < 0) {
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * Performs extra steps required to bootstrap when using a Drupal 6 database.
+ *
+ * Users who still have a Drupal 6 database (and are in the process of
+ * updating to Drupal 7) need extra help before a full bootstrap can be
+ * achieved. This function does the necessary preliminary work that allows
+ * the bootstrap to be successful.
+ *
+ * No access check has been performed when this function is called, so no
+ * irreversible changes to the database are made here.
+ */
+function update_prepare_d7_bootstrap() {
+  // Allow the bootstrap to proceed even if a Drupal 6 settings.php file is
+  // still being used.
+  include_once DRUPAL_ROOT . '/includes/install.inc';
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
+  global $databases, $db_url, $db_prefix, $update_rewrite_settings;
+  if (empty($databases) && !empty($db_url)) {
+    $databases = update_parse_db_url($db_url, $db_prefix);
+    // Record the fact that the settings.php file will need to be rewritten.
+    $update_rewrite_settings = TRUE;
+    $settings_file = conf_path() . '/settings.php';
+    $writable = drupal_verify_install_file($settings_file, FILE_EXIST|FILE_READABLE|FILE_WRITABLE);
+    $requirements = array(
+      'settings file' => array(
+        'title' => 'Settings file',
+        'value' => $writable ? 'The settings file is writable.' : 'The settings file is not writable.',
+        'severity' => $writable ? REQUIREMENT_OK : REQUIREMENT_ERROR,
+        'description' => $writable ? '' : 'Drupal requires write permissions to <em>' . $settings_file . '</em> during the update process. If you are unsure how to grant file permissions, consult the <a href="http://drupal.org/server-permissions">online handbook</a>.',
+      ),
+    );
+    update_extra_requirements($requirements);
+  }
+
+  // The new {blocked_ips} table is used in Drupal 7 to store a list of
+  // banned IP addresses. If this table doesn't exist then we are still
+  // running on a Drupal 6 database, so we suppress the unavoidable errors
+  // that occur by creating a static list.
+  $GLOBALS['conf']['blocked_ips'] = array();
+
+  // Check that PDO is available and that the correct PDO database driver is
+  // loaded. Bootstrapping to DRUPAL_BOOTSTRAP_DATABASE will result in a fatal
+  // error otherwise.
+  $message = '';
+  $pdo_link = 'http://drupal.org/requirements/pdo';
+  // Check that PDO is loaded.
+  if (!extension_loaded('pdo')) {
+    $message = '<h2>PDO is required!</h2><p>Drupal 7 requires PHP ' . DRUPAL_MINIMUM_PHP . ' or higher with the PHP Data Objects (PDO) extension enabled.</p>';
+  }
+  // The PDO::ATTR_DEFAULT_FETCH_MODE constant is not available in the PECL
+  // version of PDO.
+  elseif (!defined('PDO::ATTR_DEFAULT_FETCH_MODE')) {
+    $message = '<h2>The wrong version of PDO is installed!</h2><p>Drupal 7 requires the PHP Data Objects (PDO) extension from PHP core to be enabled. This system has the older PECL version installed.';
+    $pdo_link = 'http://drupal.org/requirements/pdo#pecl';
+  }
+  // Check that the correct driver is loaded for the database being updated.
+  // If we have no driver information (for example, if someone tried to create
+  // the Drupal 7 $databases array themselves but did not do it correctly),
+  // this message will be confusing, so do not perform the check; instead, just
+  // let the database connection fail in the code that follows.
+  elseif (isset($databases['default']['default']['driver']) && !in_array($databases['default']['default']['driver'], PDO::getAvailableDrivers())) {
+    $message = '<h2>A PDO database driver is required!</h2><p>You need to enable the PDO_' . strtoupper($databases['default']['default']['driver']) . ' database driver for PHP ' . DRUPAL_MINIMUM_PHP . ' or higher so that Drupal 7 can access the database.</p>';
+  }
+  if ($message) {
+    print $message . '<p>See the <a href="' . $pdo_link . '">system requirements page</a> for more information.</p>';
+    exit();
+  }
+
+  // Allow the database system to work even if the registry has not been
+  // created yet.
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
+
+  // If the site has not updated to Drupal 7 yet, check to make sure that it is
+  // running an up-to-date version of Drupal 6 before proceeding. Note this has
+  // to happen AFTER the database bootstraps because of
+  // drupal_get_installed_schema_version().
+  $system_schema = drupal_get_installed_schema_version('system');
+  if ($system_schema < 7000) {
+    $has_required_schema = $system_schema >= REQUIRED_D6_SCHEMA_VERSION;
+    $requirements = array(
+      'drupal 6 version' => array(
+        'title' => 'Drupal 6 version',
+        'value' => $has_required_schema ? 'You are running a current version of Drupal 6.' : 'You are not running a current version of Drupal 6',
+        'severity' => $has_required_schema ? REQUIREMENT_OK : REQUIREMENT_ERROR,
+        'description' => $has_required_schema ? '' : 'Please update your Drupal 6 installation to the most recent version before attempting to upgrade to Drupal 7',
+      ),
+    );
+
+    // Make sure that the database environment is properly set up.
+    try {
+      db_run_tasks(db_driver());
+    }
+    catch (DatabaseTaskException $e) {
+      $requirements['database tasks'] = array(
+        'title' => 'Database environment',
+        'value' => 'There is a problem with your database environment',
+        'severity' => REQUIREMENT_ERROR,
+        'description' => $e->getMessage(),
+      );
+    }
+
+    update_extra_requirements($requirements);
+
+    // Allow a D6 session to work, since the upgrade has not been performed yet.
+    $d6_session_name = update_get_d6_session_name();
+    if (!empty($_COOKIE[$d6_session_name])) {
+      // Set the current sid to the one found in the D6 cookie.
+      $sid = $_COOKIE[$d6_session_name];
+      $_COOKIE[session_name()] = $sid;
+      session_id($sid);
+    }
+
+    // Upgrading from D6 to D7.{0,1,2,3,4,8,...} is different than upgrading
+    // from D6 to D7.{5,6,7} which should be considered broken. To be able to
+    // properly handle this difference in node_update_7012 we need to keep track
+    // of whether a D6 > D7 upgrade or a D7 > D7 update is running.
+    // Since variable_set() is not available here, the D6 status is being saved
+    // in a local variable to be able to store it later.
+    $update_d6 = TRUE;
+  }
+
+  // Create the registry tables.
+  if (!db_table_exists('registry')) {
+    $schema['registry'] = array(
+      'fields' => array(
+        'name'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
+        'type'   => array('type' => 'varchar', 'length' => 9, 'not null' => TRUE, 'default' => ''),
+        'filename'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
+        'module'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
+        'weight'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      ),
+      'primary key' => array('name', 'type'),
+      'indexes' => array(
+        'hook' => array('type', 'weight', 'module'),
+      ),
+    );
+    db_create_table('registry', $schema['registry']);
+  }
+  if (!db_table_exists('registry_file')) {
+    $schema['registry_file'] = array(
+      'fields' => array(
+        'filename'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE),
+        'hash'   => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE),
+      ),
+      'primary key' => array('filename'),
+    );
+    db_create_table('registry_file', $schema['registry_file']);
+  }
+
+  // Older versions of Drupal 6 do not include the semaphore table, which is
+  // required to bootstrap, so we add it now so that we can bootstrap and
+  // provide a reasonable error message.
+  if (!db_table_exists('semaphore')) {
+    $semaphore = array(
+      'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as Drupal variables since they must not be cached.',
+      'fields' => array(
+        'name' => array(
+          'description' => 'Primary Key: Unique name.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => ''
+        ),
+        'value' => array(
+          'description' => 'A value for the semaphore.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => ''
+        ),
+        'expire' => array(
+          'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
+          'type' => 'float',
+          'size' => 'big',
+          'not null' => TRUE
+        ),
+      ),
+      'indexes' => array(
+        'value' => array('value'),
+        'expire' => array('expire'),
+      ),
+      'primary key' => array('name'),
+    );
+    db_create_table('semaphore', $semaphore);
+  }
+
+  // The new cache_bootstrap bin is required to bootstrap to
+  // DRUPAL_BOOTSTRAP_SESSION, so create it here rather than in
+  // update_fix_d7_requirements().
+  if (!db_table_exists('cache_bootstrap')) {
+    $cache_bootstrap = array(
+      'description' => 'Cache table for data required to bootstrap Drupal, may be routed to a shared memory cache.',
+      'fields' => array(
+        'cid' => array(
+          'description' => 'Primary Key: Unique cache ID.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'data' => array(
+          'description' => 'A collection of data to cache.',
+          'type' => 'blob',
+          'not null' => FALSE,
+          'size' => 'big',
+        ),
+        'expire' => array(
+          'description' => 'A Unix timestamp indicating when the cache entry should expire, or 0 for never.',
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'created' => array(
+          'description' => 'A Unix timestamp indicating when the cache entry was created.',
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'serialized' => array(
+          'description' => 'A flag to indicate whether content is serialized (1) or not (0).',
+          'type' => 'int',
+          'size' => 'small',
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+      ),
+      'indexes' => array(
+        'expire' => array('expire'),
+      ),
+      'primary key' => array('cid'),
+    );
+    db_create_table('cache_bootstrap', $cache_bootstrap);
+  }
+
+  // Set a valid timezone for 6 -> 7 upgrade process.
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
+  $timezone_offset = variable_get('date_default_timezone', 0);
+  if (is_numeric($timezone_offset)) {
+    // Save the original offset.
+    variable_set('date_temporary_timezone', $timezone_offset);
+    // Set the timezone for this request only.
+    $GLOBALS['conf']['date_default_timezone'] = 'UTC';
+  }
+
+  // This allows update functions to tell if an upgrade from D6 is running.
+  if (!empty($update_d6)) {
+    variable_set('update_d6', TRUE);
+  }
+}
+
+/**
+ * A helper function that modules can use to assist with the transformation
+ * from numeric block deltas to string block deltas during the 6.x -> 7.x
+ * upgrade.
+ *
+ * @todo This function should be removed in 8.x.
+ *
+ * @param $sandbox
+ *   An array holding data for the batch process.
+ * @param $renamed_deltas
+ *   An associative array.  Keys are module names, values an associative array
+ *   mapping the old block deltas to the new block deltas for the module.
+ *   Example:
+ *   @code
+ *     $renamed_deltas = array(
+ *       'mymodule' =>
+ *         array(
+ *           0 => 'mymodule-block-1',
+ *           1 => 'mymodule-block-2',
+ *         ),
+ *     );
+ *   @endcode
+ * @param $moved_deltas
+ *   An associative array. Keys are source module names, values an associative
+ *   array mapping the (possibly renamed) block name to the new module name.
+ *   Example:
+ *   @code
+ *     $moved_deltas = array(
+ *       'user' =>
+ *         array(
+ *           'navigation' => 'system',
+ *         ),
+ *     );
+ *   @endcode
+ */
+function update_fix_d7_block_deltas(&$sandbox, $renamed_deltas, $moved_deltas) {
+  // Loop through each block and make changes to the block tables.
+  // Only run this the first time through the batch update.
+  if (!isset($sandbox['progress'])) {
+    // Determine whether to use the old or new block table names.
+    $block_tables = db_table_exists('blocks') ? array('blocks', 'blocks_roles') : array('block', 'block_role');
+    foreach ($block_tables as $table) {
+      foreach ($renamed_deltas as $module => $deltas) {
+        foreach ($deltas as $old_delta => $new_delta) {
+          // Only do the update if the old block actually exists.
+          $block_exists = db_query("SELECT COUNT(*) FROM {" . $table . "} WHERE module = :module AND delta = :delta", array(
+              ':module' => $module,
+              ':delta' => $old_delta,
+            ))
+            ->fetchField();
+          if ($block_exists) {
+            // Delete any existing blocks with the new module+delta.
+            db_delete($table)
+              ->condition('module', $module)
+              ->condition('delta', $new_delta)
+              ->execute();
+            // Rename the old block to the new module+delta.
+            db_update($table)
+              ->fields(array('delta' => $new_delta))
+              ->condition('module', $module)
+              ->condition('delta', $old_delta)
+              ->execute();
+          }
+        }
+      }
+      foreach ($moved_deltas as $old_module => $deltas) {
+        foreach ($deltas as $delta => $new_module) {
+          // Only do the update if the old block actually exists.
+          $block_exists = db_query("SELECT COUNT(*) FROM {" . $table . "} WHERE module = :module AND delta = :delta", array(
+              ':module' => $old_module,
+              ':delta' => $delta,
+            ))
+            ->fetchField();
+          if ($block_exists) {
+            // Delete any existing blocks with the new module+delta.
+            db_delete($table)
+              ->condition('module', $new_module)
+              ->condition('delta', $delta)
+              ->execute();
+            // Rename the old block to the new module+delta.
+            db_update($table)
+              ->fields(array('module' => $new_module))
+              ->condition('module', $old_module)
+              ->condition('delta', $delta)
+              ->execute();
+          }
+        }
+      }
+    }
+
+    // Initialize batch update information.
+    $sandbox['progress'] = 0;
+    $sandbox['last_user_processed'] = -1;
+    $sandbox['max'] = db_query("SELECT COUNT(*) FROM {users} WHERE data LIKE :block", array(
+        ':block' => '%' . db_like(serialize('block')) . '%',
+      ))
+      ->fetchField();
+  }
+  // Now do the batch update of the user-specific block visibility settings.
+  $limit = 100;
+  $result = db_select('users', 'u')
+    ->fields('u', array('uid', 'data'))
+    ->condition('uid', $sandbox['last_user_processed'], '>')
+    ->condition('data', '%' . db_like(serialize('block')) . '%', 'LIKE')
+    ->orderBy('uid', 'ASC')
+    ->range(0, $limit)
+    ->execute();
+  foreach ($result as $row) {
+    $data = unserialize($row->data);
+    $user_needs_update = FALSE;
+    foreach ($renamed_deltas as $module => $deltas) {
+      foreach ($deltas as $old_delta => $new_delta) {
+        if (isset($data['block'][$module][$old_delta])) {
+          // Transfer the old block visibility settings to the newly-renamed
+          // block, and mark this user for a database update.
+          $data['block'][$module][$new_delta] = $data['block'][$module][$old_delta];
+          unset($data['block'][$module][$old_delta]);
+          $user_needs_update = TRUE;
+        }
+      }
+    }
+    foreach ($moved_deltas as $old_module => $deltas) {
+      foreach ($deltas as $delta => $new_module) {
+        if (isset($data['block'][$old_module][$delta])) {
+          // Transfer the old block visibility settings to the moved
+          // block, and mark this user for a database update.
+          $data['block'][$new_module][$delta] = $data['block'][$old_module][$delta];
+          unset($data['block'][$old_module][$delta]);
+          $user_needs_update = TRUE;
+        }
+      }
+    }
+    // Update the current user.
+    if ($user_needs_update) {
+      db_update('users')
+        ->fields(array('data' => serialize($data)))
+        ->condition('uid', $row->uid)
+        ->execute();
+    }
+    // Update our progress information for the batch update.
+    $sandbox['progress']++;
+    $sandbox['last_user_processed'] = $row->uid;
+  }
+  // Indicate our current progress to the batch update system.
+  if ($sandbox['progress'] < $sandbox['max']) {
+    $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max'];
+  }
+}
+
+/**
+ * Perform Drupal 6.x to 7.x updates that are required for update.php
+ * to function properly.
+ *
+ * This function runs when update.php is run the first time for 7.x,
+ * even before updates are selected or performed. It is important
+ * that if updates are not ultimately performed that no changes are
+ * made which make it impossible to continue using the prior version.
+ */
+function update_fix_d7_requirements() {
+  global $conf;
+
+  // Rewrite the settings.php file if necessary, see
+  // update_prepare_d7_bootstrap().
+  global $update_rewrite_settings, $db_url, $db_prefix;
+  if (!empty($update_rewrite_settings)) {
+    $databases = update_parse_db_url($db_url, $db_prefix);
+    $salt = drupal_hash_base64(drupal_random_bytes(55));
+    file_put_contents(conf_path() . '/settings.php', "\n" . '$databases = ' . var_export($databases, TRUE) . ";\n\$drupal_hash_salt = '$salt';", FILE_APPEND);
+  }
+  if (drupal_get_installed_schema_version('system') < 7000 && !variable_get('update_d7_requirements', FALSE)) {
+    // Change 6.x system table field values to 7.x equivalent.
+    // Change field values.
+    db_change_field('system', 'schema_version', 'schema_version', array(
+     'type' => 'int',
+     'size' => 'small',
+     'not null' => TRUE,
+     'default' => -1)
+    );
+    db_change_field('system', 'status', 'status', array(
+      'type' => 'int', 'not null' => TRUE, 'default' => 0));
+    db_change_field('system', 'weight', 'weight', array(
+      'type' => 'int', 'not null' => TRUE, 'default' => 0));
+    db_change_field('system', 'bootstrap', 'bootstrap', array(
+      'type' => 'int', 'not null' => TRUE, 'default' => 0));
+    // Drop and recreate 6.x indexes.
+    db_drop_index('system', 'bootstrap');
+    db_add_index('system', 'bootstrap', array(
+      'status', 'bootstrap', array('type', 12), 'weight', 'name'));
+
+    db_drop_index('system' ,'modules');
+    db_add_index('system', 'modules', array(array(
+      'type', 12), 'status', 'weight', 'name'));
+
+    db_drop_index('system', 'type_name');
+    db_add_index('system', 'type_name', array(array('type', 12), 'name'));
+    // Add 7.x indexes.
+    db_add_index('system', 'system_list', array('weight', 'name'));
+
+    // Add the cache_path table.
+    if (db_table_exists('cache_path')) {
+      db_drop_table('cache_path');
+    }
+    require_once('./modules/system/system.install');
+    $schema['cache_path'] = system_schema_cache_7054();
+    $schema['cache_path']['description'] = 'Cache table used for path alias lookups.';
+    db_create_table('cache_path', $schema['cache_path']);
+
+    // system_update_7042() renames columns, but these are needed to bootstrap.
+    // Add empty columns for now.
+    db_add_field('url_alias', 'source', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''));
+    db_add_field('url_alias', 'alias', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''));
+
+    // Add new columns to {menu_router}.
+    db_add_field('menu_router', 'delivery_callback', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''));
+    db_add_field('menu_router', 'context', array(
+      'description' => 'Only for local tasks (tabs) - the context of a local task to control its placement.',
+      'type' => 'int',
+      'not null' => TRUE,
+      'default' => 0,
+    ));
+    db_drop_index('menu_router', 'tab_parent');
+    db_add_index('menu_router', 'tab_parent', array(array('tab_parent', 64), 'weight', 'title'));
+    db_add_field('menu_router', 'theme_callback', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''));
+    db_add_field('menu_router', 'theme_arguments', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''));
+
+    // Add the role_permission table.
+    $schema['role_permission'] = array(
+      'fields' => array(
+        'rid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'permission' => array(
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+    ),
+    'primary key' => array('rid', 'permission'),
+    'indexes' => array(
+      'permission' => array('permission'),
+      ),
+    );
+    db_create_table('role_permission', $schema['role_permission']);
+
+    // Drops and recreates semaphore value index.
+    db_drop_index('semaphore', 'value');
+    db_add_index('semaphore', 'value', array('value'));
+
+    $schema['date_format_type'] = array(
+      'description' => 'Stores configured date format types.',
+      'fields' => array(
+        'type' => array(
+          'description' => 'The date format type, e.g. medium.',
+          'type' => 'varchar',
+          'length' => 64,
+          'not null' => TRUE,
+        ),
+        'title' => array(
+          'description' => 'The human readable name of the format type.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+        ),
+        'locked' => array(
+          'description' => 'Whether or not this is a system provided format.',
+          'type' => 'int',
+          'size' => 'tiny',
+          'default' => 0,
+          'not null' => TRUE,
+        ),
+      ),
+      'primary key' => array('type'),
+    );
+
+    $schema['date_formats'] = array(
+      'description' => 'Stores configured date formats.',
+      'fields' => array(
+        'dfid' => array(
+          'description' => 'The date format identifier.',
+          'type' => 'serial',
+          'not null' => TRUE,
+          'unsigned' => TRUE,
+        ),
+        'format' => array(
+          'description' => 'The date format string.',
+          'type' => 'varchar',
+          'length' => 100,
+          'not null' => TRUE,
+        ),
+        'type' => array(
+          'description' => 'The date format type, e.g. medium.',
+          'type' => 'varchar',
+          'length' => 64,
+          'not null' => TRUE,
+        ),
+        'locked' => array(
+          'description' => 'Whether or not this format can be modified.',
+          'type' => 'int',
+          'size' => 'tiny',
+          'default' => 0,
+          'not null' => TRUE,
+        ),
+      ),
+      'primary key' => array('dfid'),
+      'unique keys' => array('formats' => array('format', 'type')),
+    );
+
+    $schema['date_format_locale'] = array(
+      'description' => 'Stores configured date formats for each locale.',
+      'fields' => array(
+        'format' => array(
+          'description' => 'The date format string.',
+          'type' => 'varchar',
+          'length' => 100,
+          'not null' => TRUE,
+        ),
+        'type' => array(
+          'description' => 'The date format type, e.g. medium.',
+          'type' => 'varchar',
+          'length' => 64,
+          'not null' => TRUE,
+        ),
+        'language' => array(
+          'description' => 'A {languages}.language for this format to be used with.',
+          'type' => 'varchar',
+          'length' => 12,
+          'not null' => TRUE,
+        ),
+      ),
+      'primary key' => array('type', 'language'),
+    );
+
+    db_create_table('date_format_type', $schema['date_format_type']);
+    // Sites that have the Drupal 6 Date module installed already have the
+    // following tables.
+    if (db_table_exists('date_formats')) {
+      db_rename_table('date_formats', 'd6_date_formats');
+    }
+    db_create_table('date_formats', $schema['date_formats']);
+    if (db_table_exists('date_format_locale')) {
+      db_rename_table('date_format_locale', 'd6_date_format_locale');
+    }
+    db_create_table('date_format_locale', $schema['date_format_locale']);
+
+    // Add the queue table.
+    $schema['queue'] = array(
+      'description' => 'Stores items in queues.',
+      'fields' => array(
+        'item_id' => array(
+          'type' => 'serial',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'description' => 'Primary Key: Unique item ID.',
+        ),
+        'name' => array(
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'The queue name.',
+        ),
+        'data' => array(
+          'type' => 'blob',
+          'not null' => FALSE,
+          'size' => 'big',
+          'serialize' => TRUE,
+          'description' => 'The arbitrary data for the item.',
+        ),
+        'expire' => array(
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'Timestamp when the claim lease expires on the item.',
+        ),
+        'created' => array(
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'Timestamp when the item was created.',
+        ),
+      ),
+      'primary key' => array('item_id'),
+      'indexes' => array(
+        'name_created' => array('name', 'created'),
+        'expire' => array('expire'),
+      ),
+    );
+    // Check for queue table that may remain from D5 or D6, if found
+    //drop it.
+    if (db_table_exists('queue')) {
+      db_drop_table('queue');
+    }
+
+    db_create_table('queue', $schema['queue']);
+
+    // Create the sequences table.
+    $schema['sequences'] = array(
+      'description' => 'Stores IDs.',
+      'fields' => array(
+        'value' => array(
+          'description' => 'The value of the sequence.',
+          'type' => 'serial',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+        ),
+       ),
+      'primary key' => array('value'),
+    );
+    // Check for sequences table that may remain from D5 or D6, if found
+    //drop it.
+    if (db_table_exists('sequences')) {
+      db_drop_table('sequences');
+    }
+    db_create_table('sequences', $schema['sequences']);
+    // Initialize the table with the maximum current increment of the tables
+    // that will rely on it for their ids.
+    $max_aid = db_query('SELECT MAX(aid) FROM {actions_aid}')->fetchField();
+    $max_uid = db_query('SELECT MAX(uid) FROM {users}')->fetchField();
+    $max_batch_id = db_query('SELECT MAX(bid) FROM {batch}')->fetchField();
+    db_insert('sequences')->fields(array('value' => max($max_aid, $max_uid, $max_batch_id)))->execute();
+
+    // Add column for locale context.
+    if (db_table_exists('locales_source')) {
+      db_add_field('locales_source', 'context', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', 'description' => 'The context this string applies to.'));
+    }
+
+    // Rename 'site_offline_message' variable to 'maintenance_mode_message'
+    // and 'site_offline' variable to 'maintenance_mode'.
+    // Old variable is removed in update for system.module, see
+    // system_update_7072().
+    if ($message = variable_get('site_offline_message', NULL)) {
+      variable_set('maintenance_mode_message', $message);
+    }
+    // Old variable is removed in update for system.module, see
+    // system_update_7069().
+    $site_offline = variable_get('site_offline', -1);
+    if ($site_offline != -1) {
+      variable_set('maintenance_mode', $site_offline);
+    }
+
+    // Add ssid column and index.
+    db_add_field('sessions', 'ssid', array('description' => "Secure session ID. The value is generated by Drupal's session handlers.", 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''));
+    db_add_index('sessions', 'ssid', array('ssid'));
+    // Drop existing primary key.
+    db_drop_primary_key('sessions');
+    // Add new primary key.
+    db_add_primary_key('sessions', array('sid', 'ssid'));
+
+    // Allow longer javascript file names.
+    if (db_table_exists('languages')) {
+      db_change_field('languages', 'javascript', 'javascript', array('type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => ''));
+    }
+
+    // Rename action description to label.
+    db_change_field('actions', 'description', 'label', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '0'));
+
+    variable_set('update_d7_requirements', TRUE);
+  }
+
+  update_fix_d7_install_profile();
+}
+
+/**
+ * Register the currently installed profile in the system table.
+ *
+ * Installation profiles are now treated as modules by Drupal, and have an
+ * upgrade path based on their schema version in the system table.
+ *
+ * The installation profile will be set to schema_version 0, as it has already
+ * been installed. Any other hook_update_N functions provided by the
+ * installation profile will be run by update.php.
+ */
+function update_fix_d7_install_profile() {
+  $profile = drupal_get_profile();
+
+  $results = db_select('system', 's')
+    ->fields('s', array('name', 'schema_version'))
+    ->condition('name', $profile)
+    ->condition('type', 'module')
+    ->execute()
+    ->fetchAll();
+
+  if (empty($results)) {
+    $filename = 'profiles/' . $profile . '/' . $profile . '.profile';
+
+    // Read profile info file
+    $info = drupal_parse_info_file(dirname($filename) . '/' . $profile . '.info');
+
+    // Merge in defaults.
+    $info = $info + array(
+      'dependencies' => array(),
+      'description' => '',
+      'package' => 'Other',
+      'version' => NULL,
+      'php' => DRUPAL_MINIMUM_PHP,
+      'files' => array(),
+    );
+
+    $values = array(
+      'filename' => $filename,
+      'name' => $profile,
+      'info' => serialize($info),
+      'schema_version' => 0,
+      'type' => 'module',
+      'status' => 1,
+      'owner' => '',
+    );
+
+    // Installation profile hooks are always executed last by the module system
+    $values['weight'] = 1000;
+
+    // Initializing the system table entry for the installation profile
+    db_insert('system')
+      ->fields(array_keys($values))
+      ->values($values)
+      ->execute();
+
+    // Reset the cached schema version.
+    drupal_get_installed_schema_version($profile, TRUE);
+
+    // Load the updates again to make sure the installation profile updates
+    // are loaded.
+    drupal_load_updates();
+  }
+}
+
+/**
+ * Parse pre-Drupal 7 database connection URLs and return D7 compatible array.
+ *
+ * @return
+ *   Drupal 7 DBTNG compatible array of database connection information.
+ */
+function update_parse_db_url($db_url, $db_prefix) {
+  $databases = array();
+  if (!is_array($db_url)) {
+    $db_url = array('default' => $db_url);
+  }
+  foreach ($db_url as $database => $url) {
+    $url = parse_url($url);
+    $databases[$database]['default'] = array(
+      // MySQLi uses the mysql driver.
+      'driver' => $url['scheme'] == 'mysqli' ? 'mysql' : $url['scheme'],
+      // Remove the leading slash to get the database name.
+      'database' => substr(urldecode($url['path']), 1),
+      'username' => urldecode($url['user']),
+      'password' => isset($url['pass']) ? urldecode($url['pass']) : '',
+      'host' => urldecode($url['host']),
+      'port' => isset($url['port']) ? urldecode($url['port']) : '',
+    );
+    if (isset($db_prefix)) {
+      $databases[$database]['default']['prefix'] = $db_prefix;
+    }
+  }
+  return $databases;
+}
+
+/**
+ * Constructs a session name compatible with a D6 environment.
+ *
+ * @return
+ *   D6-compatible session name string.
+ *
+ * @see drupal_settings_initialize()
+ */
+function update_get_d6_session_name() {
+  global $base_url, $cookie_domain;
+  $cookie_secure = ini_get('session.cookie_secure');
+
+  // If a custom cookie domain is set in settings.php, that variable forms
+  // the basis of the session name. Re-compute the D7 hashing method to find
+  // out if $cookie_domain was used as the session name.
+  if (($cookie_secure ? 'SSESS' : 'SESS') . substr(hash('sha256', $cookie_domain), 0, 32) == session_name()) {
+    $session_name = $cookie_domain;
+  }
+  else {
+    // Otherwise use $base_url as session name, without the protocol
+    // to use the same session identifiers across HTTP and HTTPS.
+    list( , $session_name) = explode('://', $base_url, 2);
+  }
+
+  if ($cookie_secure) {
+    $session_name .= 'SSL';
+  }
+
+  return 'SESS' . md5($session_name);
+}
+
+/**
+ * Performs one update and stores the results for display on the results page.
+ *
+ * If an update function completes successfully, it should return a message
+ * as a string indicating success, for example:
+ * @code
+ * return t('New index added successfully.');
+ * @endcode
+ *
+ * Alternatively, it may return nothing. In that case, no message
+ * will be displayed at all.
+ *
+ * If it fails for whatever reason, it should throw an instance of
+ * DrupalUpdateException with an appropriate error message, for example:
+ * @code
+ * throw new DrupalUpdateException(t('Description of what went wrong'));
+ * @endcode
+ *
+ * If an exception is thrown, the current update and all updates that depend on
+ * it will be aborted. The schema version will not be updated in this case, and
+ * all the aborted updates will continue to appear on update.php as updates
+ * that have not yet been run.
+ *
+ * If an update function needs to be re-run as part of a batch process, it
+ * should accept the $sandbox array by reference as its first parameter
+ * and set the #finished property to the percentage completed that it is, as a
+ * fraction of 1.
+ *
+ * @param $module
+ *   The module whose update will be run.
+ * @param $number
+ *   The update number to run.
+ * @param $dependency_map
+ *   An array whose keys are the names of all update functions that will be
+ *   performed during this batch process, and whose values are arrays of other
+ *   update functions that each one depends on.
+ * @param $context
+ *   The batch context array.
+ *
+ * @see update_resolve_dependencies()
+ */
+function update_do_one($module, $number, $dependency_map, &$context) {
+  $function = $module . '_update_' . $number;
+
+  // If this update was aborted in a previous step, or has a dependency that
+  // was aborted in a previous step, go no further.
+  if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, array($function)))) {
+    return;
+  }
+
+  $ret = array();
+  if (function_exists($function)) {
+    try {
+      $ret['results']['query'] = $function($context['sandbox']);
+      $ret['results']['success'] = TRUE;
+    }
+    // @TODO We may want to do different error handling for different
+    // exception types, but for now we'll just log the exception and
+    // return the message for printing.
+    catch (Exception $e) {
+      watchdog_exception('update', $e);
+
+      require_once DRUPAL_ROOT . '/includes/errors.inc';
+      $variables = _drupal_decode_exception($e);
+      // The exception message is run through check_plain() by _drupal_decode_exception().
+      $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables));
+    }
+  }
+
+  if (isset($context['sandbox']['#finished'])) {
+    $context['finished'] = $context['sandbox']['#finished'];
+    unset($context['sandbox']['#finished']);
+  }
+
+  if (!isset($context['results'][$module])) {
+    $context['results'][$module] = array();
+  }
+  if (!isset($context['results'][$module][$number])) {
+    $context['results'][$module][$number] = array();
+  }
+  $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);
+
+  if (!empty($ret['#abort'])) {
+    // Record this function in the list of updates that were aborted.
+    $context['results']['#abort'][] = $function;
+  }
+
+  // Record the schema update if it was completed successfully.
+  if ($context['finished'] == 1 && empty($ret['#abort'])) {
+    drupal_set_installed_schema_version($module, $number);
+  }
+
+  $context['message'] = 'Updating ' . check_plain($module) . ' module';
+}
+
+/**
+ * @class Exception class used to throw error if a module update fails.
+ */
+class DrupalUpdateException extends Exception { }
+
+/**
+ * Starts the database update batch process.
+ *
+ * @param $start
+ *   An array whose keys contain the names of modules to be updated during the
+ *   current batch process, and whose values contain the number of the first
+ *   requested update for that module. The actual updates that are run (and the
+ *   order they are run in) will depend on the results of passing this data
+ *   through the update dependency system.
+ * @param $redirect
+ *   Path to redirect to when the batch has finished processing.
+ * @param $url
+ *   URL of the batch processing page (should only be used for separate
+ *   scripts like update.php).
+ * @param $batch
+ *   Optional parameters to pass into the batch API.
+ * @param $redirect_callback
+ *   (optional) Specify a function to be called to redirect to the progressive
+ *   processing page.
+ *
+ * @see update_resolve_dependencies()
+ */
+function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $redirect_callback = 'drupal_goto') {
+  // During the update, bring the site offline so that schema changes do not
+  // affect visiting users.
+  $_SESSION['maintenance_mode'] = variable_get('maintenance_mode', FALSE);
+  if ($_SESSION['maintenance_mode'] == FALSE) {
+    variable_set('maintenance_mode', TRUE);
+  }
+
+  // Resolve any update dependencies to determine the actual updates that will
+  // be run and the order they will be run in.
+  $updates = update_resolve_dependencies($start);
+
+  // Store the dependencies for each update function in an array which the
+  // batch API can pass in to the batch operation each time it is called. (We
+  // do not store the entire update dependency array here because it is
+  // potentially very large.)
+  $dependency_map = array();
+  foreach ($updates as $function => $update) {
+    $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array();
+  }
+
+  $operations = array();
+  foreach ($updates as $update) {
+    if ($update['allowed']) {
+      // Set the installed version of each module so updates will start at the
+      // correct place. (The updates are already sorted, so we can simply base
+      // this on the first one we come across in the above foreach loop.)
+      if (isset($start[$update['module']])) {
+        drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
+        unset($start[$update['module']]);
+      }
+      // Add this update function to the batch.
+      $function = $update['module'] . '_update_' . $update['number'];
+      $operations[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function]));
+    }
+  }
+  $batch['operations'] = $operations;
+  $batch += array(
+    'title' => 'Updating',
+    'init_message' => 'Starting updates',
+    'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.',
+    'finished' => 'update_finished',
+    'file' => 'includes/update.inc',
+  );
+  batch_set($batch);
+  batch_process($redirect, $url, $redirect_callback);
+}
+
+/**
+ * Finishes the update process and stores the results for eventual display.
+ *
+ * After the updates run, all caches are flushed. The update results are
+ * stored into the session (for example, to be displayed on the update results
+ * page in update.php). Additionally, if the site was off-line, now that the
+ * update process is completed, the site is set back online.
+ *
+ * @param $success
+ *   Indicate that the batch API tasks were all completed successfully.
+ * @param $results
+ *   An array of all the results that were updated in update_do_one().
+ * @param $operations
+ *   A list of all the operations that had not been completed by the batch API.
+ *
+ * @see update_batch()
+ */
+function update_finished($success, $results, $operations) {
+  // Remove the D6 upgrade flag variable so that subsequent update runs do not
+  // get the wrong context.
+  variable_del('update_d6');
+
+  // Clear the caches in case the data has been updated.
+  drupal_flush_all_caches();
+
+  $_SESSION['update_results'] = $results;
+  $_SESSION['update_success'] = $success;
+  $_SESSION['updates_remaining'] = $operations;
+
+  // Now that the update is done, we can put the site back online if it was
+  // previously in maintenance mode.
+  if (isset($_SESSION['maintenance_mode']) && $_SESSION['maintenance_mode'] == FALSE) {
+    variable_set('maintenance_mode', FALSE);
+    unset($_SESSION['maintenance_mode']);
+  }
+}
+
+/**
+ * Returns a list of all the pending database updates.
+ *
+ * @return
+ *   An associative array keyed by module name which contains all information
+ *   about database updates that need to be run, and any updates that are not
+ *   going to proceed due to missing requirements. The system module will
+ *   always be listed first.
+ *
+ *   The subarray for each module can contain the following keys:
+ *   - start: The starting update that is to be processed. If this does not
+ *       exist then do not process any updates for this module as there are
+ *       other requirements that need to be resolved.
+ *   - warning: Any warnings about why this module can not be updated.
+ *   - pending: An array of all the pending updates for the module including
+ *       the update number and the description from source code comment for
+ *       each update function. This array is keyed by the update number.
+ */
+function update_get_update_list() {
+  // Make sure that the system module is first in the list of updates.
+  $ret = array('system' => array());
+
+  $modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE);
+  foreach ($modules as $module => $schema_version) {
+    // Skip uninstalled and incompatible modules.
+    if ($schema_version == SCHEMA_UNINSTALLED || update_check_incompatibility($module)) {
+      continue;
+    }
+    // Otherwise, get the list of updates defined by this module.
+    $updates = drupal_get_schema_versions($module);
+    if ($updates !== FALSE) {
+      // module_invoke returns NULL for nonexisting hooks, so if no updates
+      // are removed, it will == 0.
+      $last_removed = module_invoke($module, 'update_last_removed');
+      if ($schema_version < $last_removed) {
+        $ret[$module]['warning'] = '<em>' . $module . '</em> module can not be updated. Its schema version is ' . $schema_version . '. Updates up to and including ' . $last_removed . ' have been removed in this release. In order to update <em>' . $module . '</em> module, you will first <a href="http://drupal.org/upgrade">need to upgrade</a> to the last version in which these updates were available.';
+        continue;
+      }
+
+      $updates = drupal_map_assoc($updates);
+      foreach (array_keys($updates) as $update) {
+        if ($update > $schema_version) {
+          // The description for an update comes from its Doxygen.
+          $func = new ReflectionFunction($module . '_update_' . $update);
+          $description = str_replace(array("\n", '*', '/'), '', $func->getDocComment());
+          $ret[$module]['pending'][$update] = "$update - $description";
+          if (!isset($ret[$module]['start'])) {
+            $ret[$module]['start'] = $update;
+          }
+        }
+      }
+      if (!isset($ret[$module]['start']) && isset($ret[$module]['pending'])) {
+        $ret[$module]['start'] = $schema_version;
+      }
+    }
+  }
+
+  if (empty($ret['system'])) {
+    unset($ret['system']);
+  }
+  return $ret;
+}
+
+/**
+ * Resolves dependencies in a set of module updates, and orders them correctly.
+ *
+ * This function receives a list of requested module updates and determines an
+ * appropriate order to run them in such that all update dependencies are met.
+ * Any updates whose dependencies cannot be met are included in the returned
+ * array but have the key 'allowed' set to FALSE; the calling function should
+ * take responsibility for ensuring that these updates are ultimately not
+ * performed.
+ *
+ * In addition, the returned array also includes detailed information about the
+ * dependency chain for each update, as provided by the depth-first search
+ * algorithm in drupal_depth_first_search().
+ *
+ * @param $starting_updates
+ *   An array whose keys contain the names of modules with updates to be run
+ *   and whose values contain the number of the first requested update for that
+ *   module.
+ *
+ * @return
+ *   An array whose keys are the names of all update functions within the
+ *   provided modules that would need to be run in order to fulfill the
+ *   request, arranged in the order in which the update functions should be
+ *   run. (This includes the provided starting update for each module and all
+ *   subsequent updates that are available.) The values are themselves arrays
+ *   containing all the keys provided by the drupal_depth_first_search()
+ *   algorithm, which encode detailed information about the dependency chain
+ *   for this update function (for example: 'paths', 'reverse_paths', 'weight',
+ *   and 'component'), as well as the following additional keys:
+ *   - 'allowed': A boolean which is TRUE when the update function's
+ *     dependencies are met, and FALSE otherwise. Calling functions should
+ *     inspect this value before running the update.
+ *   - 'missing_dependencies': An array containing the names of any other
+ *     update functions that are required by this one but that are unavailable
+ *     to be run. This array will be empty when 'allowed' is TRUE.
+ *   - 'module': The name of the module that this update function belongs to.
+ *   - 'number': The number of this update function within that module.
+ *
+ * @see drupal_depth_first_search()
+ */
+function update_resolve_dependencies($starting_updates) {
+  // Obtain a dependency graph for the requested update functions.
+  $update_functions = update_get_update_function_list($starting_updates);
+  $graph = update_build_dependency_graph($update_functions);
+
+  // Perform the depth-first search and sort the results.
+  require_once DRUPAL_ROOT . '/includes/graph.inc';
+  drupal_depth_first_search($graph);
+  uasort($graph, 'drupal_sort_weight');
+
+  foreach ($graph as $function => &$data) {
+    $module = $data['module'];
+    $number = $data['number'];
+    // If the update function is missing and has not yet been performed, mark
+    // it and everything that ultimately depends on it as disallowed.
+    if (update_is_missing($module, $number, $update_functions) && !update_already_performed($module, $number)) {
+      $data['allowed'] = FALSE;
+      foreach (array_keys($data['paths']) as $dependent) {
+        $graph[$dependent]['allowed'] = FALSE;
+        $graph[$dependent]['missing_dependencies'][] = $function;
+      }
+    }
+    elseif (!isset($data['allowed'])) {
+      $data['allowed'] = TRUE;
+      $data['missing_dependencies'] = array();
+    }
+    // Now that we have finished processing this function, remove it from the
+    // graph if it was not part of the original list. This ensures that we
+    // never try to run any updates that were not specifically requested.
+    if (!isset($update_functions[$module][$number])) {
+      unset($graph[$function]);
+    }
+  }
+
+  return $graph;
+}
+
+/**
+ * Returns an organized list of update functions for a set of modules.
+ *
+ * @param $starting_updates
+ *   An array whose keys contain the names of modules and whose values contain
+ *   the number of the first requested update for that module.
+ *
+ * @return
+ *   An array containing all the update functions that should be run for each
+ *   module, including the provided starting update and all subsequent updates
+ *   that are available. The keys of the array contain the module names, and
+ *   each value is an ordered array of update functions, keyed by the update
+ *   number.
+ *
+ * @see update_resolve_dependencies()
+ */
+function update_get_update_function_list($starting_updates) {
+  // Go through each module and find all updates that we need (including the
+  // first update that was requested and any updates that run after it).
+  $update_functions = array();
+  foreach ($starting_updates as $module => $version) {
+    $update_functions[$module] = array();
+    $updates = drupal_get_schema_versions($module);
+    if ($updates !== FALSE) {
+      $max_version = max($updates);
+      if ($version <= $max_version) {
+        foreach ($updates as $update) {
+          if ($update >= $version) {
+            $update_functions[$module][$update] = $module . '_update_' . $update;
+          }
+        }
+      }
+    }
+  }
+  return $update_functions;
+}
+
+/**
+ * Constructs a graph which encodes the dependencies between module updates.
+ *
+ * This function returns an associative array which contains a "directed graph"
+ * representation of the dependencies between a provided list of update
+ * functions, as well as any outside update functions that they directly depend
+ * on but that were not in the provided list. The vertices of the graph
+ * represent the update functions themselves, and each edge represents a
+ * requirement that the first update function needs to run before the second.
+ * For example, consider this graph:
+ *
+ * system_update_7000 ---> system_update_7001 ---> system_update_7002
+ *
+ * Visually, this indicates that system_update_7000() must run before
+ * system_update_7001(), which in turn must run before system_update_7002().
+ *
+ * The function takes into account standard dependencies within each module, as
+ * shown above (i.e., the fact that each module's updates must run in numerical
+ * order), but also finds any cross-module dependencies that are defined by
+ * modules which implement hook_update_dependencies(), and builds them into the
+ * graph as well.
+ *
+ * @param $update_functions
+ *   An organized array of update functions, in the format returned by
+ *   update_get_update_function_list().
+ *
+ * @return
+ *   A multidimensional array representing the dependency graph, suitable for
+ *   passing in to drupal_depth_first_search(), but with extra information
+ *   about each update function also included. Each array key contains the name
+ *   of an update function, including all update functions from the provided
+ *   list as well as any outside update functions which they directly depend
+ *   on. Each value is an associative array containing the following keys:
+ *   - 'edges': A representation of any other update functions that immediately
+ *     depend on this one. See drupal_depth_first_search() for more details on
+ *     the format.
+ *   - 'module': The name of the module that this update function belongs to.
+ *   - 'number': The number of this update function within that module.
+ *
+ * @see drupal_depth_first_search()
+ * @see update_resolve_dependencies()
+ */
+function update_build_dependency_graph($update_functions) {
+  // Initialize an array that will define a directed graph representing the
+  // dependencies between update functions.
+  $graph = array();
+
+  // Go through each update function and build an initial list of dependencies.
+  foreach ($update_functions as $module => $functions) {
+    $previous_function = NULL;
+    foreach ($functions as $number => $function) {
+      // Add an edge to the directed graph representing the fact that each
+      // update function in a given module must run after the update that
+      // numerically precedes it.
+      if ($previous_function) {
+        $graph[$previous_function]['edges'][$function] = TRUE;
+      }
+      $previous_function = $function;
+
+      // Define the module and update number associated with this function.
+      $graph[$function]['module'] = $module;
+      $graph[$function]['number'] = $number;
+    }
+  }
+
+  // Now add any explicit update dependencies declared by modules.
+  $update_dependencies = update_retrieve_dependencies();
+  foreach ($graph as $function => $data) {
+    if (!empty($update_dependencies[$data['module']][$data['number']])) {
+      foreach ($update_dependencies[$data['module']][$data['number']] as $module => $number) {
+        $dependency = $module . '_update_' . $number;
+        $graph[$dependency]['edges'][$function] = TRUE;
+        $graph[$dependency]['module'] = $module;
+        $graph[$dependency]['number'] = $number;
+      }
+    }
+  }
+
+  return $graph;
+}
+
+/**
+ * Determines if a module update is missing or unavailable.
+ *
+ * @param $module
+ *   The name of the module.
+ * @param $number
+ *   The number of the update within that module.
+ * @param $update_functions
+ *   An organized array of update functions, in the format returned by
+ *   update_get_update_function_list(). This should represent all module
+ *   updates that are requested to run at the time this function is called.
+ *
+ * @return
+ *   TRUE if the provided module update is not installed or is not in the
+ *   provided list of updates to run; FALSE otherwise.
+ */
+function update_is_missing($module, $number, $update_functions) {
+  return !isset($update_functions[$module][$number]) || !function_exists($update_functions[$module][$number]);
+}
+
+/**
+ * Determines if a module update has already been performed.
+ *
+ * @param $module
+ *   The name of the module.
+ * @param $number
+ *   The number of the update within that module.
+ *
+ * @return
+ *   TRUE if the database schema indicates that the update has already been
+ *   performed; FALSE otherwise.
+ */
+function update_already_performed($module, $number) {
+  return $number <= drupal_get_installed_schema_version($module);
+}
+
+/**
+ * Invokes hook_update_dependencies() in all installed modules.
+ *
+ * This function is similar to module_invoke_all(), with the main difference
+ * that it does not require that a module be enabled to invoke its hook, only
+ * that it be installed. This allows the update system to properly perform
+ * updates even on modules that are currently disabled.
+ *
+ * @return
+ *   An array of return values obtained by merging the results of the
+ *   hook_update_dependencies() implementations in all installed modules.
+ *
+ * @see module_invoke_all()
+ * @see hook_update_dependencies()
+ */
+function update_retrieve_dependencies() {
+  $return = array();
+  // Get a list of installed modules, arranged so that we invoke their hooks in
+  // the same order that module_invoke_all() does.
+  $modules = db_query("SELECT name FROM {system} WHERE type = 'module' AND schema_version <> :schema ORDER BY weight ASC, name ASC", array(':schema' => SCHEMA_UNINSTALLED))->fetchCol();
+  foreach ($modules as $module) {
+    $function = $module . '_update_dependencies';
+    if (function_exists($function)) {
+      $result = $function();
+      // Each implementation of hook_update_dependencies() returns a
+      // multidimensional, associative array containing some keys that
+      // represent module names (which are strings) and other keys that
+      // represent update function numbers (which are integers). We cannot use
+      // array_merge_recursive() to properly merge these results, since it
+      // treats strings and integers differently. Therefore, we have to
+      // explicitly loop through the expected array structure here and perform
+      // the merge manually.
+      if (isset($result) && is_array($result)) {
+        foreach ($result as $module => $module_data) {
+          foreach ($module_data as $update => $update_data) {
+            foreach ($update_data as $module_dependency => $update_dependency) {
+              // If there are redundant dependencies declared for the same
+              // update function (so that it is declared to depend on more than
+              // one update from a particular module), record the dependency on
+              // the highest numbered update here, since that automatically
+              // implies the previous ones. For example, if one module's
+              // implementation of hook_update_dependencies() required this
+              // ordering:
+              //
+              // system_update_7001 ---> user_update_7000
+              //
+              // but another module's implementation of the hook required this
+              // one:
+              //
+              // system_update_7002 ---> user_update_7000
+              //
+              // we record the second one, since system_update_7001() is always
+              // guaranteed to run before system_update_7002() anyway (within
+              // an individual module, updates are always run in numerical
+              // order).
+              if (!isset($return[$module][$update][$module_dependency]) || $update_dependency > $return[$module][$update][$module_dependency]) {
+                $return[$module][$update][$module_dependency] = $update_dependency;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return $return;
+}

+ 427 - 0
includes/updater.inc

@@ -0,0 +1,427 @@
+<?php
+
+/**
+ * @file
+ * Classes used for updating various files in the Drupal webroot. These
+ * classes use a FileTransfer object to actually perform the operations.
+ * Normally, the FileTransfer is provided when the site owner is redirected to
+ * authorize.php as part of a multistep process.
+ */
+
+/**
+ * Interface for a class which can update a Drupal project.
+ *
+ * An Updater currently serves the following purposes:
+ *   - It can take a given directory, and determine if it can operate on it.
+ *   - It can move the contents of that directory into the appropriate place
+ *     on the system using FileTransfer classes.
+ *   - It can return a list of "next steps" after an update or install.
+ *   - In the future, it will most likely perform some of those steps as well.
+ */
+interface DrupalUpdaterInterface {
+
+  /**
+   * Checks if the project is installed.
+   *
+   * @return bool
+   */
+  public function isInstalled();
+
+  /**
+   * Returns the system name of the project.
+   *
+   * @param string $directory
+   *  A directory containing a project.
+   */
+  public static function getProjectName($directory);
+
+  /**
+   * @return string
+   *   An absolute path to the default install location.
+   */
+  public function getInstallDirectory();
+
+  /**
+   * Determine if the Updater can handle the project provided in $directory.
+   *
+   * @todo: Provide something more rational here, like a project spec file.
+   *
+   * @param string $directory
+   *
+   * @return bool
+   *   TRUE if the project is installed, FALSE if not.
+   */
+  public static function canUpdateDirectory($directory);
+
+  /**
+   * Actions to run after an install has occurred.
+   */
+  public function postInstall();
+
+  /**
+   * Actions to run after an update has occurred.
+   */
+  public function postUpdate();
+}
+
+/**
+ * Base class for Updaters used in Drupal.
+ */
+class Updater {
+
+  /**
+   * @var string $source Directory to install from.
+   */
+  public $source;
+
+  public function __construct($source) {
+    $this->source = $source;
+    $this->name = self::getProjectName($source);
+    $this->title = self::getProjectTitle($source);
+  }
+
+  /**
+   * Return an Updater of the appropriate type depending on the source.
+   *
+   * If a directory is provided which contains a module, will return a
+   * ModuleUpdater.
+   *
+   * @param string $source
+   *   Directory of a Drupal project.
+   *
+   * @return Updater
+   */
+  public static function factory($source) {
+    if (is_dir($source)) {
+      $updater = self::getUpdaterFromDirectory($source);
+    }
+    else {
+      throw new UpdaterException(t('Unable to determine the type of the source directory.'));
+    }
+    return new $updater($source);
+  }
+
+  /**
+   * Determine which Updater class can operate on the given directory.
+   *
+   * @param string $directory
+   *   Extracted Drupal project.
+   *
+   * @return string
+   *   The class name which can work with this project type.
+   */
+  public static function getUpdaterFromDirectory($directory) {
+    // Gets a list of possible implementing classes.
+    $updaters = drupal_get_updaters();
+    foreach ($updaters as $updater) {
+      $class = $updater['class'];
+      if (call_user_func(array($class, 'canUpdateDirectory'), $directory)) {
+        return $class;
+      }
+    }
+    throw new UpdaterException(t('Cannot determine the type of project.'));
+  }
+
+  /**
+   * Figure out what the most important (or only) info file is in a directory.
+   *
+   * Since there is no enforcement of which info file is the project's "main"
+   * info file, this will get one with the same name as the directory, or the
+   * first one it finds.  Not ideal, but needs a larger solution.
+   *
+   * @param string $directory
+   *   Directory to search in.
+   *
+   * @return string
+   *   Path to the info file.
+   */
+  public static function findInfoFile($directory) {
+    $info_files = file_scan_directory($directory, '/.*\.info$/');
+    if (!$info_files) {
+      return FALSE;
+    }
+    foreach ($info_files as $info_file) {
+      if (drupal_substr($info_file->filename, 0, -5) == drupal_basename($directory)) {
+        // Info file Has the same name as the directory, return it.
+        return $info_file->uri;
+      }
+    }
+    // Otherwise, return the first one.
+    $info_file = array_shift($info_files);
+    return $info_file->uri;
+  }
+
+  /**
+   * Get the name of the project directory (basename).
+   *
+   * @todo: It would be nice, if projects contained an info file which could
+   *        provide their canonical name.
+   *
+   * @param string $directory
+   *
+   * @return string
+   *   The name of the project.
+   */
+  public static function getProjectName($directory) {
+    return drupal_basename($directory);
+  }
+
+  /**
+   * Return the project name from a Drupal info file.
+   *
+   * @param string $directory
+   *   Directory to search for the info file.
+   *
+   * @return string
+   *   The title of the project.
+   */
+  public static function getProjectTitle($directory) {
+    $info_file = self::findInfoFile($directory);
+    $info = drupal_parse_info_file($info_file);
+    if (empty($info)) {
+      throw new UpdaterException(t('Unable to parse info file: %info_file.', array('%info_file' => $info_file)));
+    }
+    if (empty($info['name'])) {
+      throw new UpdaterException(t("The info file (%info_file) does not define a 'name' attribute.", array('%info_file' => $info_file)));
+    }
+    return $info['name'];
+  }
+
+  /**
+   * Store the default parameters for the Updater.
+   *
+   * @param array $overrides
+   *   An array of overrides for the default parameters.
+   *
+   * @return array
+   *   An array of configuration parameters for an update or install operation.
+   */
+  protected function getInstallArgs($overrides = array()) {
+    $args = array(
+      'make_backup' => FALSE,
+      'install_dir' => $this->getInstallDirectory(),
+      'backup_dir'  => $this->getBackupDir(),
+    );
+    return array_merge($args, $overrides);
+  }
+
+  /**
+   * Updates a Drupal project, returns a list of next actions.
+   *
+   * @param FileTransfer $filetransfer
+   *   Object that is a child of FileTransfer. Used for moving files
+   *   to the server.
+   * @param array $overrides
+   *   An array of settings to override defaults; see self::getInstallArgs().
+   *
+   * @return array
+   *   An array of links which the user may need to complete the update
+   */
+  public function update(&$filetransfer, $overrides = array()) {
+    try {
+      // Establish arguments with possible overrides.
+      $args = $this->getInstallArgs($overrides);
+
+      // Take a Backup.
+      if ($args['make_backup']) {
+        $this->makeBackup($args['install_dir'], $args['backup_dir']);
+      }
+
+      if (!$this->name) {
+        // This is bad, don't want to delete the install directory.
+        throw new UpdaterException(t('Fatal error in update, cowardly refusing to wipe out the install directory.'));
+      }
+
+      // Make sure the installation parent directory exists and is writable.
+      $this->prepareInstallDirectory($filetransfer, $args['install_dir']);
+
+      // Note: If the project is installed in sites/all, it will not be
+      // deleted. It will be installed in sites/default as that will override
+      // the sites/all reference and not break other sites which are using it.
+      if (is_dir($args['install_dir'] . '/' . $this->name)) {
+        // Remove the existing installed file.
+        $filetransfer->removeDirectory($args['install_dir'] . '/' . $this->name);
+      }
+
+      // Copy the directory in place.
+      $filetransfer->copyDirectory($this->source, $args['install_dir']);
+
+      // Make sure what we just installed is readable by the web server.
+      $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
+
+      // Run the updates.
+      // @TODO: decide if we want to implement this.
+      $this->postUpdate();
+
+      // For now, just return a list of links of things to do.
+      return $this->postUpdateTasks();
+    }
+    catch (FileTransferException $e) {
+      throw new UpdaterFileTransferException(t('File Transfer failed, reason: !reason', array('!reason' => strtr($e->getMessage(), $e->arguments))));
+    }
+  }
+
+  /**
+   * Installs a Drupal project, returns a list of next actions.
+   *
+   * @param FileTransfer $filetransfer
+   *   Object that is a child of FileTransfer.
+   * @param array $overrides
+   *   An array of settings to override defaults; see self::getInstallArgs().
+   *
+   * @return array
+   *   An array of links which the user may need to complete the install.
+   */
+  public function install(&$filetransfer, $overrides = array()) {
+    try {
+      // Establish arguments with possible overrides.
+      $args = $this->getInstallArgs($overrides);
+
+      // Make sure the installation parent directory exists and is writable.
+      $this->prepareInstallDirectory($filetransfer, $args['install_dir']);
+
+      // Copy the directory in place.
+      $filetransfer->copyDirectory($this->source, $args['install_dir']);
+
+      // Make sure what we just installed is readable by the web server.
+      $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
+
+      // Potentially enable something?
+      // @TODO: decide if we want to implement this.
+      $this->postInstall();
+      // For now, just return a list of links of things to do.
+      return $this->postInstallTasks();
+    }
+    catch (FileTransferException $e) {
+      throw new UpdaterFileTransferException(t('File Transfer failed, reason: !reason', array('!reason' => strtr($e->getMessage(), $e->arguments))));
+    }
+  }
+
+  /**
+   * Make sure the installation parent directory exists and is writable.
+   *
+   * @param FileTransfer $filetransfer
+   *   Object which is a child of FileTransfer.
+   * @param string $directory
+   *   The installation directory to prepare.
+   */
+  public function prepareInstallDirectory(&$filetransfer, $directory) {
+    // Make the parent dir writable if need be and create the dir.
+    if (!is_dir($directory)) {
+      $parent_dir = dirname($directory);
+      if (!is_writable($parent_dir)) {
+        @chmod($parent_dir, 0755);
+        // It is expected that this will fail if the directory is owned by the
+        // FTP user. If the FTP user == web server, it will succeed.
+        try {
+          $filetransfer->createDirectory($directory);
+          $this->makeWorldReadable($filetransfer, $directory);
+        }
+        catch (FileTransferException $e) {
+          // Probably still not writable. Try to chmod and do it again.
+          // @todo: Make a new exception class so we can catch it differently.
+          try {
+            $old_perms = substr(sprintf('%o', fileperms($parent_dir)), -4);
+            $filetransfer->chmod($parent_dir, 0755);
+            $filetransfer->createDirectory($directory);
+            $this->makeWorldReadable($filetransfer, $directory);
+            // Put the permissions back.
+            $filetransfer->chmod($parent_dir, intval($old_perms, 8));
+          }
+          catch (FileTransferException $e) {
+            $message = t($e->getMessage(), $e->arguments);
+            $throw_message = t('Unable to create %directory due to the following: %reason', array('%directory' => $directory, '%reason' => $message));
+            throw new UpdaterException($throw_message);
+          }
+        }
+        // Put the parent directory back.
+        @chmod($parent_dir, 0555);
+      }
+    }
+  }
+
+  /**
+   * Ensure that a given directory is world readable.
+   *
+   * @param FileTransfer $filetransfer
+   *   Object which is a child of FileTransfer.
+   * @param string $path
+   *   The file path to make world readable.
+   * @param bool $recursive
+   *   If the chmod should be applied recursively.
+   */
+  public function makeWorldReadable(&$filetransfer, $path, $recursive = TRUE) {
+    if (!is_executable($path)) {
+      // Set it to read + execute.
+      $new_perms = substr(sprintf('%o', fileperms($path)), -4, -1) . "5";
+      $filetransfer->chmod($path, intval($new_perms, 8), $recursive);
+    }
+  }
+
+  /**
+   * Perform a backup.
+   *
+   * @todo Not implemented.
+   */
+  public function makeBackup(&$filetransfer, $from, $to) {
+  }
+
+  /**
+   * Return the full path to a directory where backups should be written.
+   */
+  public function getBackupDir() {
+    return file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath();
+  }
+
+  /**
+   * Perform actions after new code is updated.
+   */
+  public function postUpdate() {
+  }
+
+  /**
+   * Perform actions after installation.
+   */
+  public function postInstall() {
+  }
+
+  /**
+   * Return an array of links to pages that should be visited post operation.
+   *
+   * @return array
+   *   Links which provide actions to take after the install is finished.
+   */
+  public function postInstallTasks() {
+    return array();
+  }
+
+  /**
+   * Return an array of links to pages that should be visited post operation.
+   *
+   * @return array
+   *   Links which provide actions to take after the update is finished.
+   */
+  public function postUpdateTasks() {
+    return array();
+  }
+}
+
+/**
+ * Exception class for the Updater class hierarchy.
+ *
+ * This is identical to the base Exception class, we just give it a more
+ * specific name so that call sites that want to tell the difference can
+ * specifically catch these exceptions and treat them differently.
+ */
+class UpdaterException extends Exception {
+}
+
+/**
+ * Child class of UpdaterException that indicates a FileTransfer exception.
+ *
+ * We have to catch FileTransfer exceptions and wrap those in t(), since
+ * FileTransfer is so low-level that it doesn't use any Drupal APIs and none
+ * of the strings are translated.
+ */
+class UpdaterFileTransferException extends UpdaterException {
+}

+ 66 - 0
includes/utility.inc

@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Miscellaneous functions.
+ */
+
+/**
+ * Drupal-friendly var_export().
+ *
+ * @param $var
+ *   The variable to export.
+ * @param $prefix
+ *   A prefix that will be added at the beginning of every lines of the output.
+ *
+ * @return
+ *   The variable exported in a way compatible to Drupal's coding standards.
+ */
+function drupal_var_export($var, $prefix = '') {
+  if (is_array($var)) {
+    if (empty($var)) {
+      $output = 'array()';
+    }
+    else {
+      $output = "array(\n";
+      // Don't export keys if the array is non associative.
+      $export_keys = array_values($var) != $var;
+      foreach ($var as $key => $value) {
+        $output .= '  ' . ($export_keys ? drupal_var_export($key) . ' => ' : '') . drupal_var_export($value, '  ', FALSE) . ",\n";
+      }
+      $output .= ')';
+    }
+  }
+  elseif (is_bool($var)) {
+    $output = $var ? 'TRUE' : 'FALSE';
+  }
+  elseif (is_string($var)) {
+    $line_safe_var = str_replace("\n", '\n', $var);
+    if (strpos($var, "\n") !== FALSE || strpos($var, "'") !== FALSE) {
+      // If the string contains a line break or a single quote, use the
+      // double quote export mode. Encode backslash and double quotes and
+      // transform some common control characters.
+      $var = str_replace(array('\\', '"', "\n", "\r", "\t"), array('\\\\', '\"', '\n', '\r', '\t'), $var);
+      $output = '"' . $var . '"';
+    }
+    else {
+      $output = "'" . $var . "'";
+    }
+  }
+  elseif (is_object($var) && get_class($var) === 'stdClass') {
+    // var_export() will export stdClass objects using an undefined
+    // magic method __set_state() leaving the export broken. This
+    // workaround avoids this by casting the object as an array for
+    // export and casting it back to an object when evaluated.
+    $output = '(object) ' . drupal_var_export((array) $var, $prefix);
+  }
+  else {
+    $output = var_export($var, TRUE);
+  }
+
+  if ($prefix) {
+    $output = str_replace("\n", "\n$prefix", $output);
+  }
+
+  return $output;
+}

+ 624 - 0
includes/xmlrpc.inc

@@ -0,0 +1,624 @@
+<?php
+
+/**
+ * @file
+ * Drupal XML-RPC library.
+ *
+ * Based on the IXR - The Incutio XML-RPC Library - (c) Incutio Ltd 2002-2005
+ * Version 1.7 (beta) - Simon Willison, 23rd May 2005
+ * Site:   http://scripts.incutio.com/xmlrpc/
+ * Manual: http://scripts.incutio.com/xmlrpc/manual.php
+ * This version is made available under the GNU GPL License
+ */
+
+/**
+ * Turns a data structure into objects with 'data' and 'type' attributes.
+ *
+ * @param $data
+ *   The data structure.
+ * @param $type
+ *   Optional type to assign to $data.
+ *
+ * @return object
+ *   An XML-RPC data object containing the input $data.
+ */
+function xmlrpc_value($data, $type = FALSE) {
+  $xmlrpc_value = new stdClass();
+  $xmlrpc_value->data = $data;
+  if (!$type) {
+    $type = xmlrpc_value_calculate_type($xmlrpc_value);
+  }
+  $xmlrpc_value->type = $type;
+  if ($type == 'struct') {
+    // Turn all the values in the array into new xmlrpc_values
+    foreach ($xmlrpc_value->data as $key => $value) {
+      $xmlrpc_value->data[$key] = xmlrpc_value($value);
+    }
+  }
+  if ($type == 'array') {
+    for ($i = 0, $j = count($xmlrpc_value->data); $i < $j; $i++) {
+      $xmlrpc_value->data[$i] = xmlrpc_value($xmlrpc_value->data[$i]);
+    }
+  }
+  return $xmlrpc_value;
+}
+
+/**
+ * Maps a PHP type to an XML-RPC type.
+ *
+ * @param $xmlrpc_value
+ *   Variable whose type should be mapped.
+ *
+ * @return string
+ *   The corresponding XML-RPC type.
+ *
+ * @see http://www.xmlrpc.com/spec#scalars
+ */
+function xmlrpc_value_calculate_type($xmlrpc_value) {
+  // http://www.php.net/gettype: Never use gettype() to test for a certain type
+  // [...] Instead, use the is_* functions.
+  if (is_bool($xmlrpc_value->data)) {
+    return 'boolean';
+  }
+  if (is_double($xmlrpc_value->data)) {
+    return 'double';
+  }
+  if (is_int($xmlrpc_value->data)) {
+    return 'int';
+  }
+  if (is_array($xmlrpc_value->data)) {
+    // empty or integer-indexed arrays are 'array', string-indexed arrays 'struct'
+    return empty($xmlrpc_value->data) || range(0, count($xmlrpc_value->data) - 1) === array_keys($xmlrpc_value->data) ? 'array' : 'struct';
+  }
+  if (is_object($xmlrpc_value->data)) {
+    if (isset($xmlrpc_value->data->is_date)) {
+      return 'date';
+    }
+    if (isset($xmlrpc_value->data->is_base64)) {
+      return 'base64';
+    }
+    $xmlrpc_value->data = get_object_vars($xmlrpc_value->data);
+    return 'struct';
+  }
+  // default
+  return 'string';
+}
+
+/**
+ * Generates XML representing the given value.
+ *
+ * @param $xmlrpc_value
+ *   A value to be represented in XML.
+ *
+ * @return
+ *   XML representation of $xmlrpc_value.
+ */
+function xmlrpc_value_get_xml($xmlrpc_value) {
+  switch ($xmlrpc_value->type) {
+    case 'boolean':
+      return '<boolean>' . (($xmlrpc_value->data) ? '1' : '0') . '</boolean>';
+
+    case 'int':
+      return '<int>' . $xmlrpc_value->data . '</int>';
+
+    case 'double':
+      return '<double>' . $xmlrpc_value->data . '</double>';
+
+    case 'string':
+      // Note: we don't escape apostrophes because of the many blogging clients
+      // that don't support numerical entities (and XML in general) properly.
+      return '<string>' . htmlspecialchars($xmlrpc_value->data) . '</string>';
+
+    case 'array':
+      $return = '<array><data>' . "\n";
+      foreach ($xmlrpc_value->data as $item) {
+        $return .= '  <value>' . xmlrpc_value_get_xml($item) . "</value>\n";
+      }
+      $return .= '</data></array>';
+      return $return;
+
+    case 'struct':
+      $return = '<struct>' . "\n";
+      foreach ($xmlrpc_value->data as $name => $value) {
+        $return .= "  <member><name>" . check_plain($name) . "</name><value>";
+        $return .= xmlrpc_value_get_xml($value) . "</value></member>\n";
+      }
+      $return .= '</struct>';
+      return $return;
+
+    case 'date':
+      return xmlrpc_date_get_xml($xmlrpc_value->data);
+
+    case 'base64':
+      return xmlrpc_base64_get_xml($xmlrpc_value->data);
+  }
+  return FALSE;
+}
+
+/**
+ * Constructs an object representing an XML-RPC message.
+ *
+ * @param $message
+ *   A string containing an XML message.
+ *
+ * @return object
+ *   An XML-RPC object containing the message.
+ *
+ * @see http://www.xmlrpc.com/spec
+ */
+function xmlrpc_message($message) {
+  $xmlrpc_message = new stdClass();
+  // The stack used to keep track of the current array/struct
+  $xmlrpc_message->array_structs = array();
+  // The stack used to keep track of if things are structs or array
+  $xmlrpc_message->array_structs_types = array();
+  // A stack as well
+  $xmlrpc_message->current_struct_name = array();
+  $xmlrpc_message->message = $message;
+  return $xmlrpc_message;
+}
+
+/**
+ * Parses an XML-RPC message.
+ *
+ * If parsing fails, the faultCode and faultString will be added to the message
+ * object.
+ *
+ * @param $xmlrpc_message
+ *   An object generated by xmlrpc_message().
+ *
+ * @return
+ *   TRUE if parsing succeeded; FALSE otherwise.
+ */
+function xmlrpc_message_parse($xmlrpc_message) {
+  $xmlrpc_message->_parser = xml_parser_create();
+  // Set XML parser to take the case of tags into account.
+  xml_parser_set_option($xmlrpc_message->_parser, XML_OPTION_CASE_FOLDING, FALSE);
+  // Set XML parser callback functions
+  xml_set_element_handler($xmlrpc_message->_parser, 'xmlrpc_message_tag_open', 'xmlrpc_message_tag_close');
+  xml_set_character_data_handler($xmlrpc_message->_parser, 'xmlrpc_message_cdata');
+  xmlrpc_message_set($xmlrpc_message);
+  if (!xml_parse($xmlrpc_message->_parser, $xmlrpc_message->message)) {
+    return FALSE;
+  }
+  xml_parser_free($xmlrpc_message->_parser);
+
+  // Grab the error messages, if any.
+  $xmlrpc_message = xmlrpc_message_get();
+  if (!isset($xmlrpc_message->messagetype)) {
+    return FALSE;
+  }
+  elseif ($xmlrpc_message->messagetype == 'fault') {
+    $xmlrpc_message->fault_code = $xmlrpc_message->params[0]['faultCode'];
+    $xmlrpc_message->fault_string = $xmlrpc_message->params[0]['faultString'];
+  }
+  return TRUE;
+}
+
+/**
+ * Stores a copy of the most recent XML-RPC message object temporarily.
+ *
+ * @param $value
+ *   An XML-RPC message to store, or NULL to keep the last message.
+ *
+ * @return object
+ *   The most recently stored message.
+ *
+ * @see xmlrpc_message_get()
+ */
+function xmlrpc_message_set($value = NULL) {
+  static $xmlrpc_message;
+  if ($value) {
+    $xmlrpc_message = $value;
+  }
+  return $xmlrpc_message;
+}
+
+/**
+ * Returns the most recently stored XML-RPC message object.
+ *
+ * @return object
+ *   The most recently stored message.
+ *
+ * @see xmlrpc_message_set()
+ */
+function xmlrpc_message_get() {
+  return xmlrpc_message_set();
+}
+
+/**
+ * Handles opening tags for XML parsing in xmlrpc_message_parse().
+ */
+function xmlrpc_message_tag_open($parser, $tag, $attr) {
+  $xmlrpc_message = xmlrpc_message_get();
+  $xmlrpc_message->current_tag_contents = '';
+  $xmlrpc_message->last_open = $tag;
+  switch ($tag) {
+    case 'methodCall':
+    case 'methodResponse':
+    case 'fault':
+      $xmlrpc_message->messagetype = $tag;
+      break;
+
+    // Deal with stacks of arrays and structs
+    case 'data':
+      $xmlrpc_message->array_structs_types[] = 'array';
+      $xmlrpc_message->array_structs[] = array();
+      break;
+
+    case 'struct':
+      $xmlrpc_message->array_structs_types[] = 'struct';
+      $xmlrpc_message->array_structs[] = array();
+      break;
+  }
+  xmlrpc_message_set($xmlrpc_message);
+}
+
+/**
+ * Handles character data for XML parsing in xmlrpc_message_parse().
+ */
+function xmlrpc_message_cdata($parser, $cdata) {
+  $xmlrpc_message = xmlrpc_message_get();
+  $xmlrpc_message->current_tag_contents .= $cdata;
+  xmlrpc_message_set($xmlrpc_message);
+}
+
+/**
+ * Handles closing tags for XML parsing in xmlrpc_message_parse().
+ */
+function xmlrpc_message_tag_close($parser, $tag) {
+  $xmlrpc_message = xmlrpc_message_get();
+  $value_flag = FALSE;
+  switch ($tag) {
+    case 'int':
+    case 'i4':
+      $value = (int)trim($xmlrpc_message->current_tag_contents);
+      $value_flag = TRUE;
+      break;
+
+    case 'double':
+      $value = (double)trim($xmlrpc_message->current_tag_contents);
+      $value_flag = TRUE;
+      break;
+
+    case 'string':
+      $value = $xmlrpc_message->current_tag_contents;
+      $value_flag = TRUE;
+      break;
+
+    case 'dateTime.iso8601':
+      $value = xmlrpc_date(trim($xmlrpc_message->current_tag_contents));
+      // $value = $iso->getTimestamp();
+      $value_flag = TRUE;
+      break;
+
+    case 'value':
+      // If no type is indicated, the type is string
+      // We take special care for empty values
+      if (trim($xmlrpc_message->current_tag_contents) != '' || (isset($xmlrpc_message->last_open) && ($xmlrpc_message->last_open == 'value'))) {
+        $value = (string) $xmlrpc_message->current_tag_contents;
+        $value_flag = TRUE;
+      }
+      unset($xmlrpc_message->last_open);
+      break;
+
+    case 'boolean':
+      $value = (boolean)trim($xmlrpc_message->current_tag_contents);
+      $value_flag = TRUE;
+      break;
+
+    case 'base64':
+      $value = base64_decode(trim($xmlrpc_message->current_tag_contents));
+      $value_flag = TRUE;
+      break;
+
+    // Deal with stacks of arrays and structs
+    case 'data':
+    case 'struct':
+      $value = array_pop($xmlrpc_message->array_structs);
+      array_pop($xmlrpc_message->array_structs_types);
+      $value_flag = TRUE;
+      break;
+
+    case 'member':
+      array_pop($xmlrpc_message->current_struct_name);
+      break;
+
+    case 'name':
+      $xmlrpc_message->current_struct_name[] = trim($xmlrpc_message->current_tag_contents);
+      break;
+
+    case 'methodName':
+      $xmlrpc_message->methodname = trim($xmlrpc_message->current_tag_contents);
+      break;
+  }
+  if ($value_flag) {
+    if (count($xmlrpc_message->array_structs) > 0) {
+      // Add value to struct or array
+      if ($xmlrpc_message->array_structs_types[count($xmlrpc_message->array_structs_types) - 1] == 'struct') {
+        // Add to struct
+        $xmlrpc_message->array_structs[count($xmlrpc_message->array_structs) - 1][$xmlrpc_message->current_struct_name[count($xmlrpc_message->current_struct_name) - 1]] = $value;
+      }
+      else {
+        // Add to array
+        $xmlrpc_message->array_structs[count($xmlrpc_message->array_structs) - 1][] = $value;
+      }
+    }
+    else {
+      // Just add as a parameter
+      $xmlrpc_message->params[] = $value;
+    }
+  }
+  if (!in_array($tag, array("data", "struct", "member"))) {
+    $xmlrpc_message->current_tag_contents = '';
+  }
+  xmlrpc_message_set($xmlrpc_message);
+}
+
+/**
+ * Constructs an object representing an XML-RPC request.
+ *
+ * @param $method
+ *   The name of the method to be called.
+ * @param $args
+ *   An array of parameters to send with the method.
+ *
+ * @return object
+ *   An XML-RPC object representing the request.
+ */
+function xmlrpc_request($method, $args) {
+  $xmlrpc_request = new stdClass();
+  $xmlrpc_request->method = $method;
+  $xmlrpc_request->args = $args;
+  $xmlrpc_request->xml = <<<EOD
+<?xml version="1.0"?>
+<methodCall>
+<methodName>{$xmlrpc_request->method}</methodName>
+<params>
+
+EOD;
+  foreach ($xmlrpc_request->args as $arg) {
+    $xmlrpc_request->xml .= '<param><value>';
+    $v = xmlrpc_value($arg);
+    $xmlrpc_request->xml .= xmlrpc_value_get_xml($v);
+    $xmlrpc_request->xml .= "</value></param>\n";
+  }
+  $xmlrpc_request->xml .= '</params></methodCall>';
+  return $xmlrpc_request;
+}
+
+/**
+ * Generates, temporarily saves, and returns an XML-RPC error object.
+ *
+ * @param $code
+ *   The error code.
+ * @param $message
+ *   The error message.
+ * @param $reset
+ *   TRUE to empty the temporary error storage. Ignored if $code is supplied.
+ *
+ * @return object
+ *   An XML-RPC error object representing $code and $message, or the most
+ *   recently stored error object if omitted.
+ */
+function xmlrpc_error($code = NULL, $message = NULL, $reset = FALSE) {
+  static $xmlrpc_error;
+  if (isset($code)) {
+    $xmlrpc_error = new stdClass();
+    $xmlrpc_error->is_error = TRUE;
+    $xmlrpc_error->code = $code;
+    $xmlrpc_error->message = $message;
+  }
+  elseif ($reset) {
+    $xmlrpc_error = NULL;
+  }
+  return $xmlrpc_error;
+}
+
+/**
+ * Converts an XML-RPC error object into XML.
+ *
+ * @param $xmlrpc_error
+ *   The XML-RPC error object.
+ *
+ * @return string
+ *   An XML representation of the error as an XML methodResponse.
+ */
+function xmlrpc_error_get_xml($xmlrpc_error) {
+  return <<<EOD
+<methodResponse>
+  <fault>
+  <value>
+    <struct>
+    <member>
+      <name>faultCode</name>
+      <value><int>{$xmlrpc_error->code}</int></value>
+    </member>
+    <member>
+      <name>faultString</name>
+      <value><string>{$xmlrpc_error->message}</string></value>
+    </member>
+    </struct>
+  </value>
+  </fault>
+</methodResponse>
+
+EOD;
+}
+
+/**
+ * Converts a PHP or ISO date/time to an XML-RPC object.
+ *
+ * @param $time
+ *   A PHP timestamp or an ISO date-time string.
+ *
+ * @return object
+ *   An XML-RPC time/date object.
+ */
+function xmlrpc_date($time) {
+  $xmlrpc_date = new stdClass();
+  $xmlrpc_date->is_date = TRUE;
+  // $time can be a PHP timestamp or an ISO one
+  if (is_numeric($time)) {
+    $xmlrpc_date->year = gmdate('Y', $time);
+    $xmlrpc_date->month = gmdate('m', $time);
+    $xmlrpc_date->day = gmdate('d', $time);
+    $xmlrpc_date->hour = gmdate('H', $time);
+    $xmlrpc_date->minute = gmdate('i', $time);
+    $xmlrpc_date->second = gmdate('s', $time);
+    $xmlrpc_date->iso8601 = gmdate('Ymd\TH:i:s', $time);
+  }
+  else {
+    $xmlrpc_date->iso8601 = $time;
+    $time = str_replace(array('-', ':'), '', $time);
+    $xmlrpc_date->year = substr($time, 0, 4);
+    $xmlrpc_date->month = substr($time, 4, 2);
+    $xmlrpc_date->day = substr($time, 6, 2);
+    $xmlrpc_date->hour = substr($time, 9, 2);
+    $xmlrpc_date->minute = substr($time, 11, 2);
+    $xmlrpc_date->second = substr($time, 13, 2);
+  }
+  return $xmlrpc_date;
+}
+
+/**
+ * Converts an XML-RPC date-time object into XML.
+ *
+ * @param $xmlrpc_date
+ *   The XML-RPC date-time object.
+ *
+ * @return string
+ *   An XML representation of the date/time as XML.
+ */
+function xmlrpc_date_get_xml($xmlrpc_date) {
+  return '<dateTime.iso8601>' . $xmlrpc_date->year . $xmlrpc_date->month . $xmlrpc_date->day . 'T' . $xmlrpc_date->hour . ':' . $xmlrpc_date->minute . ':' . $xmlrpc_date->second . '</dateTime.iso8601>';
+}
+
+/**
+ * Returns an XML-RPC base 64 object.
+ *
+ * @param $data
+ *   Base 64 data to store in returned object.
+ *
+ * @return object
+ *   An XML-RPC base 64 object.
+ */
+function xmlrpc_base64($data) {
+  $xmlrpc_base64 = new stdClass();
+  $xmlrpc_base64->is_base64 = TRUE;
+  $xmlrpc_base64->data = $data;
+  return $xmlrpc_base64;
+}
+
+/**
+ * Converts an XML-RPC base 64 object into XML.
+ *
+ * @param $xmlrpc_base64
+ *   The XML-RPC base 64 object.
+ *
+ * @return string
+ *   An XML representation of the base 64 data as XML.
+ */
+function xmlrpc_base64_get_xml($xmlrpc_base64) {
+  return '<base64>' . base64_encode($xmlrpc_base64->data) . '</base64>';
+}
+
+/**
+ * Performs one or more XML-RPC requests.
+ *
+ * @param $url
+ *   An absolute URL of the XML-RPC endpoint, e.g.,
+ *   http://example.com/xmlrpc.php
+ * @param $args
+ *   An associative array whose keys are the methods to call and whose values
+ *   are the arguments to pass to the respective method. If multiple methods
+ *   are specified, a system.multicall is performed.
+ * @param $options
+ *   (optional) An array of options to pass along to drupal_http_request().
+ *
+ * @return
+ *   A single response (single request) or an array of responses (multicall
+ *   request). Each response is the return value of the method, just as if it
+ *   has been a local function call, on success, or FALSE on failure. If FALSE
+ *   is returned, see xmlrpc_errno() and xmlrpc_error_msg() to get more
+ *   information.
+ */
+function _xmlrpc($url, $args, $options = array()) {
+  xmlrpc_clear_error();
+  if (count($args) > 1) {
+    $multicall_args = array();
+    foreach ($args as $method => $call) {
+      $multicall_args[] = array('methodName' => $method, 'params' => $call);
+    }
+    $method = 'system.multicall';
+    $args = array($multicall_args);
+  }
+  else {
+    $method = key($args);
+    $args = $args[$method];
+  }
+  $xmlrpc_request = xmlrpc_request($method, $args);
+  // Required options which will replace any that are passed in.
+  $options['method'] = 'POST';
+  $options['headers']['Content-Type'] = 'text/xml';
+  $options['data'] = $xmlrpc_request->xml;
+  $result = drupal_http_request($url, $options);
+  if ($result->code != 200) {
+    xmlrpc_error($result->code, $result->error);
+    return FALSE;
+  }
+  $message = xmlrpc_message($result->data);
+  // Now parse what we've got back
+  if (!xmlrpc_message_parse($message)) {
+    // XML error
+    xmlrpc_error(-32700, t('Parse error. Not well formed'));
+    return FALSE;
+  }
+  // Is the message a fault?
+  if ($message->messagetype == 'fault') {
+    xmlrpc_error($message->fault_code, $message->fault_string);
+    return FALSE;
+  }
+  // We now know that the message is well-formed and a non-fault result.
+  if ($method == 'system.multicall') {
+    // Return per-method results or error objects.
+    $return = array();
+    foreach ($message->params[0] as $result) {
+      if (array_keys($result) == array(0)) {
+        $return[] = $result[0];
+      }
+      else {
+        $return[] = xmlrpc_error($result['faultCode'], $result['faultString']);
+      }
+    }
+  }
+  else {
+    $return = $message->params[0];
+  }
+  return $return;
+}
+
+/**
+ * Returns the last XML-RPC client error number.
+ */
+function xmlrpc_errno() {
+  $error = xmlrpc_error();
+  return ($error != NULL ? $error->code : NULL);
+}
+
+/**
+ * Returns the last XML-RPC client error message.
+ */
+function xmlrpc_error_msg() {
+  $error = xmlrpc_error();
+  return ($error != NULL ? $error->message : NULL);
+}
+
+/**
+ * Clears any previously-saved errors.
+ *
+ * @see xmlrpc_error()
+ */
+function xmlrpc_clear_error() {
+  xmlrpc_error(NULL, NULL, TRUE);
+}

+ 384 - 0
includes/xmlrpcs.inc

@@ -0,0 +1,384 @@
+<?php
+
+/**
+ * @file
+ * Provides API for defining and handling XML-RPC requests.
+ */
+
+/**
+ * Invokes XML-RPC methods on this server.
+ *
+ * @param array $callbacks
+ *   Array of external XML-RPC method names with the callbacks they map to.
+ */
+function xmlrpc_server($callbacks) {
+  $xmlrpc_server = new stdClass();
+  // Define built-in XML-RPC method names
+  $defaults = array(
+    'system.multicall' => 'xmlrpc_server_multicall',
+    array(
+      'system.methodSignature',
+      'xmlrpc_server_method_signature',
+      array('array', 'string'),
+      'Returns an array describing the return type and required parameters of a method.',
+    ),
+    array(
+      'system.getCapabilities',
+      'xmlrpc_server_get_capabilities',
+      array('struct'),
+      'Returns a struct describing the XML-RPC specifications supported by this server.',
+    ),
+    array(
+      'system.listMethods',
+      'xmlrpc_server_list_methods',
+      array('array'),
+      'Returns an array of available methods on this server.',
+    ),
+    array(
+      'system.methodHelp',
+      'xmlrpc_server_method_help',
+      array('string', 'string'),
+      'Returns a documentation string for the specified method.',
+    ),
+  );
+  // We build an array of all method names by combining the built-ins
+  // with those defined by modules implementing the _xmlrpc hook.
+  // Built-in methods are overridable.
+  $callbacks = array_merge($defaults, (array) $callbacks);
+  drupal_alter('xmlrpc', $callbacks);
+  foreach ($callbacks as $key => $callback) {
+    // we could check for is_array($callback)
+    if (is_int($key)) {
+      $method = $callback[0];
+      $xmlrpc_server->callbacks[$method] = $callback[1];
+      $xmlrpc_server->signatures[$method] = $callback[2];
+      $xmlrpc_server->help[$method] = $callback[3];
+    }
+    else {
+      $xmlrpc_server->callbacks[$key] = $callback;
+      $xmlrpc_server->signatures[$key] = '';
+      $xmlrpc_server->help[$key] = '';
+    }
+  }
+
+  $data = file_get_contents('php://input');
+  if (!$data) {
+    print 'XML-RPC server accepts POST requests only.';
+    drupal_exit();
+  }
+  $xmlrpc_server->message = xmlrpc_message($data);
+  if (!xmlrpc_message_parse($xmlrpc_server->message)) {
+    xmlrpc_server_error(-32700, t('Parse error. Request not well formed.'));
+  }
+  if ($xmlrpc_server->message->messagetype != 'methodCall') {
+    xmlrpc_server_error(-32600, t('Server error. Invalid XML-RPC. Request must be a methodCall.'));
+  }
+  if (!isset($xmlrpc_server->message->params)) {
+    $xmlrpc_server->message->params = array();
+  }
+  xmlrpc_server_set($xmlrpc_server);
+  $result = xmlrpc_server_call($xmlrpc_server, $xmlrpc_server->message->methodname, $xmlrpc_server->message->params);
+
+  if (is_object($result) && !empty($result->is_error)) {
+    xmlrpc_server_error($result);
+  }
+  // Encode the result
+  $r = xmlrpc_value($result);
+  // Create the XML
+  $xml = '
+<methodResponse>
+  <params>
+  <param>
+    <value>' . xmlrpc_value_get_xml($r) . '</value>
+  </param>
+  </params>
+</methodResponse>
+
+';
+  // Send it
+  xmlrpc_server_output($xml);
+}
+
+/**
+ * Throws an XML-RPC error.
+ *
+ * @param $error
+ *   An error object or integer error code.
+ * @param $message
+ *   (optional) The description of the error. Used only if an integer error
+ *   code was passed in.
+ */
+function xmlrpc_server_error($error, $message = FALSE) {
+  if ($message && !is_object($error)) {
+    $error = xmlrpc_error($error, $message);
+  }
+  xmlrpc_server_output(xmlrpc_error_get_xml($error));
+}
+
+/**
+ * Sends XML-RPC output to the browser.
+ *
+ * @param string $xml
+ *   XML to send to the browser.
+ */
+function xmlrpc_server_output($xml) {
+  $xml = '<?xml version="1.0"?>' . "\n" . $xml;
+  drupal_add_http_header('Content-Length', strlen($xml));
+  drupal_add_http_header('Content-Type', 'text/xml');
+  echo $xml;
+  drupal_exit();
+}
+
+/**
+ * Stores a copy of an XML-RPC request temporarily.
+ *
+ * @param object $xmlrpc_server
+ *   (optional) Request object created by xmlrpc_server(). Omit to leave the
+ *   previous server object saved.
+ *
+ * @return
+ *   The latest stored request.
+ *
+ * @see xmlrpc_server_get()
+ */
+function xmlrpc_server_set($xmlrpc_server = NULL) {
+  static $server;
+  if (!isset($server)) {
+    $server = $xmlrpc_server;
+  }
+  return $server;
+}
+
+/**
+ * Retrieves the latest stored XML-RPC request.
+ *
+ * @return object
+ *   The stored request.
+ *
+ * @see xmlrpc_server_set()
+ */
+function xmlrpc_server_get() {
+  return xmlrpc_server_set();
+}
+
+/**
+ * Dispatches an XML-RPC request and any parameters to the appropriate handler.
+ *
+ * @param object $xmlrpc_server
+ *   Object containing information about this XML-RPC server, the methods it
+ *   provides, their signatures, etc.
+ * @param string $methodname
+ *   The external XML-RPC method name; e.g., 'system.methodHelp'.
+ * @param array $args
+ *   Array containing any parameters that are to be sent along with the request.
+ *
+ * @return
+ *   The results of the call.
+ */
+function xmlrpc_server_call($xmlrpc_server, $methodname, $args) {
+  // Make sure parameters are in an array
+  if ($args && !is_array($args)) {
+    $args = array($args);
+  }
+  // Has this method been mapped to a Drupal function by us or by modules?
+  if (!isset($xmlrpc_server->callbacks[$methodname])) {
+    return xmlrpc_error(-32601, t('Server error. Requested method @methodname not specified.', array("@methodname" => $xmlrpc_server->message->methodname)));
+  }
+  $method = $xmlrpc_server->callbacks[$methodname];
+  $signature = $xmlrpc_server->signatures[$methodname];
+
+  // If the method has a signature, validate the request against the signature
+  if (is_array($signature)) {
+    $ok = TRUE;
+    $return_type = array_shift($signature);
+    // Check the number of arguments
+    if (count($args) != count($signature)) {
+      return xmlrpc_error(-32602, t('Server error. Wrong number of method parameters.'));
+    }
+    // Check the argument types
+    foreach ($signature as $key => $type) {
+      $arg = $args[$key];
+      switch ($type) {
+        case 'int':
+        case 'i4':
+          if (is_array($arg) || !is_int($arg)) {
+            $ok = FALSE;
+          }
+          break;
+
+        case 'base64':
+        case 'string':
+          if (!is_string($arg)) {
+            $ok = FALSE;
+          }
+          break;
+
+        case 'boolean':
+          if ($arg !== FALSE && $arg !== TRUE) {
+            $ok = FALSE;
+          }
+          break;
+
+        case 'float':
+        case 'double':
+          if (!is_float($arg)) {
+            $ok = FALSE;
+          }
+          break;
+
+        case 'date':
+        case 'dateTime.iso8601':
+          if (!$arg->is_date) {
+            $ok = FALSE;
+          }
+          break;
+      }
+      if (!$ok) {
+        return xmlrpc_error(-32602, t('Server error. Invalid method parameters.'));
+      }
+    }
+  }
+
+  if (!function_exists($method)) {
+    return xmlrpc_error(-32601, t('Server error. Requested function @method does not exist.', array("@method" => $method)));
+  }
+  // Call the mapped function
+  return call_user_func_array($method, $args);
+}
+
+/**
+ * Dispatches multiple XML-RPC requests.
+ *
+ * @param array $methodcalls
+ *   An array of XML-RPC requests to make. Each request is an array with the
+ *   following elements:
+ *   - methodName: Name of the method to invoke.
+ *   - params: Parameters to pass to the method.
+ *
+ * @return
+ *   An array of the results of each request.
+ *
+ * @see xmlrpc_server_call()
+ */
+function xmlrpc_server_multicall($methodcalls) {
+  // See http://www.xmlrpc.com/discuss/msgReader$1208
+  $return = array();
+  $xmlrpc_server = xmlrpc_server_get();
+  foreach ($methodcalls as $call) {
+    $ok = TRUE;
+    if (!isset($call['methodName']) || !isset($call['params'])) {
+      $result = xmlrpc_error(3, t('Invalid syntax for system.multicall.'));
+      $ok = FALSE;
+    }
+    $method = $call['methodName'];
+    $params = $call['params'];
+    if ($method == 'system.multicall') {
+      $result = xmlrpc_error(-32600, t('Recursive calls to system.multicall are forbidden.'));
+    }
+    elseif ($ok) {
+      $result = xmlrpc_server_call($xmlrpc_server, $method, $params);
+    }
+    if (is_object($result) && !empty($result->is_error)) {
+      $return[] = array(
+        'faultCode' => $result->code,
+        'faultString' => $result->message,
+      );
+    }
+    else {
+      $return[] = array($result);
+    }
+  }
+  return $return;
+}
+
+/**
+ * Lists the methods available on this XML-RPC server.
+ *
+ * XML-RPC method system.listMethods maps to this function.
+ *
+ * @return array
+ *   Array of the names of methods available on this server.
+ */
+function xmlrpc_server_list_methods() {
+  $xmlrpc_server = xmlrpc_server_get();
+  return array_keys($xmlrpc_server->callbacks);
+}
+
+/**
+ * Returns a list of the capabilities of this server.
+ *
+ * XML-RPC method system.getCapabilities maps to this function.
+ *
+ * @return array
+ *   Array of server capabilities.
+ *
+ * @see http://groups.yahoo.com/group/xml-rpc/message/2897
+ */
+function xmlrpc_server_get_capabilities() {
+  return array(
+    'xmlrpc' => array(
+      'specUrl' => 'http://www.xmlrpc.com/spec',
+      'specVersion' => 1,
+    ),
+    'faults_interop' => array(
+      'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
+      'specVersion' => 20010516,
+    ),
+    'system.multicall' => array(
+      'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
+      'specVersion' => 1,
+    ),
+    'introspection' => array(
+      'specUrl' => 'http://scripts.incutio.com/xmlrpc/introspection.html',
+      'specVersion' => 1,
+    ),
+  );
+}
+
+/**
+ * Returns one method signature for a function.
+ *
+ * This is the function mapped to the XML-RPC method system.methodSignature.
+ *
+ * A method signature is an array of the input and output types of a method. For
+ * instance, the method signature of this function is array('array', 'string'),
+ * because it takes an array and returns a string.
+ *
+ * @param string $methodname
+ *   Name of method to return a method signature for.
+ *
+ * @return array
+ *   An array of arrays of types, each of the arrays representing one method
+ *   signature of the function that $methodname maps to.
+ */
+function xmlrpc_server_method_signature($methodname) {
+  $xmlrpc_server = xmlrpc_server_get();
+  if (!isset($xmlrpc_server->callbacks[$methodname])) {
+    return xmlrpc_error(-32601, t('Server error. Requested method @methodname not specified.', array("@methodname" => $methodname)));
+  }
+  if (!is_array($xmlrpc_server->signatures[$methodname])) {
+    return xmlrpc_error(-32601, t('Server error. Requested method @methodname signature not specified.', array("@methodname" => $methodname)));
+  }
+  // We array of types
+  $return = array();
+  foreach ($xmlrpc_server->signatures[$methodname] as $type) {
+    $return[] = $type;
+  }
+  return array($return);
+}
+
+/**
+ * Returns the help for an XML-RPC method.
+ *
+ * XML-RPC method system.methodHelp maps to this function.
+ *
+ * @param string $method
+ *   Name of method for which we return a help string.
+ *
+ * @return string
+ *   Help text for $method.
+ */
+function xmlrpc_server_method_help($method) {
+  $xmlrpc_server = xmlrpc_server_get();
+  return $xmlrpc_server->help[$method];
+}

+ 21 - 0
index.php

@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @file
+ * The PHP page that serves all page requests on a Drupal installation.
+ *
+ * The routines here dispatch control to the appropriate handler, which then
+ * prints the appropriate page.
+ *
+ * All Drupal code is released under the GNU General Public License.
+ * See COPYRIGHT.txt and LICENSE.txt.
+ */
+
+/**
+ * Root directory of Drupal installation.
+ */
+define('DRUPAL_ROOT', getcwd());
+
+require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+menu_execute_active_handler();

+ 26 - 0
install.php

@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Initiates a browser-based installation of Drupal.
+ */
+
+/**
+ * Defines the root directory of the Drupal installation.
+ */
+define('DRUPAL_ROOT', getcwd());
+
+/**
+ * Global flag to indicate the site is in installation mode.
+ */
+define('MAINTENANCE_MODE', 'install');
+
+// Exit early if running an incompatible PHP version to avoid fatal errors.
+if (version_compare(PHP_VERSION, '5.2.4') < 0) {
+  print 'Your PHP installation is too old. Drupal requires at least PHP 5.2.4. See the <a href="http://drupal.org/requirements">system requirements</a> page for more information.';
+  exit;
+}
+
+// Start the installer.
+require_once DRUPAL_ROOT . '/includes/install.core.inc';
+install_drupal();

+ 622 - 0
misc/ajax.js

@@ -0,0 +1,622 @@
+(function ($) {
+
+/**
+ * Provides Ajax page updating via jQuery $.ajax (Asynchronous JavaScript and XML).
+ *
+ * Ajax is a method of making a request via JavaScript while viewing an HTML
+ * page. The request returns an array of commands encoded in JSON, which is
+ * then executed to make any changes that are necessary to the page.
+ *
+ * Drupal uses this file to enhance form elements with #ajax['path'] and
+ * #ajax['wrapper'] properties. If set, this file will automatically be included
+ * to provide Ajax capabilities.
+ */
+
+Drupal.ajax = Drupal.ajax || {};
+
+/**
+ * Attaches the Ajax behavior to each Ajax form element.
+ */
+Drupal.behaviors.AJAX = {
+  attach: function (context, settings) {
+    // Load all Ajax behaviors specified in the settings.
+    for (var base in settings.ajax) {
+      if (!$('#' + base + '.ajax-processed').length) {
+        var element_settings = settings.ajax[base];
+
+        if (typeof element_settings.selector == 'undefined') {
+          element_settings.selector = '#' + base;
+        }
+        $(element_settings.selector).each(function () {
+          element_settings.element = this;
+          Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
+        });
+
+        $('#' + base).addClass('ajax-processed');
+      }
+    }
+
+    // Bind Ajax behaviors to all items showing the class.
+    $('.use-ajax:not(.ajax-processed)').addClass('ajax-processed').each(function () {
+      var element_settings = {};
+      // Clicked links look better with the throbber than the progress bar.
+      element_settings.progress = { 'type': 'throbber' };
+
+      // For anchor tags, these will go to the target of the anchor rather
+      // than the usual location.
+      if ($(this).attr('href')) {
+        element_settings.url = $(this).attr('href');
+        element_settings.event = 'click';
+      }
+      var base = $(this).attr('id');
+      Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
+    });
+
+    // This class means to submit the form to the action using Ajax.
+    $('.use-ajax-submit:not(.ajax-processed)').addClass('ajax-processed').each(function () {
+      var element_settings = {};
+
+      // Ajax submits specified in this manner automatically submit to the
+      // normal form action.
+      element_settings.url = $(this.form).attr('action');
+      // Form submit button clicks need to tell the form what was clicked so
+      // it gets passed in the POST request.
+      element_settings.setClick = true;
+      // Form buttons use the 'click' event rather than mousedown.
+      element_settings.event = 'click';
+      // Clicked form buttons look better with the throbber than the progress bar.
+      element_settings.progress = { 'type': 'throbber' };
+
+      var base = $(this).attr('id');
+      Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
+    });
+  }
+};
+
+/**
+ * Ajax object.
+ *
+ * All Ajax objects on a page are accessible through the global Drupal.ajax
+ * object and are keyed by the submit button's ID. You can access them from
+ * your module's JavaScript file to override properties or functions.
+ *
+ * For example, if your Ajax enabled button has the ID 'edit-submit', you can
+ * redefine the function that is called to insert the new content like this
+ * (inside a Drupal.behaviors attach block):
+ * @code
+ *    Drupal.behaviors.myCustomAJAXStuff = {
+ *      attach: function (context, settings) {
+ *        Drupal.ajax['edit-submit'].commands.insert = function (ajax, response, status) {
+ *          new_content = $(response.data);
+ *          $('#my-wrapper').append(new_content);
+ *          alert('New content was appended to #my-wrapper');
+ *        }
+ *      }
+ *    };
+ * @endcode
+ */
+Drupal.ajax = function (base, element, element_settings) {
+  var defaults = {
+    url: 'system/ajax',
+    event: 'mousedown',
+    keypress: true,
+    selector: '#' + base,
+    effect: 'none',
+    speed: 'none',
+    method: 'replaceWith',
+    progress: {
+      type: 'throbber',
+      message: Drupal.t('Please wait...')
+    },
+    submit: {
+      'js': true
+    }
+  };
+
+  $.extend(this, defaults, element_settings);
+
+  this.element = element;
+  this.element_settings = element_settings;
+
+  // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let
+  // the server detect when it needs to degrade gracefully.
+  // There are five scenarios to check for:
+  // 1. /nojs/
+  // 2. /nojs$ - The end of a URL string.
+  // 3. /nojs? - Followed by a query (with clean URLs enabled).
+  //      E.g.: path/nojs?destination=foobar
+  // 4. /nojs& - Followed by a query (without clean URLs enabled).
+  //      E.g.: ?q=path/nojs&destination=foobar
+  // 5. /nojs# - Followed by a fragment.
+  //      E.g.: path/nojs#myfragment
+  this.url = element_settings.url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
+  this.wrapper = '#' + element_settings.wrapper;
+
+  // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
+  // bind Ajax to links as well.
+  if (this.element.form) {
+    this.form = $(this.element.form);
+  }
+
+  // Set the options for the ajaxSubmit function.
+  // The 'this' variable will not persist inside of the options object.
+  var ajax = this;
+  ajax.options = {
+    url: ajax.url,
+    data: ajax.submit,
+    beforeSerialize: function (element_settings, options) {
+      return ajax.beforeSerialize(element_settings, options);
+    },
+    beforeSubmit: function (form_values, element_settings, options) {
+      ajax.ajaxing = true;
+      return ajax.beforeSubmit(form_values, element_settings, options);
+    },
+    beforeSend: function (xmlhttprequest, options) {
+      ajax.ajaxing = true;
+      return ajax.beforeSend(xmlhttprequest, options);
+    },
+    success: function (response, status) {
+      // Sanity check for browser support (object expected).
+      // When using iFrame uploads, responses must be returned as a string.
+      if (typeof response == 'string') {
+        response = $.parseJSON(response);
+      }
+      return ajax.success(response, status);
+    },
+    complete: function (response, status) {
+      ajax.ajaxing = false;
+      if (status == 'error' || status == 'parsererror') {
+        return ajax.error(response, ajax.url);
+      }
+    },
+    dataType: 'json',
+    type: 'POST'
+  };
+
+  // Bind the ajaxSubmit function to the element event.
+  $(ajax.element).bind(element_settings.event, function (event) {
+    return ajax.eventResponse(this, event);
+  });
+
+  // If necessary, enable keyboard submission so that Ajax behaviors
+  // can be triggered through keyboard input as well as e.g. a mousedown
+  // action.
+  if (element_settings.keypress) {
+    $(ajax.element).keypress(function (event) {
+      return ajax.keypressResponse(this, event);
+    });
+  }
+
+  // If necessary, prevent the browser default action of an additional event.
+  // For example, prevent the browser default action of a click, even if the
+  // AJAX behavior binds to mousedown.
+  if (element_settings.prevent) {
+    $(ajax.element).bind(element_settings.prevent, false);
+  }
+};
+
+/**
+ * Handle a key press.
+ *
+ * The Ajax object will, if instructed, bind to a key press response. This
+ * will test to see if the key press is valid to trigger this event and
+ * if it is, trigger it for us and prevent other keypresses from triggering.
+ * In this case we're handling RETURN and SPACEBAR keypresses (event codes 13
+ * and 32. RETURN is often used to submit a form when in a textfield, and 
+ * SPACE is often used to activate an element without submitting. 
+ */
+Drupal.ajax.prototype.keypressResponse = function (element, event) {
+  // Create a synonym for this to reduce code confusion.
+  var ajax = this;
+
+  // Detect enter key and space bar and allow the standard response for them,
+  // except for form elements of type 'text' and 'textarea', where the 
+  // spacebar activation causes inappropriate activation if #ajax['keypress'] is 
+  // TRUE. On a text-type widget a space should always be a space.
+  if (event.which == 13 || (event.which == 32 && element.type != 'text' && element.type != 'textarea')) {
+    $(ajax.element_settings.element).trigger(ajax.element_settings.event);
+    return false;
+  }
+};
+
+/**
+ * Handle an event that triggers an Ajax response.
+ *
+ * When an event that triggers an Ajax response happens, this method will
+ * perform the actual Ajax call. It is bound to the event using
+ * bind() in the constructor, and it uses the options specified on the
+ * ajax object.
+ */
+Drupal.ajax.prototype.eventResponse = function (element, event) {
+  // Create a synonym for this to reduce code confusion.
+  var ajax = this;
+
+  // Do not perform another ajax command if one is already in progress.
+  if (ajax.ajaxing) {
+    return false;
+  }
+
+  try {
+    if (ajax.form) {
+      // If setClick is set, we must set this to ensure that the button's
+      // value is passed.
+      if (ajax.setClick) {
+        // Mark the clicked button. 'form.clk' is a special variable for
+        // ajaxSubmit that tells the system which element got clicked to
+        // trigger the submit. Without it there would be no 'op' or
+        // equivalent.
+        element.form.clk = element;
+      }
+
+      ajax.form.ajaxSubmit(ajax.options);
+    }
+    else {
+      ajax.beforeSerialize(ajax.element, ajax.options);
+      $.ajax(ajax.options);
+    }
+  }
+  catch (e) {
+    // Unset the ajax.ajaxing flag here because it won't be unset during
+    // the complete response.
+    ajax.ajaxing = false;
+    alert("An error occurred while attempting to process " + ajax.options.url + ": " + e.message);
+  }
+
+  // For radio/checkbox, allow the default event. On IE, this means letting
+  // it actually check the box.
+  if (typeof element.type != 'undefined' && (element.type == 'checkbox' || element.type == 'radio')) {
+    return true;
+  }
+  else {
+    return false;
+  }
+
+};
+
+/**
+ * Handler for the form serialization.
+ *
+ * Runs before the beforeSend() handler (see below), and unlike that one, runs
+ * before field data is collected.
+ */
+Drupal.ajax.prototype.beforeSerialize = function (element, options) {
+  // Allow detaching behaviors to update field values before collecting them.
+  // This is only needed when field values are added to the POST data, so only
+  // when there is a form such that this.form.ajaxSubmit() is used instead of
+  // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize()
+  // isn't called, but don't rely on that: explicitly check this.form.
+  if (this.form) {
+    var settings = this.settings || Drupal.settings;
+    Drupal.detachBehaviors(this.form, settings, 'serialize');
+  }
+
+  // Prevent duplicate HTML ids in the returned markup.
+  // @see drupal_html_id()
+  options.data['ajax_html_ids[]'] = [];
+  $('[id]').each(function () {
+    options.data['ajax_html_ids[]'].push(this.id);
+  });
+
+  // Allow Drupal to return new JavaScript and CSS files to load without
+  // returning the ones already loaded.
+  // @see ajax_base_page_theme()
+  // @see drupal_get_css()
+  // @see drupal_get_js()
+  options.data['ajax_page_state[theme]'] = Drupal.settings.ajaxPageState.theme;
+  options.data['ajax_page_state[theme_token]'] = Drupal.settings.ajaxPageState.theme_token;
+  for (var key in Drupal.settings.ajaxPageState.css) {
+    options.data['ajax_page_state[css][' + key + ']'] = 1;
+  }
+  for (var key in Drupal.settings.ajaxPageState.js) {
+    options.data['ajax_page_state[js][' + key + ']'] = 1;
+  }
+};
+
+/**
+ * Modify form values prior to form submission.
+ */
+Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) {
+  // This function is left empty to make it simple to override for modules
+  // that wish to add functionality here.
+};
+
+/**
+ * Prepare the Ajax request before it is sent.
+ */
+Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
+  // For forms without file inputs, the jQuery Form plugin serializes the form
+  // values, and then calls jQuery's $.ajax() function, which invokes this
+  // handler. In this circumstance, options.extraData is never used. For forms
+  // with file inputs, the jQuery Form plugin uses the browser's normal form
+  // submission mechanism, but captures the response in a hidden IFRAME. In this
+  // circumstance, it calls this handler first, and then appends hidden fields
+  // to the form to submit the values in options.extraData. There is no simple
+  // way to know which submission mechanism will be used, so we add to extraData
+  // regardless, and allow it to be ignored in the former case.
+  if (this.form) {
+    options.extraData = options.extraData || {};
+
+    // Let the server know when the IFRAME submission mechanism is used. The
+    // server can use this information to wrap the JSON response in a TEXTAREA,
+    // as per http://jquery.malsup.com/form/#file-upload.
+    options.extraData.ajax_iframe_upload = '1';
+
+    // The triggering element is about to be disabled (see below), but if it
+    // contains a value (e.g., a checkbox, textfield, select, etc.), ensure that
+    // value is included in the submission. As per above, submissions that use
+    // $.ajax() are already serialized prior to the element being disabled, so
+    // this is only needed for IFRAME submissions.
+    var v = $.fieldValue(this.element);
+    if (v !== null) {
+      options.extraData[this.element.name] = v;
+    }
+  }
+
+  // Disable the element that received the change to prevent user interface
+  // interaction while the Ajax request is in progress. ajax.ajaxing prevents
+  // the element from triggering a new request, but does not prevent the user
+  // from changing its value.
+  $(this.element).addClass('progress-disabled').attr('disabled', true);
+
+  // Insert progressbar or throbber.
+  if (this.progress.type == 'bar') {
+    var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
+    if (this.progress.message) {
+      progressBar.setProgress(-1, this.progress.message);
+    }
+    if (this.progress.url) {
+      progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500);
+    }
+    this.progress.element = $(progressBar.element).addClass('ajax-progress ajax-progress-bar');
+    this.progress.object = progressBar;
+    $(this.element).after(this.progress.element);
+  }
+  else if (this.progress.type == 'throbber') {
+    this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
+    if (this.progress.message) {
+      $('.throbber', this.progress.element).after('<div class="message">' + this.progress.message + '</div>');
+    }
+    $(this.element).after(this.progress.element);
+  }
+};
+
+/**
+ * Handler for the form redirection completion.
+ */
+Drupal.ajax.prototype.success = function (response, status) {
+  // Remove the progress element.
+  if (this.progress.element) {
+    $(this.progress.element).remove();
+  }
+  if (this.progress.object) {
+    this.progress.object.stopMonitoring();
+  }
+  $(this.element).removeClass('progress-disabled').removeAttr('disabled');
+
+  Drupal.freezeHeight();
+
+  for (var i in response) {
+    if (response.hasOwnProperty(i) && response[i]['command'] && this.commands[response[i]['command']]) {
+      this.commands[response[i]['command']](this, response[i], status);
+    }
+  }
+
+  // Reattach behaviors, if they were detached in beforeSerialize(). The
+  // attachBehaviors() called on the new content from processing the response
+  // commands is not sufficient, because behaviors from the entire form need
+  // to be reattached.
+  if (this.form) {
+    var settings = this.settings || Drupal.settings;
+    Drupal.attachBehaviors(this.form, settings);
+  }
+
+  Drupal.unfreezeHeight();
+
+  // Remove any response-specific settings so they don't get used on the next
+  // call by mistake.
+  this.settings = null;
+};
+
+/**
+ * Build an effect object which tells us how to apply the effect when adding new HTML.
+ */
+Drupal.ajax.prototype.getEffect = function (response) {
+  var type = response.effect || this.effect;
+  var speed = response.speed || this.speed;
+
+  var effect = {};
+  if (type == 'none') {
+    effect.showEffect = 'show';
+    effect.hideEffect = 'hide';
+    effect.showSpeed = '';
+  }
+  else if (type == 'fade') {
+    effect.showEffect = 'fadeIn';
+    effect.hideEffect = 'fadeOut';
+    effect.showSpeed = speed;
+  }
+  else {
+    effect.showEffect = type + 'Toggle';
+    effect.hideEffect = type + 'Toggle';
+    effect.showSpeed = speed;
+  }
+
+  return effect;
+};
+
+/**
+ * Handler for the form redirection error.
+ */
+Drupal.ajax.prototype.error = function (response, uri) {
+  alert(Drupal.ajaxError(response, uri));
+  // Remove the progress element.
+  if (this.progress.element) {
+    $(this.progress.element).remove();
+  }
+  if (this.progress.object) {
+    this.progress.object.stopMonitoring();
+  }
+  // Undo hide.
+  $(this.wrapper).show();
+  // Re-enable the element.
+  $(this.element).removeClass('progress-disabled').removeAttr('disabled');
+  // Reattach behaviors, if they were detached in beforeSerialize().
+  if (this.form) {
+    var settings = response.settings || this.settings || Drupal.settings;
+    Drupal.attachBehaviors(this.form, settings);
+  }
+};
+
+/**
+ * Provide a series of commands that the server can request the client perform.
+ */
+Drupal.ajax.prototype.commands = {
+  /**
+   * Command to insert new content into the DOM.
+   */
+  insert: function (ajax, response, status) {
+    // Get information from the response. If it is not there, default to
+    // our presets.
+    var wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
+    var method = response.method || ajax.method;
+    var effect = ajax.getEffect(response);
+
+    // We don't know what response.data contains: it might be a string of text
+    // without HTML, so don't rely on jQuery correctly iterpreting
+    // $(response.data) as new HTML rather than a CSS selector. Also, if
+    // response.data contains top-level text nodes, they get lost with either
+    // $(response.data) or $('<div></div>').replaceWith(response.data).
+    var new_content_wrapped = $('<div></div>').html(response.data);
+    var new_content = new_content_wrapped.contents();
+
+    // For legacy reasons, the effects processing code assumes that new_content
+    // consists of a single top-level element. Also, it has not been
+    // sufficiently tested whether attachBehaviors() can be successfully called
+    // with a context object that includes top-level text nodes. However, to
+    // give developers full control of the HTML appearing in the page, and to
+    // enable Ajax content to be inserted in places where DIV elements are not
+    // allowed (e.g., within TABLE, TR, and SPAN parents), we check if the new
+    // content satisfies the requirement of a single top-level element, and
+    // only use the container DIV created above when it doesn't. For more
+    // information, please see http://drupal.org/node/736066.
+    if (new_content.length != 1 || new_content.get(0).nodeType != 1) {
+      new_content = new_content_wrapped;
+    }
+
+    // If removing content from the wrapper, detach behaviors first.
+    switch (method) {
+      case 'html':
+      case 'replaceWith':
+      case 'replaceAll':
+      case 'empty':
+      case 'remove':
+        var settings = response.settings || ajax.settings || Drupal.settings;
+        Drupal.detachBehaviors(wrapper, settings);
+    }
+
+    // Add the new content to the page.
+    wrapper[method](new_content);
+
+    // Immediately hide the new content if we're using any effects.
+    if (effect.showEffect != 'show') {
+      new_content.hide();
+    }
+
+    // Determine which effect to use and what content will receive the
+    // effect, then show the new content.
+    if ($('.ajax-new-content', new_content).length > 0) {
+      $('.ajax-new-content', new_content).hide();
+      new_content.show();
+      $('.ajax-new-content', new_content)[effect.showEffect](effect.showSpeed);
+    }
+    else if (effect.showEffect != 'show') {
+      new_content[effect.showEffect](effect.showSpeed);
+    }
+
+    // Attach all JavaScript behaviors to the new content, if it was successfully
+    // added to the page, this if statement allows #ajax['wrapper'] to be
+    // optional.
+    if (new_content.parents('html').length > 0) {
+      // Apply any settings from the returned JSON if available.
+      var settings = response.settings || ajax.settings || Drupal.settings;
+      Drupal.attachBehaviors(new_content, settings);
+    }
+  },
+
+  /**
+   * Command to remove a chunk from the page.
+   */
+  remove: function (ajax, response, status) {
+    var settings = response.settings || ajax.settings || Drupal.settings;
+    Drupal.detachBehaviors($(response.selector), settings);
+    $(response.selector).remove();
+  },
+
+  /**
+   * Command to mark a chunk changed.
+   */
+  changed: function (ajax, response, status) {
+    if (!$(response.selector).hasClass('ajax-changed')) {
+      $(response.selector).addClass('ajax-changed');
+      if (response.asterisk) {
+        $(response.selector).find(response.asterisk).append(' <span class="ajax-changed">*</span> ');
+      }
+    }
+  },
+
+  /**
+   * Command to provide an alert.
+   */
+  alert: function (ajax, response, status) {
+    alert(response.text, response.title);
+  },
+
+  /**
+   * Command to provide the jQuery css() function.
+   */
+  css: function (ajax, response, status) {
+    $(response.selector).css(response.argument);
+  },
+
+  /**
+   * Command to set the settings that will be used for other commands in this response.
+   */
+  settings: function (ajax, response, status) {
+    if (response.merge) {
+      $.extend(true, Drupal.settings, response.settings);
+    }
+    else {
+      ajax.settings = response.settings;
+    }
+  },
+
+  /**
+   * Command to attach data using jQuery's data API.
+   */
+  data: function (ajax, response, status) {
+    $(response.selector).data(response.name, response.value);
+  },
+
+  /**
+   * Command to apply a jQuery method.
+   */
+  invoke: function (ajax, response, status) {
+    var $element = $(response.selector);
+    $element[response.method].apply($element, response.arguments);
+  },
+
+  /**
+   * Command to restripe a table.
+   */
+  restripe: function (ajax, response, status) {
+    // :even and :odd are reversed because jQuery counts from 0 and
+    // we count from 1, so we're out of sync.
+    // Match immediate children of the parent element to allow nesting.
+    $('> tbody > tr:visible, > tr:visible', $(response.selector))
+      .removeClass('odd even')
+      .filter(':even').addClass('odd').end()
+      .filter(':odd').addClass('even');
+  }
+};
+
+})(jQuery);

BIN
misc/arrow-asc.png


BIN
misc/arrow-desc.png


+ 27 - 0
misc/authorize.js

@@ -0,0 +1,27 @@
+
+/**
+ * @file
+ * Conditionally hide or show the appropriate settings and saved defaults
+ * on the file transfer connection settings form used by authorize.php.
+ */
+
+(function ($) {
+
+Drupal.behaviors.authorizeFileTransferForm = {
+  attach: function(context) {
+    $('#edit-connection-settings-authorize-filetransfer-default').change(function() {
+      $('.filetransfer').hide().filter('.filetransfer-' + $(this).val()).show();
+    });
+    $('.filetransfer').hide().filter('.filetransfer-' + $('#edit-connection-settings-authorize-filetransfer-default').val()).show();
+
+    // Removes the float on the select box (used for non-JS interface).
+    if ($('.connection-settings-update-filetransfer-default-wrapper').length > 0) {
+      $('.connection-settings-update-filetransfer-default-wrapper').css('float', 'none');
+    }
+    // Hides the submit button for non-js users.
+    $('#edit-submit-connection').hide();
+    $('#edit-submit-process').show();
+  }
+};
+
+})(jQuery);

+ 324 - 0
misc/autocomplete.js

@@ -0,0 +1,324 @@
+(function ($) {
+
+/**
+ * Attaches the autocomplete behavior to all required fields.
+ */
+Drupal.behaviors.autocomplete = {
+  attach: function (context, settings) {
+    var acdb = [];
+    $('input.autocomplete', context).once('autocomplete', function () {
+      var uri = this.value;
+      if (!acdb[uri]) {
+        acdb[uri] = new Drupal.ACDB(uri);
+      }
+      var $input = $('#' + this.id.substr(0, this.id.length - 13))
+        .attr('autocomplete', 'OFF')
+        .attr('aria-autocomplete', 'list');
+      $($input[0].form).submit(Drupal.autocompleteSubmit);
+      $input.parent()
+        .attr('role', 'application')
+        .append($('<span class="element-invisible" aria-live="assertive"></span>')
+          .attr('id', $input.attr('id') + '-autocomplete-aria-live')
+        );
+      new Drupal.jsAC($input, acdb[uri]);
+    });
+  }
+};
+
+/**
+ * Prevents the form from submitting if the suggestions popup is open
+ * and closes the suggestions popup when doing so.
+ */
+Drupal.autocompleteSubmit = function () {
+  return $('#autocomplete').each(function () {
+    this.owner.hidePopup();
+  }).length == 0;
+};
+
+/**
+ * An AutoComplete object.
+ */
+Drupal.jsAC = function ($input, db) {
+  var ac = this;
+  this.input = $input[0];
+  this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live');
+  this.db = db;
+
+  $input
+    .keydown(function (event) { return ac.onkeydown(this, event); })
+    .keyup(function (event) { ac.onkeyup(this, event); })
+    .blur(function () { ac.hidePopup(); ac.db.cancel(); });
+
+};
+
+/**
+ * Handler for the "keydown" event.
+ */
+Drupal.jsAC.prototype.onkeydown = function (input, e) {
+  if (!e) {
+    e = window.event;
+  }
+  switch (e.keyCode) {
+    case 40: // down arrow.
+      this.selectDown();
+      return false;
+    case 38: // up arrow.
+      this.selectUp();
+      return false;
+    default: // All other keys.
+      return true;
+  }
+};
+
+/**
+ * Handler for the "keyup" event.
+ */
+Drupal.jsAC.prototype.onkeyup = function (input, e) {
+  if (!e) {
+    e = window.event;
+  }
+  switch (e.keyCode) {
+    case 16: // Shift.
+    case 17: // Ctrl.
+    case 18: // Alt.
+    case 20: // Caps lock.
+    case 33: // Page up.
+    case 34: // Page down.
+    case 35: // End.
+    case 36: // Home.
+    case 37: // Left arrow.
+    case 38: // Up arrow.
+    case 39: // Right arrow.
+    case 40: // Down arrow.
+      return true;
+
+    case 9:  // Tab.
+    case 13: // Enter.
+    case 27: // Esc.
+      this.hidePopup(e.keyCode);
+      return true;
+
+    default: // All other keys.
+      if (input.value.length > 0 && !input.readOnly) {
+        this.populatePopup();
+      }
+      else {
+        this.hidePopup(e.keyCode);
+      }
+      return true;
+  }
+};
+
+/**
+ * Puts the currently highlighted suggestion into the autocomplete field.
+ */
+Drupal.jsAC.prototype.select = function (node) {
+  this.input.value = $(node).data('autocompleteValue');
+};
+
+/**
+ * Highlights the next suggestion.
+ */
+Drupal.jsAC.prototype.selectDown = function () {
+  if (this.selected && this.selected.nextSibling) {
+    this.highlight(this.selected.nextSibling);
+  }
+  else if (this.popup) {
+    var lis = $('li', this.popup);
+    if (lis.length > 0) {
+      this.highlight(lis.get(0));
+    }
+  }
+};
+
+/**
+ * Highlights the previous suggestion.
+ */
+Drupal.jsAC.prototype.selectUp = function () {
+  if (this.selected && this.selected.previousSibling) {
+    this.highlight(this.selected.previousSibling);
+  }
+};
+
+/**
+ * Highlights a suggestion.
+ */
+Drupal.jsAC.prototype.highlight = function (node) {
+  if (this.selected) {
+    $(this.selected).removeClass('selected');
+  }
+  $(node).addClass('selected');
+  this.selected = node;
+  $(this.ariaLive).html($(this.selected).html());
+};
+
+/**
+ * Unhighlights a suggestion.
+ */
+Drupal.jsAC.prototype.unhighlight = function (node) {
+  $(node).removeClass('selected');
+  this.selected = false;
+  $(this.ariaLive).empty();
+};
+
+/**
+ * Hides the autocomplete suggestions.
+ */
+Drupal.jsAC.prototype.hidePopup = function (keycode) {
+  // Select item if the right key or mousebutton was pressed.
+  if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
+    this.input.value = $(this.selected).data('autocompleteValue');
+  }
+  // Hide popup.
+  var popup = this.popup;
+  if (popup) {
+    this.popup = null;
+    $(popup).fadeOut('fast', function () { $(popup).remove(); });
+  }
+  this.selected = false;
+  $(this.ariaLive).empty();
+};
+
+/**
+ * Positions the suggestions popup and starts a search.
+ */
+Drupal.jsAC.prototype.populatePopup = function () {
+  var $input = $(this.input);
+  var position = $input.position();
+  // Show popup.
+  if (this.popup) {
+    $(this.popup).remove();
+  }
+  this.selected = false;
+  this.popup = $('<div id="autocomplete"></div>')[0];
+  this.popup.owner = this;
+  $(this.popup).css({
+    top: parseInt(position.top + this.input.offsetHeight, 10) + 'px',
+    left: parseInt(position.left, 10) + 'px',
+    width: $input.innerWidth() + 'px',
+    display: 'none'
+  });
+  $input.before(this.popup);
+
+  // Do search.
+  this.db.owner = this;
+  this.db.search(this.input.value);
+};
+
+/**
+ * Fills the suggestion popup with any matches received.
+ */
+Drupal.jsAC.prototype.found = function (matches) {
+  // If no value in the textfield, do not show the popup.
+  if (!this.input.value.length) {
+    return false;
+  }
+
+  // Prepare matches.
+  var ul = $('<ul></ul>');
+  var ac = this;
+  for (key in matches) {
+    $('<li></li>')
+      .html($('<div></div>').html(matches[key]))
+      .mousedown(function () { ac.select(this); })
+      .mouseover(function () { ac.highlight(this); })
+      .mouseout(function () { ac.unhighlight(this); })
+      .data('autocompleteValue', key)
+      .appendTo(ul);
+  }
+
+  // Show popup with matches, if any.
+  if (this.popup) {
+    if (ul.children().length) {
+      $(this.popup).empty().append(ul).show();
+      $(this.ariaLive).html(Drupal.t('Autocomplete popup'));
+    }
+    else {
+      $(this.popup).css({ visibility: 'hidden' });
+      this.hidePopup();
+    }
+  }
+};
+
+Drupal.jsAC.prototype.setStatus = function (status) {
+  switch (status) {
+    case 'begin':
+      $(this.input).addClass('throbbing');
+      $(this.ariaLive).html(Drupal.t('Searching for matches...'));
+      break;
+    case 'cancel':
+    case 'error':
+    case 'found':
+      $(this.input).removeClass('throbbing');
+      break;
+  }
+};
+
+/**
+ * An AutoComplete DataBase object.
+ */
+Drupal.ACDB = function (uri) {
+  this.uri = uri;
+  this.delay = 300;
+  this.cache = {};
+};
+
+/**
+ * Performs a cached and delayed search.
+ */
+Drupal.ACDB.prototype.search = function (searchString) {
+  var db = this;
+  this.searchString = searchString;
+
+  // See if this string needs to be searched for anyway.
+  searchString = searchString.replace(/^\s+|\s+$/, '');
+  if (searchString.length <= 0 ||
+    searchString.charAt(searchString.length - 1) == ',') {
+    return;
+  }
+
+  // See if this key has been searched for before.
+  if (this.cache[searchString]) {
+    return this.owner.found(this.cache[searchString]);
+  }
+
+  // Initiate delayed search.
+  if (this.timer) {
+    clearTimeout(this.timer);
+  }
+  this.timer = setTimeout(function () {
+    db.owner.setStatus('begin');
+
+    // Ajax GET request for autocompletion. We use Drupal.encodePath instead of
+    // encodeURIComponent to allow autocomplete search terms to contain slashes.
+    $.ajax({
+      type: 'GET',
+      url: db.uri + '/' + Drupal.encodePath(searchString),
+      dataType: 'json',
+      success: function (matches) {
+        if (typeof matches.status == 'undefined' || matches.status != 0) {
+          db.cache[searchString] = matches;
+          // Verify if these are still the matches the user wants to see.
+          if (db.searchString == searchString) {
+            db.owner.found(matches);
+          }
+          db.owner.setStatus('found');
+        }
+      },
+      error: function (xmlhttp) {
+        alert(Drupal.ajaxError(xmlhttp, db.uri));
+      }
+    });
+  }, this.delay);
+};
+
+/**
+ * Cancels the current autocomplete request.
+ */
+Drupal.ACDB.prototype.cancel = function () {
+  if (this.owner) this.owner.setStatus('cancel');
+  if (this.timer) clearTimeout(this.timer);
+  this.searchString = '';
+};
+
+})(jQuery);

+ 32 - 0
misc/batch.js

@@ -0,0 +1,32 @@
+(function ($) {
+
+/**
+ * Attaches the batch behavior to progress bars.
+ */
+Drupal.behaviors.batch = {
+  attach: function (context, settings) {
+    $('#progress', context).once('batch', function () {
+      var holder = $(this);
+
+      // Success: redirect to the summary.
+      var updateCallback = function (progress, status, pb) {
+        if (progress == 100) {
+          pb.stopMonitoring();
+          window.location = settings.batch.uri + '&op=finished';
+        }
+      };
+
+      var errorCallback = function (pb) {
+        holder.prepend($('<p class="error"></p>').html(settings.batch.errorMessage));
+        $('#wait').hide();
+      };
+
+      var progress = new Drupal.progressBar('updateprogress', updateCallback, 'POST', errorCallback);
+      progress.setProgress(-1, settings.batch.initMessage);
+      holder.append(progress.element);
+      progress.startMonitoring(settings.batch.uri + '&op=do', 10);
+    });
+  }
+};
+
+})(jQuery);

+ 103 - 0
misc/collapse.js

@@ -0,0 +1,103 @@
+(function ($) {
+
+/**
+ * Toggle the visibility of a fieldset using smooth animations.
+ */
+Drupal.toggleFieldset = function (fieldset) {
+  var $fieldset = $(fieldset);
+  if ($fieldset.is('.collapsed')) {
+    var $content = $('> .fieldset-wrapper', fieldset).hide();
+    $fieldset
+      .removeClass('collapsed')
+      .trigger({ type: 'collapsed', value: false })
+      .find('> legend span.fieldset-legend-prefix').html(Drupal.t('Hide'));
+    $content.slideDown({
+      duration: 'fast',
+      easing: 'linear',
+      complete: function () {
+        Drupal.collapseScrollIntoView(fieldset);
+        fieldset.animating = false;
+      },
+      step: function () {
+        // Scroll the fieldset into view.
+        Drupal.collapseScrollIntoView(fieldset);
+      }
+    });
+  }
+  else {
+    $fieldset.trigger({ type: 'collapsed', value: true });
+    $('> .fieldset-wrapper', fieldset).slideUp('fast', function () {
+      $fieldset
+        .addClass('collapsed')
+        .find('> legend span.fieldset-legend-prefix').html(Drupal.t('Show'));
+      fieldset.animating = false;
+    });
+  }
+};
+
+/**
+ * Scroll a given fieldset into view as much as possible.
+ */
+Drupal.collapseScrollIntoView = function (node) {
+  var h = document.documentElement.clientHeight || document.body.clientHeight || 0;
+  var offset = document.documentElement.scrollTop || document.body.scrollTop || 0;
+  var posY = $(node).offset().top;
+  var fudge = 55;
+  if (posY + node.offsetHeight + fudge > h + offset) {
+    if (node.offsetHeight > h) {
+      window.scrollTo(0, posY);
+    }
+    else {
+      window.scrollTo(0, posY + node.offsetHeight - h + fudge);
+    }
+  }
+};
+
+Drupal.behaviors.collapse = {
+  attach: function (context, settings) {
+    $('fieldset.collapsible', context).once('collapse', function () {
+      var $fieldset = $(this);
+      // Expand fieldset if there are errors inside, or if it contains an
+      // element that is targeted by the URI fragment identifier.
+      var anchor = location.hash && location.hash != '#' ? ', ' + location.hash : '';
+      if ($fieldset.find('.error' + anchor).length) {
+        $fieldset.removeClass('collapsed');
+      }
+
+      var summary = $('<span class="summary"></span>');
+      $fieldset.
+        bind('summaryUpdated', function () {
+          var text = $.trim($fieldset.drupalGetSummary());
+          summary.html(text ? ' (' + text + ')' : '');
+        })
+        .trigger('summaryUpdated');
+
+      // Turn the legend into a clickable link, but retain span.fieldset-legend
+      // for CSS positioning.
+      var $legend = $('> legend .fieldset-legend', this);
+
+      $('<span class="fieldset-legend-prefix element-invisible"></span>')
+        .append($fieldset.hasClass('collapsed') ? Drupal.t('Show') : Drupal.t('Hide'))
+        .prependTo($legend)
+        .after(' ');
+
+      // .wrapInner() does not retain bound events.
+      var $link = $('<a class="fieldset-title" href="#"></a>')
+        .prepend($legend.contents())
+        .appendTo($legend)
+        .click(function () {
+          var fieldset = $fieldset.get(0);
+          // Don't animate multiple times.
+          if (!fieldset.animating) {
+            fieldset.animating = true;
+            Drupal.toggleFieldset(fieldset);
+          }
+          return false;
+        });
+
+      $legend.append(summary);
+    });
+  }
+};
+
+})(jQuery);

BIN
misc/configure.png


BIN
misc/draggable.png


+ 433 - 0
misc/drupal.js

@@ -0,0 +1,433 @@
+
+var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'locale': {} };
+
+// Allow other JavaScript libraries to use $.
+jQuery.noConflict();
+
+(function ($) {
+
+/**
+ * Override jQuery.fn.init to guard against XSS attacks.
+ *
+ * See http://bugs.jquery.com/ticket/9521
+ */
+var jquery_init = $.fn.init;
+$.fn.init = function (selector, context, rootjQuery) {
+  // If the string contains a "#" before a "<", treat it as invalid HTML.
+  if (selector && typeof selector === 'string') {
+    var hash_position = selector.indexOf('#');
+    if (hash_position >= 0) {
+      var bracket_position = selector.indexOf('<');
+      if (bracket_position > hash_position) {
+        throw 'Syntax error, unrecognized expression: ' + selector;
+      }
+    }
+  }
+  return jquery_init.call(this, selector, context, rootjQuery);
+};
+$.fn.init.prototype = jquery_init.prototype;
+
+/**
+ * Attach all registered behaviors to a page element.
+ *
+ * Behaviors are event-triggered actions that attach to page elements, enhancing
+ * default non-JavaScript UIs. Behaviors are registered in the Drupal.behaviors
+ * object using the method 'attach' and optionally also 'detach' as follows:
+ * @code
+ *    Drupal.behaviors.behaviorName = {
+ *      attach: function (context, settings) {
+ *        ...
+ *      },
+ *      detach: function (context, settings, trigger) {
+ *        ...
+ *      }
+ *    };
+ * @endcode
+ *
+ * Drupal.attachBehaviors is added below to the jQuery ready event and so
+ * runs on initial page load. Developers implementing AHAH/Ajax in their
+ * solutions should also call this function after new page content has been
+ * loaded, feeding in an element to be processed, in order to attach all
+ * behaviors to the new content.
+ *
+ * Behaviors should use
+ * @code
+ *   $(selector).once('behavior-name', function () {
+ *     ...
+ *   });
+ * @endcode
+ * to ensure the behavior is attached only once to a given element. (Doing so
+ * enables the reprocessing of given elements, which may be needed on occasion
+ * despite the ability to limit behavior attachment to a particular element.)
+ *
+ * @param context
+ *   An element to attach behaviors to. If none is given, the document element
+ *   is used.
+ * @param settings
+ *   An object containing settings for the current context. If none given, the
+ *   global Drupal.settings object is used.
+ */
+Drupal.attachBehaviors = function (context, settings) {
+  context = context || document;
+  settings = settings || Drupal.settings;
+  // Execute all of them.
+  $.each(Drupal.behaviors, function () {
+    if ($.isFunction(this.attach)) {
+      this.attach(context, settings);
+    }
+  });
+};
+
+/**
+ * Detach registered behaviors from a page element.
+ *
+ * Developers implementing AHAH/Ajax in their solutions should call this
+ * function before page content is about to be removed, feeding in an element
+ * to be processed, in order to allow special behaviors to detach from the
+ * content.
+ *
+ * Such implementations should look for the class name that was added in their
+ * corresponding Drupal.behaviors.behaviorName.attach implementation, i.e.
+ * behaviorName-processed, to ensure the behavior is detached only from
+ * previously processed elements.
+ *
+ * @param context
+ *   An element to detach behaviors from. If none is given, the document element
+ *   is used.
+ * @param settings
+ *   An object containing settings for the current context. If none given, the
+ *   global Drupal.settings object is used.
+ * @param trigger
+ *   A string containing what's causing the behaviors to be detached. The
+ *   possible triggers are:
+ *   - unload: (default) The context element is being removed from the DOM.
+ *   - move: The element is about to be moved within the DOM (for example,
+ *     during a tabledrag row swap). After the move is completed,
+ *     Drupal.attachBehaviors() is called, so that the behavior can undo
+ *     whatever it did in response to the move. Many behaviors won't need to
+ *     do anything simply in response to the element being moved, but because
+ *     IFRAME elements reload their "src" when being moved within the DOM,
+ *     behaviors bound to IFRAME elements (like WYSIWYG editors) may need to
+ *     take some action.
+ *   - serialize: When an Ajax form is submitted, this is called with the
+ *     form as the context. This provides every behavior within the form an
+ *     opportunity to ensure that the field elements have correct content
+ *     in them before the form is serialized. The canonical use-case is so
+ *     that WYSIWYG editors can update the hidden textarea to which they are
+ *     bound.
+ *
+ * @see Drupal.attachBehaviors
+ */
+Drupal.detachBehaviors = function (context, settings, trigger) {
+  context = context || document;
+  settings = settings || Drupal.settings;
+  trigger = trigger || 'unload';
+  // Execute all of them.
+  $.each(Drupal.behaviors, function () {
+    if ($.isFunction(this.detach)) {
+      this.detach(context, settings, trigger);
+    }
+  });
+};
+
+/**
+ * Encode special characters in a plain-text string for display as HTML.
+ *
+ * @ingroup sanitization
+ */
+Drupal.checkPlain = function (str) {
+  var character, regex,
+      replace = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
+  str = String(str);
+  for (character in replace) {
+    if (replace.hasOwnProperty(character)) {
+      regex = new RegExp(character, 'g');
+      str = str.replace(regex, replace[character]);
+    }
+  }
+  return str;
+};
+
+/**
+ * Replace placeholders with sanitized values in a string.
+ *
+ * @param str
+ *   A string with placeholders.
+ * @param args
+ *   An object of replacements pairs to make. Incidences of any key in this
+ *   array are replaced with the corresponding value. Based on the first
+ *   character of the key, the value is escaped and/or themed:
+ *    - !variable: inserted as is
+ *    - @variable: escape plain text to HTML (Drupal.checkPlain)
+ *    - %variable: escape text and theme as a placeholder for user-submitted
+ *      content (checkPlain + Drupal.theme('placeholder'))
+ *
+ * @see Drupal.t()
+ * @ingroup sanitization
+ */
+Drupal.formatString = function(str, args) {
+  // Transform arguments before inserting them.
+  for (var key in args) {
+    switch (key.charAt(0)) {
+      // Escaped only.
+      case '@':
+        args[key] = Drupal.checkPlain(args[key]);
+      break;
+      // Pass-through.
+      case '!':
+        break;
+      // Escaped and placeholder.
+      case '%':
+      default:
+        args[key] = Drupal.theme('placeholder', args[key]);
+        break;
+    }
+    str = str.replace(key, args[key]);
+  }
+  return str;
+};
+
+/**
+ * Translate strings to the page language or a given language.
+ *
+ * See the documentation of the server-side t() function for further details.
+ *
+ * @param str
+ *   A string containing the English string to translate.
+ * @param args
+ *   An object of replacements pairs to make after translation. Incidences
+ *   of any key in this array are replaced with the corresponding value.
+ *   See Drupal.formatString().
+ *
+ * @param options
+ *   - 'context' (defaults to the empty context): The context the source string
+ *     belongs to.
+ *
+ * @return
+ *   The translated string.
+ */
+Drupal.t = function (str, args, options) {
+  options = options || {};
+  options.context = options.context || '';
+
+  // Fetch the localized version of the string.
+  if (Drupal.locale.strings && Drupal.locale.strings[options.context] && Drupal.locale.strings[options.context][str]) {
+    str = Drupal.locale.strings[options.context][str];
+  }
+
+  if (args) {
+    str = Drupal.formatString(str, args);
+  }
+  return str;
+};
+
+/**
+ * Format a string containing a count of items.
+ *
+ * This function ensures that the string is pluralized correctly. Since Drupal.t() is
+ * called by this function, make sure not to pass already-localized strings to it.
+ *
+ * See the documentation of the server-side format_plural() function for further details.
+ *
+ * @param count
+ *   The item count to display.
+ * @param singular
+ *   The string for the singular case. Please make sure it is clear this is
+ *   singular, to ease translation (e.g. use "1 new comment" instead of "1 new").
+ *   Do not use @count in the singular string.
+ * @param plural
+ *   The string for the plural case. Please make sure it is clear this is plural,
+ *   to ease translation. Use @count in place of the item count, as in "@count
+ *   new comments".
+ * @param args
+ *   An object of replacements pairs to make after translation. Incidences
+ *   of any key in this array are replaced with the corresponding value.
+ *   See Drupal.formatString().
+ *   Note that you do not need to include @count in this array.
+ *   This replacement is done automatically for the plural case.
+ * @param options
+ *   The options to pass to the Drupal.t() function.
+ * @return
+ *   A translated string.
+ */
+Drupal.formatPlural = function (count, singular, plural, args, options) {
+  var args = args || {};
+  args['@count'] = count;
+  // Determine the index of the plural form.
+  var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] == 1) ? 0 : 1);
+
+  if (index == 0) {
+    return Drupal.t(singular, args, options);
+  }
+  else if (index == 1) {
+    return Drupal.t(plural, args, options);
+  }
+  else {
+    args['@count[' + index + ']'] = args['@count'];
+    delete args['@count'];
+    return Drupal.t(plural.replace('@count', '@count[' + index + ']'), args, options);
+  }
+};
+
+/**
+ * Generate the themed representation of a Drupal object.
+ *
+ * All requests for themed output must go through this function. It examines
+ * the request and routes it to the appropriate theme function. If the current
+ * theme does not provide an override function, the generic theme function is
+ * called.
+ *
+ * For example, to retrieve the HTML for text that should be emphasized and
+ * displayed as a placeholder inside a sentence, call
+ * Drupal.theme('placeholder', text).
+ *
+ * @param func
+ *   The name of the theme function to call.
+ * @param ...
+ *   Additional arguments to pass along to the theme function.
+ * @return
+ *   Any data the theme function returns. This could be a plain HTML string,
+ *   but also a complex object.
+ */
+Drupal.theme = function (func) {
+  var args = Array.prototype.slice.apply(arguments, [1]);
+
+  return (Drupal.theme[func] || Drupal.theme.prototype[func]).apply(this, args);
+};
+
+/**
+ * Freeze the current body height (as minimum height). Used to prevent
+ * unnecessary upwards scrolling when doing DOM manipulations.
+ */
+Drupal.freezeHeight = function () {
+  Drupal.unfreezeHeight();
+  $('<div id="freeze-height"></div>').css({
+    position: 'absolute',
+    top: '0px',
+    left: '0px',
+    width: '1px',
+    height: $('body').css('height')
+  }).appendTo('body');
+};
+
+/**
+ * Unfreeze the body height.
+ */
+Drupal.unfreezeHeight = function () {
+  $('#freeze-height').remove();
+};
+
+/**
+ * Encodes a Drupal path for use in a URL.
+ *
+ * For aesthetic reasons slashes are not escaped.
+ */
+Drupal.encodePath = function (item, uri) {
+  uri = uri || location.href;
+  return encodeURIComponent(item).replace(/%2F/g, '/');
+};
+
+/**
+ * Get the text selection in a textarea.
+ */
+Drupal.getSelection = function (element) {
+  if (typeof element.selectionStart != 'number' && document.selection) {
+    // The current selection.
+    var range1 = document.selection.createRange();
+    var range2 = range1.duplicate();
+    // Select all text.
+    range2.moveToElementText(element);
+    // Now move 'dummy' end point to end point of original range.
+    range2.setEndPoint('EndToEnd', range1);
+    // Now we can calculate start and end points.
+    var start = range2.text.length - range1.text.length;
+    var end = start + range1.text.length;
+    return { 'start': start, 'end': end };
+  }
+  return { 'start': element.selectionStart, 'end': element.selectionEnd };
+};
+
+/**
+ * Build an error message from an Ajax response.
+ */
+Drupal.ajaxError = function (xmlhttp, uri) {
+  var statusCode, statusText, pathText, responseText, readyStateText, message;
+  if (xmlhttp.status) {
+    statusCode = "\n" + Drupal.t("An AJAX HTTP error occurred.") +  "\n" + Drupal.t("HTTP Result Code: !status", {'!status': xmlhttp.status});
+  }
+  else {
+    statusCode = "\n" + Drupal.t("An AJAX HTTP request terminated abnormally.");
+  }
+  statusCode += "\n" + Drupal.t("Debugging information follows.");
+  pathText = "\n" + Drupal.t("Path: !uri", {'!uri': uri} );
+  statusText = '';
+  // In some cases, when statusCode == 0, xmlhttp.statusText may not be defined.
+  // Unfortunately, testing for it with typeof, etc, doesn't seem to catch that
+  // and the test causes an exception. So we need to catch the exception here.
+  try {
+    statusText = "\n" + Drupal.t("StatusText: !statusText", {'!statusText': $.trim(xmlhttp.statusText)});
+  }
+  catch (e) {}
+
+  responseText = '';
+  // Again, we don't have a way to know for sure whether accessing
+  // xmlhttp.responseText is going to throw an exception. So we'll catch it.
+  try {
+    responseText = "\n" + Drupal.t("ResponseText: !responseText", {'!responseText': $.trim(xmlhttp.responseText) } );
+  } catch (e) {}
+
+  // Make the responseText more readable by stripping HTML tags and newlines.
+  responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi,"");
+  responseText = responseText.replace(/[\n]+\s+/g,"\n");
+
+  // We don't need readyState except for status == 0.
+  readyStateText = xmlhttp.status == 0 ? ("\n" + Drupal.t("ReadyState: !readyState", {'!readyState': xmlhttp.readyState})) : "";
+
+  message = statusCode + pathText + statusText + responseText + readyStateText;
+  return message;
+};
+
+// Class indicating that JS is enabled; used for styling purpose.
+$('html').addClass('js');
+
+// 'js enabled' cookie.
+document.cookie = 'has_js=1; path=/';
+
+/**
+ * Additions to jQuery.support.
+ */
+$(function () {
+  /**
+   * Boolean indicating whether or not position:fixed is supported.
+   */
+  if (jQuery.support.positionFixed === undefined) {
+    var el = $('<div style="position:fixed; top:10px" />').appendTo(document.body);
+    jQuery.support.positionFixed = el[0].offsetTop === 10;
+    el.remove();
+  }
+});
+
+//Attach all behaviors.
+$(function () {
+  Drupal.attachBehaviors(document, Drupal.settings);
+});
+
+/**
+ * The default themes.
+ */
+Drupal.theme.prototype = {
+
+  /**
+   * Formats text for emphasized display in a placeholder inside a sentence.
+   *
+   * @param str
+   *   The text to format (plain-text).
+   * @return
+   *   The formatted text (html).
+   */
+  placeholder: function (str) {
+    return '<em class="placeholder">' + Drupal.checkPlain(str) + '</em>';
+  }
+};
+
+})(jQuery);

BIN
misc/druplicon.png


+ 36 - 0
misc/farbtastic/farbtastic.css

@@ -0,0 +1,36 @@
+
+.farbtastic {
+  position: relative;
+}
+.farbtastic * {
+  position: absolute;
+  cursor: crosshair;
+}
+.farbtastic,
+.farbtastic .wheel {
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .color,
+.farbtastic .overlay {
+  top: 47px;
+  left: 47px;
+  width: 101px;
+  height: 101px;
+}
+.farbtastic .wheel {
+  background: url(wheel.png) no-repeat;
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .overlay {
+  background: url(mask.png) no-repeat;
+}
+.farbtastic .marker {
+  width: 17px;
+  height: 17px;
+  margin: -8px 0 0 -8px;
+  overflow: hidden;
+  background: url(marker.png) no-repeat;
+}
+

File diff suppressed because it is too large
+ 0 - 0
misc/farbtastic/farbtastic.js


BIN
misc/farbtastic/marker.png


Some files were not shown because too many files changed in this diff