Browse Source

updated core to 7.73

Bachir Soussi Chiadmi 3 years ago
parent
commit
7d87153100
100 changed files with 7209 additions and 1114 deletions
  1. 2 0
      .gitignore
  2. 722 23
      CHANGELOG.txt
  3. 1 1
      COPYRIGHT.txt
  4. 9 6
      INSTALL.mysql.txt
  5. 3 1
      INSTALL.txt
  6. 113 112
      MAINTAINERS.txt
  7. 6 6
      README.txt
  8. 10 0
      UPGRADE.txt
  9. 117 11
      includes/ajax.inc
  10. 9 10
      includes/batch.inc
  11. 516 130
      includes/bootstrap.inc
  12. 72 27
      includes/cache.inc
  13. 351 138
      includes/common.inc
  14. 81 22
      includes/database/database.inc
  15. 59 6
      includes/database/mysql/database.inc
  16. 3 16
      includes/database/mysql/query.inc
  17. 25 17
      includes/database/mysql/schema.inc
  18. 15 7
      includes/database/pgsql/database.inc
  19. 1 1
      includes/database/pgsql/install.inc
  20. 2 1
      includes/database/pgsql/query.inc
  21. 14 14
      includes/database/pgsql/schema.inc
  22. 2 2
      includes/database/pgsql/select.inc
  23. 63 64
      includes/database/query.inc
  24. 11 5
      includes/database/schema.inc
  25. 20 2
      includes/database/select.inc
  26. 9 1
      includes/database/sqlite/database.inc
  27. 0 2
      includes/database/sqlite/install.inc
  28. 8 8
      includes/database/sqlite/query.inc
  29. 15 15
      includes/database/sqlite/schema.inc
  30. 10 10
      includes/date.inc
  31. 96 25
      includes/entity.inc
  32. 15 6
      includes/errors.inc
  33. 249 99
      includes/file.inc
  34. 20 0
      includes/file.mimetypes.inc
  35. 55 0
      includes/file.phar.inc
  36. 1 1
      includes/filetransfer/filetransfer.inc
  37. 3 3
      includes/filetransfer/ftp.inc
  38. 4 2
      includes/filetransfer/ssh.inc
  39. 271 65
      includes/form.inc
  40. 44 4
      includes/install.core.inc
  41. 31 17
      includes/install.inc
  42. 4 1
      includes/iso.inc
  43. 3 4
      includes/language.inc
  44. 58 8
      includes/locale.inc
  45. 1 1
      includes/lock.inc
  46. 9 8
      includes/mail.inc
  47. 104 29
      includes/menu.inc
  48. 129 21
      includes/module.inc
  49. 31 1
      includes/pager.inc
  50. 5 1
      includes/password.inc
  51. 12 9
      includes/path.inc
  52. 37 9
      includes/registry.inc
  53. 114 0
      includes/request-sanitizer.inc
  54. 32 7
      includes/session.inc
  55. 170 24
      includes/stream_wrappers.inc
  56. 3 4
      includes/tablesort.inc
  57. 202 54
      includes/theme.inc
  58. 9 5
      includes/unicode.inc
  59. 12 0
      includes/update.inc
  60. 35 1
      includes/xmlrpc.inc
  61. 11 1
      includes/xmlrpcs.inc
  62. 83 8
      misc/ajax.js
  63. 11 6
      misc/autocomplete.js
  64. 4 0
      misc/brumann/polyfill-unserialize/.gitignore
  65. 20 0
      misc/brumann/polyfill-unserialize/.travis.yml
  66. 21 0
      misc/brumann/polyfill-unserialize/LICENSE
  67. 61 0
      misc/brumann/polyfill-unserialize/README.md
  68. 26 0
      misc/brumann/polyfill-unserialize/composer.json
  69. 25 0
      misc/brumann/polyfill-unserialize/phpunit.xml.dist
  70. 58 0
      misc/brumann/polyfill-unserialize/src/Unserialize.php
  71. 217 19
      misc/drupal.js
  72. BIN
      misc/favicon.ico
  73. 112 0
      misc/jquery-extend-3.4.0.js
  74. 251 0
      misc/jquery-html-prefilter-3.5.0-backport.js
  75. 6 2
      misc/states.js
  76. 45 10
      misc/tabledrag.js
  77. 5 1
      misc/tableselect.js
  78. BIN
      misc/throbber-active.gif
  79. BIN
      misc/throbber-inactive.png
  80. 79 0
      misc/typo3/drupal-security/PharExtensionInterceptor.php
  81. 21 0
      misc/typo3/phar-stream-wrapper/LICENSE
  82. 221 0
      misc/typo3/phar-stream-wrapper/README.md
  83. 30 0
      misc/typo3/phar-stream-wrapper/composer.json
  84. 22 0
      misc/typo3/phar-stream-wrapper/src/Assertable.php
  85. 124 0
      misc/typo3/phar-stream-wrapper/src/Behavior.php
  86. 37 0
      misc/typo3/phar-stream-wrapper/src/Collectable.php
  87. 16 0
      misc/typo3/phar-stream-wrapper/src/Exception.php
  88. 199 0
      misc/typo3/phar-stream-wrapper/src/Helper.php
  89. 88 0
      misc/typo3/phar-stream-wrapper/src/Interceptor/ConjunctionInterceptor.php
  90. 55 0
      misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php
  91. 73 0
      misc/typo3/phar-stream-wrapper/src/Interceptor/PharMetaDataInterceptor.php
  92. 135 0
      misc/typo3/phar-stream-wrapper/src/Manager.php
  93. 59 0
      misc/typo3/phar-stream-wrapper/src/Phar/Container.php
  94. 18 0
      misc/typo3/phar-stream-wrapper/src/Phar/DeserializationException.php
  95. 176 0
      misc/typo3/phar-stream-wrapper/src/Phar/Manifest.php
  96. 254 0
      misc/typo3/phar-stream-wrapper/src/Phar/Reader.php
  97. 18 0
      misc/typo3/phar-stream-wrapper/src/Phar/ReaderException.php
  98. 65 0
      misc/typo3/phar-stream-wrapper/src/Phar/Stub.php
  99. 511 0
      misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php
  100. 24 0
      misc/typo3/phar-stream-wrapper/src/Resolvable.php

+ 2 - 0
.gitignore

@@ -4,3 +4,5 @@ sites/*/settings*.php
 # Ignore paths that contain user-generated content.
 # Ignore paths that contain user-generated content.
 sites/*/files
 sites/*/files
 sites/*/private
 sites/*/private
+
+vendor/*

+ 722 - 23
CHANGELOG.txt

@@ -1,3 +1,702 @@
+Drupal 7.73, 2020-09-16
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2020-007
+
+Drupal 7.72, 2020-06-17
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2020-004
+
+Drupal 7.71, 2020-06-03
+-----------------------
+- Fix for jQuery Form bug in Chromium-based browsers
+- Full support for PHP 7.4
+
+Drupal 7.70, 2020-05-19
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2020-002
+   - SA-CORE-2020-003
+
+Drupal 7.69, 2019-12-18
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2019-012
+
+Drupal 7.68, 2019-12-04
+-----------------------
+- Fixed: Hide toolbar when printing
+- Fixed: Settings returned via ajax are not run through hook_js_alter()
+- Fixed: Use drupal_http_build_query() in drupal_http_request()
+- Fixed: DrupalRequestSanitizer not found fatal error when bootstrap phase order is changed
+- Fixed: Block web.config in .htaccess (and vice-versa)
+- Fixed: Create "scripts" element to align rendering workflow to how "styles" are handled
+- PHP 7.3: Fixed 'Cannot change session id when session is active'
+- PHP 7.1: Fixed 'A non-numeric value encountered in theme_pager()'
+- PHP 7.x: Fixed file.inc generated .htaccess does not cover PHP 7
+- PHP 5.3: Fixed check_plain() 'Invalid multibyte sequence in argument' test failures
+- Fixed: Allow passing data as array to drupal_http_request()
+- Fixed: Skip module_invoke/module_hook in calling hook_watchdog (excessive function_exist)
+- Fixed: HTTP status 200 returned for 'Additional uncaught exception thrown while handling exception'
+- Fixed: theme_table() should take an optional footer variable and produce <tfoot>
+- Fixed: 'uasort() expects parameter 1 to be array, null given in node_view_multiple()'
+- [regression] Fix default.settings.php permission
+
+Drupal 7.67, 2019-05-08
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2019-007
+
+Drupal 7.66, 2019-04-17
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2019-006
+
+Drupal 7.65, 2019-03-20
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2019-004
+
+Drupal 7.64, 2019-02-06
+-----------------------
+- [regression] Unset the 'host' header in drupal_http_request() during redirect
+- Fixed: 7.x does not have Phar protection and Phar tests are failing on Drupal 7
+- Fixed: Notice: Undefined index: display_field in file_field_widget_value() (line 582 of /module/file/file.field.inc)
+- Performance improvement: Registry rebuild should not parse the same file twice in the same request
+- Fixed _registry_update() to clear caches after transaction is committed
+
+Drupal 7.63, 2019-01-16
+-----------------------
+- Fixed a fatal error for some Drush users introduced by SA-CORE-2019-002.
+
+Drupal 7.62, 2019-01-15
+-----------------------
+- Fixed security issues:
+   - SA-CORE-2019-001
+   - SA-CORE-2019-002
+
+Drupal 7.61, 2018-11-07
+-----------------------
+- File upload validation functions and hook_file_validate() implementations are
+  now always passed the correct file URI.
+- The default form cache expiration of 6 hours is now configurable (API
+  addition: https://www.drupal.org/node/2857751).
+- Allowed callers of drupal_http_request() to optionally specify an explicit
+  Host header.
+- Allowed the + character to appear in usernames.
+- PHP 7.2: Fixed Archive_Tar incompatibility.
+- PHP 7.2: Removed deprecated function each().
+- PHP 7.2: Avoid count() calls on uncountable variables.
+- PHP 7.2: Removed deprecated create_function() call.
+- PHP 7.2: Make sure variables are arrays in theme_links().
+- Fixed theme-settings.php not being loaded on cached forms
+- Fixed problem with IE11 & Chrome(PointerEvents enabled) & some Firefox scroll to the top of the page after dragging the bottom item with jquery 1.5 <-> 1.11
+
+Drupal 7.60, 2018-10-18
+------------------------
+- Fixed security issues. See SA-CORE-2018-006.
+
+Drupal 7.59, 2018-04-25
+-----------------------
+- Fixed security issues (remote code execution). See SA-CORE-2018-004.
+
+Drupal 7.58, 2018-03-28
+-----------------------
+- Fixed security issues (remote code execution). See SA-CORE-2018-002.
+
+Drupal 7.57, 2018-02-21
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-001.
+
+Drupal 7.56, 2017-06-21
+-----------------------
+- Fixed security issues (access bypass). See SA-CORE-2017-003.
+
+Drupal 7.55, 2017-06-07
+-----------------------
+- Fixed incompatibility with PHP versions 7.0.19 and 7.1.5 due to duplicate
+  DATE_RFC7231 definition.
+- Made Drupal core pass all automated tests on PHP 7.1.
+- Allowed services such as Let's Encrypt to work with Drupal on Apache, by
+  making Drupal's .htaccess file allow access to the .well-known directory
+  defined by RFC 5785.
+- Made new Drupal sites work correctly on Apache 2.4 when the mod_access_compat
+  Apache module is disabled.
+- Fixed Drupal's URL-generating functions to always encode '[' and ']' so that
+  the URLs will pass HTML5 validation.
+- Various additional bug fixes.
+- Various API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.54, 2017-02-01
+-----------------------
+- Modules are now able to define theme engines (API addition:
+  https://www.drupal.org/node/2826480).
+- Logging of searches can now be disabled (new option in the administrative
+  interface).
+- Added menu tree render structure to (pre-)process hooks for theme_menu_tree()
+  (API addition: https://www.drupal.org/node/2827134).
+- Added new function for determining whether an HTTPS request is being served
+  (API addition: https://www.drupal.org/node/2824590).
+- Fixed incorrect default value for short and medium date formats on the date
+  type configuration page.
+- File validation error message is now removed after subsequent upload of valid
+  file.
+- Numerous bug fixes.
+- Numerous API documentation improvements.
+- Additional performance improvements.
+- Additional automated test coverage.
+
+Drupal 7.53, 2016-12-07
+-----------------------
+- Fixed drag and drop support on newer Chrome/IE 11+ versions after 7.51 update
+  when jQuery is updated to 1.7-1.11.0.
+
+Drupal 7.52, 2016-11-16
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-005.
+
+Drupal 7.51, 2016-10-05
+-----------------------
+- The Update module now also checks for updates to a disabled theme that is
+  used as an admin theme.
+- Exceptions thrown in dblog_watchdog() are now caught and ignored.
+- Clarified the warning that appears when modules are missing or have moved.
+- Log messages are now XSS filtered on display.
+- Draggable tables now work on touch screen devices.
+- Added a setting for allowing double underscores in CSS identifiers
+  (https://www.drupal.org/node/2810369).
+- If a user navigates away from a page while an Ajax request is running they
+  will no longer get an error message saying "An Ajax HTTP request terminated
+  abnormally".
+- The system_region_list() API function now takes an optional third parameter
+  which allows region name translations to be skipped when they are not needed
+  (API addition: https://www.drupal.org/node/2810365).
+- Numerous performance improvements.
+- Numerous bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.50, 2016-07-07 
+-----------------------
+- Added a new "administer fields" permission for trusted users, which is
+  required in addition to other permissions to use the field UI
+  (https://www.drupal.org/node/2483307).
+- Added clickjacking protection to Drupal core by setting the X-Frame-Options
+  header to SAMEORIGIN by default (https://www.drupal.org/node/2735873).
+- Added support for full UTF-8 (emojis, Asian symbols, mathematical symbols) on
+  MySQL and other database drivers when the site and database are configured to
+  allow it (https://www.drupal.org/node/2761183).
+- Improved performance by avoiding a re-scan of directories when a file is
+  missing; instead, trigger a PHP warning (minor API change:
+  https://www.drupal.org/node/2581445).
+- Made it possible to use any PHP callable in Ajax form callbacks, form API
+  form-building functions, and form API wrapper callbacks (API addition:
+  https://www.drupal.org/node/2761169).
+- Fixed that following a password reset link while logged in leaves users unable
+  to change their password (minor user interface change:
+  https://www.drupal.org/node/2759023).
+- Implemented various fixes for automated test failures on PHP 5.4+ and PHP 7.
+  Drupal core automated tests now pass in these environments.
+- Improved support for PHP 7 by fixing various problems.
+- Fixed various bugs with PHP 5.5+ imagerotate(), including when incorrect
+  color indices are passed in.
+- Fixed a regression introduced in Drupal 7.43 that allowed files uploaded by
+  anonymous users to be lost after form validation errors, and that also caused
+  regressions with certain contributed modules.
+- Fixed a regression introduced in Drupal 7.36 which caused the default value
+  of hidden textarea fields to be ignored.
+- Fixed robots.txt to allow search engines to access CSS, JavaScript and image
+  files.
+- Changed wording on the Update Manager settings page to clarify that the
+  option to check for disabled module updates also applies to uninstalled
+  modules (administrative-facing translatable string change).
+- Changed the help text when editing menu links and configuring URL redirect
+  actions so that it does not reference "Drupal" or the drupal.org website
+  (administrative-facing translatable string change).
+- Fixed the locale safety check that is used to ensure that translations are
+  safe to allow for tokens in the href/src attributes of translated strings.
+- Fixed that URL generation only works on port 80 when using domain based
+  language negotation.
+- Made method="get" forms work inside the administrative overlay. The fix adds
+  a new hidden field to these forms when they appear inside the overlay (minor
+  data structure change).
+- Increased maxlength of menu link title input fields in the node form and
+  menu link form from 128 to 255 characters.
+- Removed meaningless post-check=0 and pre-check=0 cache control headers from
+  Drupal HTTP responses.
+- Added a .editorconfig file to auto-configure editors that support it.
+- Added --directory option to run-tests.sh for easier test discovery of all
+  tests within a project.
+- Made run-tests.sh exit with a failure code when there are test fails or
+  problems running the script.
+- Fixed that cookies from previous tests are still present when a new test
+  starts in DrupalWebTestCase.
+- Improved performance of queries on the {authmap} database table.
+- Fixed handling of missing files and functions inside the registry.
+- Fixed Ajax handling for tableselect form elements that use checkboxes.
+- Fixed a bug which caused ip_address() to return nothing when the client IP
+  address and proxy IP address are the same.
+- Added a new option to format_xml_elements() to allow for already encoded
+  values.
+- Changed the {history} table's node ID field to be an unsigned integer, to
+  match the same field in the {node} table and to prevent errors with very
+  large node IDs.
+- Added an explicit page callback to the "admin/people/create" menu item in the
+  User module (minor data structure change). Previously this automatically
+  inherited the page callback from the parent "admin/people" menu item, which
+  broke contributed modules that override the "admin/people" page.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.44, 2016-06-15
+-----------------------
+- Fixed security issues (privilege escalation). See SA-CORE-2016-002.
+
+Drupal 7.43, 2016-02-24
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-001.
+
+Drupal 7.42, 2016-02-03
+-----------------------
+- Stopped invoking hook_flush_caches() on every cron run, since some modules
+  use that hook for expensive operations that are only needed on cache clears.
+- Changed the default .htaccess and web.config to block Composer-related files.
+- Added static caching to module_load_include() to improve performance.
+- Fixed double-encoding bugs in select field widgets provided by the Options
+  module. The fix deprecates the 'strip_tags' property on option widgets and
+  replaces it with a new 'strip_tags_and_unescape' property (minor data
+  structure change).
+- Improved MySQL 5.7 support by changing the MySQL database driver to stop
+  using the ANSI SQL mode alias, which has different meanings for different
+  MySQL versions.
+- Fixed a regression introduced in Drupal 7.39 which prevented autocomplete
+  functionality from working on servers that are not configured to
+  automatically recognize index.php.
+- Updated the Archive_Tar PEAR package to the latest 1.4.0 release, to fix bugs
+  with tar file handling on various operating systems.
+- Fixed fatal errors on node preview when a field is displayed in the node
+  teaser but hidden in the full node view. The fix removes a
+  field_attach_prepare_view() call from the node_preview() function since it is
+  redundant with one in the node preview theme layer.
+- Improved the description of the "Trimmed" format option on text fields
+  (translatable string change, and minor UI and data structure change).
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.41, 2015-10-21
+-----------------------
+- Fixed security issues (open redirect). See SA-CORE-2015-004.
+
+Drupal 7.40, 2015-10-14
+-----------------------
+- Made Drupal's code for parsing .info files run much faster and use much less
+  memory.
+- Prevented drupal_http_request() from returning an error when it receives a
+  201 through 206 HTTP status code.
+- Added support for autoloading traits via the registry on sites running PHP
+  5.4 or higher.
+- Allowed the user-picture.tpl.php theme template to have HTML classes besides
+  the default "user-picture" class printed in it (markup change).
+- Fixed the URL text filter to convert e-mail addresses with plus signs into
+  mailto: links.
+- Added alternate text to file icons displayed by the File module, to improve
+  accessibility (string change, and minor API addition to theme_file_icon()).
+- Changed one-time login link failure messages to be displayed as errors or
+  warnings as appropriate, rather than as regular status messages (minor UI
+  change and data structure change).
+- Changed the default settings.php configuration to exclude private files from
+  the "404_fast_paths" behavior.
+- Changed the page that displays filter tips for a particular text format, for
+  example filter/tips/full_html, to return "page not found" or "access denied"
+  if the format does not exist or the user does not have access to it. This
+  change adds a new menu item to the Filter module's hook_menu() entry (minor
+  data structure change).
+- Added a new hook, hook_block_cid_parts_alter(), to allow modules to alter the
+  cache keys used for caching a particular block.
+- Made drupal_set_message() display and return messages when "0" is passed in
+  as the message to set.
+- Fixed non-functional "Files displayed by default" setting on file fields.
+- The "worker callback" provided in hook_cron_queue_info() and the "finished"
+  callback specified during batch processing can now be any PHP callable
+  instead of just functions.
+- Prevented drupal_set_time_limit() from decreasing the time limit in the case
+  where the PHP maximum execution time is already unlimited.
+- Changed the default thousand marker for numeric fields from a space ("1 000")
+  to nothing ("1000") (minor UI change: https://www.drupal.org/node/1388376).
+- Prevented malformed theme .info files (without a "name" key) from causing
+  exceptions during menu rebuilds. If an .info file without a "name" key is
+  found in a module or theme directory, Drupal will now use the module or
+  theme's machine name as the display name instead.
+- Made the format column in the {date_format_locale} database table
+  case-sensitive, to match the equivalent column in the {date_formats} table.
+- Fixed a bug in the Statistics module that caused JavaScript files attached to
+  a node while it is being viewed to be omitted from the page.
+- Added an optional 'project:' prefix that can be added to dependencies in a
+  module's .info file to indicate which project the dependency resides in (API
+  addition: https://www.drupal.org/node/2299747).
+- Fixed various bugs that occurred after hooks were invoked early in the Drupal
+  bootstrap and that caused module_implements() and drupal_alter() to cache an
+  incomplete set of hook implementations for later use.
+- Set the X-Content-Type-Options header to "nosniff" when possible, to prevent
+  certain web browsers from picking an unsafe MIME type.
+- Prevented the database API from executing multiple queries at once on MySQL,
+  if the site's PHP version is new enough to do so. This is a secondary defense
+  against SQL injection (API change: https://www.drupal.org/node/2463973).
+- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused the upgrade
+  to fail when there were multiple file records pointing to the same file.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.39, 2015-08-19
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-003.
+
+Drupal 7.38, 2015-06-17
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-002.
+
+Drupal 7.37, 2015-05-07
+-----------------------
+- Fixed a regression in Drupal 7.36 which caused certain kinds of content types
+  to become disabled if they were defined by a no-longer-enabled module.
+- Removed a confusing description regarding automatic time zone detection from
+  the user account form (minor UI and data structure change).
+- Allowed custom HTML tags with a dash in the name to pass through filter_xss()
+  when specified in the list of allowed tags.
+- Allowed hook_field_schema() implementations to specify indexes for fields
+  based on a fixed-length column prefix (rather than the entire column), as was
+  already allowed in hook_schema() implementations.
+- Fixed PDO exceptions on PostgreSQL when accessing invalid entity URLs.
+- Added a sites/all/libraries folder to the codebase, with instructions for
+  using it.
+- Added a description to the "Administer text formats and filters" permission
+  on the Permissions page (string change).
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.36, 2015-04-01
+-----------------------
+- Added a 'file_public_schema' variable which allows modules that define
+  publicly-accessible streams in hook_stream_wrappers() to bypass file download
+  access checks when processing managed file upload fields.
+- Fixed a bug that caused database query tags not to be added to search-related
+  database queries under many circumstances, and which prevented the
+  corresponding hook_query_TAG_alter() implementations from being called.
+- Fixed the "for" attribute on managed file upload field labels to improve
+  accessibility (minor markup change).
+- Added a 'javascript_always_use_jquery' variable which can be set to FALSE by
+  sites that may not need jQuery loaded on all pages, and a 'requires_jquery'
+  option to drupal_add_js() which modules can set to FALSE when adding
+  JavaScript files that have no dependency on jQuery (API addition:
+  https://www.drupal.org/node/2462717).
+- Fixed incorrect foreign keys in the User module's role_permission and
+  users_roles database tables.
+- Changed permission descriptions throughout Drupal core to consistently link
+  to relevant administrative pages, regardless of whether the user viewing the
+  Permissions page can view the page being linked to (minor UI change).
+- Fixed the drupal_add_region_content() function so that it actually adds
+  content to the page.
+- Added an 'image_suppress_itok_output' variable to allow sites already using
+  the existing 'image_allow_insecure_derivatives' variable to also prevent
+  security tokens from appearing in image derivative URLs.
+- Fixed double-escaping of theme names in the Block module administrative
+  interface (minor string change).
+- Added basic support for Xdebug when running automated tests.
+- Fixed a bug which caused previewing a node to remove elements from the node
+  being edited. With this fix, calling node_preview() will no longer modify the
+  passed-in node object (minor API change).
+- Added a user_has_role() function to check whether a user has a particular
+  role (API addition: https://www.drupal.org/node/2462411).
+- Fixed installation failures when an opcode cache is enabled.
+- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused private
+  files to be inaccessible.
+- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused user
+  pictures to be lost.
+- Fixed missing language code in hook_field_attach_view_alter() when it is
+  invoked from field_view_field().
+- Stopped sending ETag and Last-Modified headers for uncached page requests,
+  since they break caching for certain Varnish and Nginx configurations.
+- Changed the Simpletest module to allow PSR-4 test classes to be used in
+  Drupal 7.
+- Fixed a fatal error that occurred when using the Comment module's "Unpublish
+  comment containing keyword(s)" action.
+- Changed the "lang" attribute on language links to "xml:lang" so it validates
+  as XHTML (minor markup change).
+- Prevented the form API from allowing arrays to be submitted for various form
+  elements, such as textfields, textareas, and password fields (API change:
+  https://www.drupal.org/node/2462723).
+- Fixed a bug in the Contact module which caused the global user object to have
+  the incorrect name and e-mail address during the remainder of the page
+  request after the contact form is submitted.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.35, 2015-03-18
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-001.
+
+Drupal 7.34, 2014-11-19
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-006.
+
+Drupal 7.33, 2014-11-07
+-----------------------
+- Began storing the file modification time of each module and theme in the
+  {system} database table so that contributed modules can use it to identify
+  recently changed modules and themes (minor data structure change to the
+  return value of system_get_info() and other related functions).
+- Added a "Did you mean?" feature to the run-tests.sh script for running
+  automated tests from the command line, to help developers who are attempting
+  to run a particular test class or group.
+- Changed the date format used in various HTTP headers output by Drupal core
+  from RFC 1123 format to RFC 7231 format.
+- Added a "block_cache_bypass_node_grants" variable to allow sites which have
+  node access modules enabled to use the block cache if desired (API addition).
+- Made image derivative generation HTTP requests return a 404 error (rather
+  than a 500 error) when the source image does not exist.
+- Fixed a bug which caused user pictures to be removed from the user object
+  after saving, and resulted in data loss if the user account was subsequently
+  re-saved.
+- Fixed a bug in which field_has_data() did not return TRUE for fields that
+  only had data in older entity revisions, leading to loss of the field's data
+  when the field configuration was edited.
+- Fixed a bug which caused the Ajax progress throbber to appear misaligned in
+  many situatons (minor styling change).
+- Prevented the Bartik theme from lower-casing the "Permalink" link on
+  comments, for improved multilingual support (minor UI change).
+- Added a "preferred_menu_links" tag to the database query that is used by
+  menu_link_get_preferred() to find the preferred menu link for a given path,
+  to make it easier to alter.
+- Increased the maximum allowed length of block titles to 255 characters
+  (database schema change to the {block} table).
+- Removed the Field module's field_modules_uninstalled() function, since it did
+  not do anything when it was invoked.
+- Added a "theme_hook_original" variable to templates and theme functions and
+  an optional sitewide theme debug mode, to provide contextual information in
+  the page's HTML to theme developers. The theme debug mode is based on the one
+  used with Twig in Drupal 8 and can be accessed by setting the "theme_debug"
+  variable to TRUE (API addition).
+- Added an entity_view_mode_prepare() API function to allow entity-defining
+  modules to properly invoke hook_entity_view_mode_alter(), and used it
+  throughout Drupal core to fix bugs with the invocation of that hook (API
+  change: https://www.drupal.org/node/2369141).
+- Security improvement: Made the database API's orderBy() method sanitize the
+  sort direction ("ASC" or "DESC") for queries built with db_select(), so that
+  calling code does not have to.
+- Changed the RDF module to consistently output RDF metadata for nodes and
+  comments near where the node is rendered in the HTML (minor markup and data
+  structure change).
+- Added an HTML class to RDFa metatags throughout Drupal to prevent them from
+  accidentally affecting the site appearance (minor markup change).
+- Fixed a bug in the Unicode requirements check which prevented installing
+  Drupal on PHP 5.6.
+- Fixed a bug which caused drupal_get_bootstrap_phase() to abort the bootstrap
+  when called early in the page request.
+- Renamed the "Search result" view mode to "Search result highlighting input"
+  to better reflect how it is used (UI change).
+- Improved database queries generated by EntityFieldQuery in the case where
+  delta or language condition groups are used, to reduce the number of INNER
+  JOINs (this is a minor data structure change affecting code which implements
+  hook_query_alter() on these queries).
+- Removed special-case behavior for file uploads which allowed user #1 to
+  bypass maximum file size and user quota limits.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.32, 2014-10-15
+-----------------------
+- Fixed security issues (SQL injection). See SA-CORE-2014-005.
+
+Drupal 7.31, 2014-08-06
+-----------------------
+- Fixed security issues (denial of service). See SA-CORE-2014-004.
+
+Drupal 7.30, 2014-07-24
+-----------------------
+- Fixed a regression introduced in Drupal 7.29 that caused files or images
+  attached to taxonomy terms to be deleted when the taxonomy term was edited
+  and resaved (and other related bugs with contributed and custom modules).
+- Added a warning on the permissions page to recommend restricting access to
+  the "View site reports" permission to trusted administrators. See
+  DRUPAL-PSA-2014-002.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.29, 2014-07-16
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-003.
+
+Drupal 7.28, 2014-05-08
+-----------------------
+- Fixed a regression introduced in Drupal 7.27 that caused JavaScript to break
+  on older browsers (such as Internet Explorer 8 and earlier) when Ajax was
+  used.
+- Increased the timeout used by the Update Manager module when it fetches data
+  from drupal.org (from 5 seconds to 30 seconds), to work around a problem
+  which causes incomplete information about security updates to be presented to
+  site administrators. This fix may lead to a performance slowdown on the
+  Update Manager administration pages, when installing Drupal distributions,
+  and (for sites that use the automated cron feature) on occasional page loads
+  by site visitors.
+- Fixed the behavior of the token system's "[node:summary]" token when the body
+  field does not have a manual summary.
+- Changed the behavior of db_query_temporary() so that it works on SELECT
+  queries even when they have leading comments/whitespace. A side effect of
+  this fix is that db_query_temporary() will now fail with an error if it is
+  ever used on non-SELECT queries.
+- Added a "node_admin_filter" tag to the database query used to build the list
+  of nodes on the content administration page, to make it easier to alter.
+- Made the cron queue system log any exceptions that are thrown while an item
+  in the queue is being processed, rather than stopping the entire PHP request.
+- Improved screen reader support by adding an aria-live HTML attribute to file
+  upload fields when there is an error uploading the file (minor markup
+  change).
+- Made the pager on the Tracker module listing pages show the same number of
+  items as other pagers throughout Drupal core (minor UI change).
+- Fixed a bug which caused caches not to be properly cleared when a file entity
+  was saved or deleted.
+- Added several missing countries to the default list returned by
+  country_get_list() (string change).
+- Replaced the term "weight" with "influence" in the content ranking settings
+  for search, and added help text for administrators (string change).
+- Fixed untranslatable text strings in the administrative interface for the
+  "Crop" effect provided by the Image module (minor string change).
+- Fixed a bug in the Taxonomy module update function introduced in Drupal 7.26
+  that caused memory and CPU problems on sites with very large numbers of
+  unpublished nodes.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.27, 2014-04-16
+-----------------------
+- Fixed security issues (information disclosure). See SA-CORE-2014-002.
+
+Drupal 7.26, 2014-01-15
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-001.
+
+Drupal 7.25, 2014-01-02
+-----------------------
+- Fixed a bug in node_save() which prevented the saved node from being updated
+  in hook_node_insert() and other similar hooks.
+- Added a meta tag to install.php to prevent it from being indexed by search
+  engines even when Drupal is installed in a subfolder (minor markup change).
+- Fixed a bug in the database API that caused frequent deadlock errors when
+  running merge queries on some servers.
+- Performance improvement: Prevented block rehashing from writing blocks to the
+  database on every cache clear and cron run when the blocks have not changed.
+  This fix results in an extra 'saved' key which is added and set to TRUE for
+  each block returned by _block_rehash() that actually is saved to the database
+  (data structure change).
+- Added an optional 'skip on cron' parameter to hook_cron_queue_info() to allow
+  queues to avoid being automatically processed on cron runs (API addition).
+- Fixed a bug which caused hook_block_view_MODULE_DELTA_alter() to never be
+  invoked if the block delta had a hyphen in it. To implement the hook when the
+  block delta has a hyphen, modules should now replace hyphens with underscores
+  when constructing the function name for the hook implementation.
+- Fixed a bug which caused cached pages to sometimes be sent to the browser
+  with incorrect compression. The fix adds a new 'page_compressed' key to the
+  $cache->data array returned by drupal_page_get_cache() (minor data structure
+  change).
+- Fixed broken tests on PHP 5.5.
+- Made the File and Image modules more robust when saving entities that have
+  deleted files attached. The code in file_field_presave() will now remove the
+  record of the deleted file from the entity before saving (minor data
+  structure change).
+- Standardized menu callback functions throughout Drupal core to return
+  MENU_NOT_FOUND and MENU_ACCESS_DENIED rather than printing their own "page
+  not found" or "access denied" pages (minor API change in the return value of
+  these functions under some circumstances).
+- Fixed a bug in which caches were not properly cleared when a node was deleted
+  via the administrative interface.
+- Changed the Bartik theme to render content contained in <pre>, <code> and
+  similar tags in a larger font size, so it is easier to read.
+- Fixed a bug in the Search module that caused exceptions to be thrown during
+  searches if the server was not configured to represent decimal points as a
+  period.
+- Fixed a regression in the Image module that made image_style_url() not work
+  when a relative path (rather than a complete file URI) was passed to it.
+- Added an optional feature to the Statistics module to allow node views to be
+  tracked by Ajax requests rather than during the server-side generation of the
+  page. This allows the node counter to work on sites that use external page
+  caches (string change and new administrative option:
+  https://drupal.org/node/2164069).
+- Added a link to the drupal.org documentation page for cron to the Cron
+  settings page (string change).
+- Added a 'drupal_anonymous_user_object' variable to allow the anonymous user
+  object returned by drupal_anonymous_user() to be overridden with a classed
+  object (API addition).
+- Changed the database API to allow inserts based on a SELECT * query to work
+  correctly.
+- Changed the database schema of the {file_managed} table to allow Drupal to
+  manage files larger than 4 GB.
+- Changed the File module's hook_field_load() implementation to prevent file
+  entity properties which have the same name as file or image field properties
+  from overwriting the field properties (minor API change).
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.24, 2013-11-20
+-----------------------
+- Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-003.
+
+Drupal 7.23, 2013-08-07
+-----------------------
+- Fixed a fatal error on PostgreSQL databases when updating the Taxonomy module
+  from Drupal 6 to Drupal 7.
+- Fixed the default ordering of CSS files for sites using right-to-left
+  languages, to consistently place the right-to-left override file immediately
+  after the CSS it is overriding (API change: https://drupal.org/node/2058463).
+- Added a drupal_check_memory_limit() API function to allow the memory limit to
+  be checked consistently (API addition).
+- Changed the default web.config file for IIS servers to allow favicon.ico
+  files which are present in the filesystem to be accessed.
+- Fixed inconsistent support for the 'tel' protocol in Drupal's URL filtering
+  functions.
+- Performance improvement: Allowed all hooks to be included in the
+  module_implements() cache, even those that are only invoked on HTTP POST
+  requests.
+- Made the database system replace truncate queries with delete queries when
+  inside a transaction, to fix issues with PostgreSQL and other databases.
+- Fixed a bug which caused nested contextual links to display improperly.
+- Fixed a bug which prevented cached image derivatives from being flushed for
+  private files and other non-default file schemes.
+- Fixed drupal_render() to always return an empty string when there is no
+  output, rather than sometimes returning NULL (minor API change).
+- Added protection to cache_clear_all() to ensure that non-cache tables cannot
+  be truncated (API addition: a new isValidBin() method has been added to the
+  default database cache implementation).
+- Changed the default .htaccess file to support HTTP authorization in CGI
+  environments.
+- Changed the password reset form to pre-fill the username when requested via a
+  URL query parameter, and used this in the error message that appears after a
+  failed login attempt (minor data structure and behavior change).
+- Fixed broken support for foreign keys in the field API.
+- Fixed "No active batch" error when a user cancels their own account.
+- Added a description to the "access content overview" permission on the
+  permissions page (string change).
+- Added a drupal_array_diff_assoc_recursive() function to allow associative
+  arrays to be compared recursively (API addition).
+- Added human-readable labels to image styles, in addition to the existing
+  machine-readable name (API change: https://drupal.org/node/2058503).
+- Moved the drupal_get_hash_salt() function to bootstrap.inc and used it in
+  additional places in the code, for added security in the case where there is
+  no hash salt in settings.php.
+- Fixed a regression in Drupal 7.22 that caused internal server errors for
+  sites running on very old Apache 1.x web servers.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
 
 
 Drupal 7.22, 2013-04-03
 Drupal 7.22, 2013-04-03
 -----------------------
 -----------------------
@@ -53,7 +752,7 @@ Drupal 7.22, 2013-04-03
   downloaded via the Update Manager (UI change).
   downloaded via the Update Manager (UI change).
 - Added an optional "exclusive" flag to installation profile .info files which
 - Added an optional "exclusive" flag to installation profile .info files which
   allows Drupal distributions to force a profile to be selected during
   allows Drupal distributions to force a profile to be selected during
-  installation (API addition).
+  installation (API addition: http://drupal.org/node/1961012).
 - Fixed a bug which caused the database API to not properly close database
 - Fixed a bug which caused the database API to not properly close database
   connections.
   connections.
 - Added a link to the URL for running cron from outside the site to the Cron
 - Added a link to the URL for running cron from outside the site to the Cron
@@ -202,8 +901,8 @@ Drupal 7.15, 2012-08-01
 - Numerous API documentation improvements.
 - Numerous API documentation improvements.
 - Additional automated test coverage.
 - Additional automated test coverage.
 
 
-Drupal 7.14 2012-05-02
-----------------------
+Drupal 7.14, 2012-05-02
+-----------------------
 - Fixed "integrity constraint" fatal errors when rebuilding registry.
 - Fixed "integrity constraint" fatal errors when rebuilding registry.
 - Fixed custom logo and favicon functionality referencing incorrect paths.
 - Fixed custom logo and favicon functionality referencing incorrect paths.
 - Fixed DB Case Sensitivity: Allow BINARY attribute in MySQL.
 - Fixed DB Case Sensitivity: Allow BINARY attribute in MySQL.
@@ -251,12 +950,12 @@ Drupal 7.14 2012-05-02
   - system_update_7061() converts filepaths too aggressively.
   - system_update_7061() converts filepaths too aggressively.
   - Trigger upgrade path: Node triggers removed when upgrading to 7-x from 6.25.
   - Trigger upgrade path: Node triggers removed when upgrading to 7-x from 6.25.
 
 
-Drupal 7.13 2012-05-02
-----------------------
+Drupal 7.13, 2012-05-02
+-----------------------
 - Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-002.
 - Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-002.
 
 
 Drupal 7.12, 2012-02-01
 Drupal 7.12, 2012-02-01
-----------------------
+-----------------------
 - Fixed bug preventing custom menus from receiving an active trail.
 - Fixed bug preventing custom menus from receiving an active trail.
 - Fixed hook_field_delete() no longer invoked during field_purge_data().
 - 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 bug causing entity info cache to not be cleared with the rest of caches.
@@ -290,11 +989,11 @@ Drupal 7.12, 2012-02-01
   cache.
   cache.
 
 
 Drupal 7.11, 2012-02-01
 Drupal 7.11, 2012-02-01
-----------------------
+-----------------------
 - Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-001.
 - Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-001.
 
 
 Drupal 7.10, 2011-12-05
 Drupal 7.10, 2011-12-05
-----------------------
+-----------------------
 - Fixed Content-Language HTTP header to not cause issues with Drush 5.x.
 - Fixed Content-Language HTTP header to not cause issues with Drush 5.x.
 - Reduce memory usage of theme registry (performance).
 - Reduce memory usage of theme registry (performance).
 - Fixed PECL upload progress bar for FileField
 - Fixed PECL upload progress bar for FileField
@@ -647,7 +1346,7 @@ Drupal 7.0, 2011-01-05
       requests.
       requests.
 
 
 Drupal 6.23-dev, xxxx-xx-xx (development release)
 Drupal 6.23-dev, xxxx-xx-xx (development release)
------------------------
+---------------------------
 
 
 Drupal 6.22, 2011-05-25
 Drupal 6.22, 2011-05-25
 -----------------------
 -----------------------
@@ -657,25 +1356,25 @@ Drupal 6.22, 2011-05-25
 - Fixed a variety of other bugs.
 - Fixed a variety of other bugs.
 
 
 Drupal 6.21, 2011-05-25
 Drupal 6.21, 2011-05-25
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2011-001.
 - Fixed security issues (Cross site scripting), see SA-CORE-2011-001.
 
 
 Drupal 6.20, 2010-12-15
 Drupal 6.20, 2010-12-15
-----------------------
+-----------------------
 - Fixed a variety of small bugs, improved code documentation.
 - Fixed a variety of small bugs, improved code documentation.
 
 
 Drupal 6.19, 2010-08-11
 Drupal 6.19, 2010-08-11
-----------------------
+-----------------------
 - Fixed a variety of small bugs, improved code documentation.
 - Fixed a variety of small bugs, improved code documentation.
 
 
 Drupal 6.18, 2010-08-11
 Drupal 6.18, 2010-08-11
-----------------------
+-----------------------
 - Fixed security issues (OpenID authentication bypass, File download access
 - Fixed security issues (OpenID authentication bypass, File download access
   bypass, Comment unpublishing bypass, Actions cross site scripting),
   bypass, Comment unpublishing bypass, Actions cross site scripting),
   see SA-CORE-2010-002.
   see SA-CORE-2010-002.
 
 
 Drupal 6.17, 2010-06-02
 Drupal 6.17, 2010-06-02
-----------------------
+-----------------------
 - Improved PostgreSQL compatibility
 - Improved PostgreSQL compatibility
 - Better PHP 5.3 and PHP 4 compatibility
 - Better PHP 5.3 and PHP 4 compatibility
 - Better browser compatibility of CSS and JS aggregation
 - Better browser compatibility of CSS and JS aggregation
@@ -684,7 +1383,7 @@ Drupal 6.17, 2010-06-02
 - Fixed a variety of other bugs.
 - Fixed a variety of other bugs.
 
 
 Drupal 6.16, 2010-03-03
 Drupal 6.16, 2010-03-03
-----------------------
+-----------------------
 - Fixed security issues (Installation cross site scripting, Open redirection,
 - Fixed security issues (Installation cross site scripting, Open redirection,
   Locale module cross site scripting, Blocked user session regeneration),
   Locale module cross site scripting, Blocked user session regeneration),
   see SA-CORE-2010-001.
   see SA-CORE-2010-001.
@@ -696,12 +1395,12 @@ Drupal 6.16, 2010-03-03
 - Fixed a variety of other bugs.
 - Fixed a variety of other bugs.
 
 
 Drupal 6.15, 2009-12-16
 Drupal 6.15, 2009-12-16
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2009-009.
 - Fixed security issues (Cross site scripting), see SA-CORE-2009-009.
 - Fixed a variety of other bugs.
 - Fixed a variety of other bugs.
 
 
 Drupal 6.14, 2009-09-16
 Drupal 6.14, 2009-09-16
-----------------------
+-----------------------
 - Fixed security issues (OpenID association cross site request forgeries,
 - Fixed security issues (OpenID association cross site request forgeries,
   OpenID impersonation and File upload), see SA-CORE-2009-008.
   OpenID impersonation and File upload), see SA-CORE-2009-008.
 - Changed the system modules page to not run all cache rebuilds; use the
 - Changed the system modules page to not run all cache rebuilds; use the
@@ -710,18 +1409,18 @@ Drupal 6.14, 2009-09-16
 - Fixed a variety of small bugs.
 - Fixed a variety of small bugs.
 
 
 Drupal 6.13, 2009-07-01
 Drupal 6.13, 2009-07-01
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting, Input format access bypass and
 - Fixed security issues (Cross site scripting, Input format access bypass and
   Password leakage in URL), see SA-CORE-2009-007.
   Password leakage in URL), see SA-CORE-2009-007.
 - Fixed a variety of small bugs.
 - Fixed a variety of small bugs.
 
 
 Drupal 6.12, 2009-05-13
 Drupal 6.12, 2009-05-13
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
 - Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
 - Fixed a variety of small bugs.
 - Fixed a variety of small bugs.
 
 
 Drupal 6.11, 2009-04-29
 Drupal 6.11, 2009-04-29
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting and limited information
 - Fixed security issues (Cross site scripting and limited information
   disclosure), see SA-CORE-2009-005
   disclosure), see SA-CORE-2009-005
 - Fixed performance issues with the menu router cache, the update
 - Fixed performance issues with the menu router cache, the update
@@ -729,7 +1428,7 @@ Drupal 6.11, 2009-04-29
 - Fixed a variety of small bugs.
 - Fixed a variety of small bugs.
 
 
 Drupal 6.10, 2009-02-25
 Drupal 6.10, 2009-02-25
-----------------------
+-----------------------
 - Fixed a security issue, (Local file inclusion on Windows),
 - Fixed a security issue, (Local file inclusion on Windows),
   see SA-CORE-2009-003
   see SA-CORE-2009-003
 - Fixed node_feed() so custom fields can show up in RSS feeds.
 - Fixed node_feed() so custom fields can show up in RSS feeds.
@@ -1125,7 +1824,7 @@ Drupal 4.7.9, 2007-12-05
 - fixed a security issue (SQL injection), see SA-2007-031
 - fixed a security issue (SQL injection), see SA-2007-031
 
 
 Drupal 4.7.8, 2007-10-17
 Drupal 4.7.8, 2007-10-17
-----------------------
+------------------------
 - fixed a security issue (HTTP response splitting), see SA-2007-024
 - 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 (Cross site scripting via uploads), see SA-2007-026
 - fixed a security issue (API handling of unpublished comment), see SA-2007-030
 - fixed a security issue (API handling of unpublished comment), see SA-2007-030
@@ -1238,7 +1937,7 @@ Drupal 4.6.11, 2007-01-05
 - Fixed security issue (DoS), see SA-2007-002
 - Fixed security issue (DoS), see SA-2007-002
 
 
 Drupal 4.6.10, 2006-10-18
 Drupal 4.6.10, 2006-10-18
-------------------------
+-------------------------
 - Fixed security issue (XSS), see SA-2006-024
 - Fixed security issue (XSS), see SA-2006-024
 - Fixed security issue (CSRF), see SA-2006-025
 - Fixed security issue (CSRF), see SA-2006-025
 - Fixed security issue (Form action attribute injection), see SA-2006-026
 - Fixed security issue (Form action attribute injection), see SA-2006-026

+ 1 - 1
COPYRIGHT.txt

@@ -1,4 +1,4 @@
-All Drupal code is Copyright 2001 - 2012 by the original authors.
+All Drupal code is Copyright 2001 - 2013 by the original authors.
 
 
 This program is free software; you can redistribute it and/or modify
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 it under the terms of the GNU General Public License as published by

+ 9 - 6
INSTALL.mysql.txt

@@ -20,18 +20,21 @@ initial database files. Next you must log in and set the access database rights:
 Again, you will be asked for the 'username' database password. At the MySQL
 Again, you will be asked for the 'username' database password. At the MySQL
 prompt, enter the following command:
 prompt, enter the following command:
 
 
-  GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER
-  ON databasename.*
+  GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
+  CREATE TEMPORARY TABLES ON databasename.*
   TO 'username'@'localhost' IDENTIFIED BY 'password';
   TO 'username'@'localhost' IDENTIFIED BY 'password';
 
 
-where
+where:
 
 
  'databasename' is the name of your database
  'databasename' is the name of your database
- 'username@localhost' is the username of your MySQL account
+ 'username' is the username of your MySQL account
+ 'localhost' is the web server host where Drupal is installed
  'password' is the password required for that username
  '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.
+Note: Unless the database user/host combination for your Drupal installation
+has all of the privileges listed above (except possibly CREATE TEMPORARY TABLES,
+which is currently only used by Drupal core automated tests and some
+contributed modules), you will not be able to install or run Drupal.
 
 
 If successful, MySQL will reply with:
 If successful, MySQL will reply with:
 
 

+ 3 - 1
INSTALL.txt

@@ -20,8 +20,10 @@ Drupal requires:
   - MySQL 5.0.15 (or greater) (http://www.mysql.com/).
   - MySQL 5.0.15 (or greater) (http://www.mysql.com/).
   - MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully
   - MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully
     compatible drop-in replacement for MySQL.
     compatible drop-in replacement for MySQL.
+  - Percona Server 5.1.70 (or greater) (http://www.percona.com/). Percona
+    Server is a backwards-compatible replacement for MySQL.
   - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/).
   - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/).
-  - SQLite 3.4.2 (or greater) (http://www.sqlite.org/).
+  - SQLite 3.3.7 (or greater) (http://www.sqlite.org/).
 
 
 For more detailed information about Drupal requirements, including a list of
 For more detailed information about Drupal requirements, including a list of
 PHP extensions and configurations that are required, see "System requirements"
 PHP extensions and configurations that are required, see "System requirements"

+ 113 - 112
MAINTAINERS.txt

@@ -1,7 +1,8 @@
 
 
 Drupal core is built and maintained by the Drupal project community. Everyone is
 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
 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.
+contribute in other ways -- see https://www.drupal.org/contribute to find out
+how.
 
 
 Branch maintainers
 Branch maintainers
 ------------------
 ------------------
@@ -9,150 +10,152 @@ Branch maintainers
 The Drupal Core branch maintainers oversee the development of Drupal as a whole.
 The Drupal Core branch maintainers oversee the development of Drupal as a whole.
 The branch maintainers for Drupal 7 are:
 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
+- Dries Buytaert 'dries' https://www.drupal.org/u/dries
+- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
+- (provisional) Drew Webber 'mcdruid' https://www.drupal.org/u/mcdruid
 
 
 
 
 Component maintainers
 Component maintainers
 ---------------------
 ---------------------
 
 
 The Drupal Core component maintainers oversee the development of Drupal
 The Drupal Core component maintainers oversee the development of Drupal
-subsystems. See http://drupal.org/contribute/core-maintainers for more
+subsystems. See https://www.drupal.org/contribute/core-maintainers for more
 information on their responsibilities, and to find out how to become a component
 information on their responsibilities, and to find out how to become a component
 maintainer. Current component maintainers for Drupal 7:
 maintainer. Current component maintainers for Drupal 7:
 
 
 Ajax system
 Ajax system
-- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
-- Earl Miles 'merlinofchaos' http://drupal.org/user/26979
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
 
 
 Base system
 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
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
 
 
 Batch system
 Batch system
-- Yves Chedemois 'yched' http://drupal.org/user/39567
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
 
 
 Cache system
 Cache system
-- Damien Tournoud 'DamZ' http://drupal.org/user/22211
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
 
 
 Cron system
 Cron system
-- Károly Négyesi 'chx' http://drupal.org/user/9446
-- Derek Wright 'dww' http://drupal.org/user/46549
+- Derek Wright 'dww' https://www.drupal.org/u/dww
 
 
 Database system
 Database system
-- Larry Garfield 'Crell' http://drupal.org/user/26398
+- ?
 
 
   - MySQL driver
   - MySQL driver
-    - Larry Garfield 'Crell' http://drupal.org/user/26398
-    - David Strauss 'David Strauss' http://drupal.org/user/93254
+    - David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 
   - PostgreSQL driver
   - PostgreSQL driver
-    - Damien Tournoud 'DamZ' http://drupal.org/user/22211
-    - Josh Waihi 'fiasco' http://drupal.org/user/188162
+    - Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
+    - Josh Waihi 'fiasco' https://www.drupal.org/u/josh-waihi
 
 
   - Sqlite driver
   - Sqlite driver
-    - Damien Tournoud 'DamZ' http://drupal.org/user/22211
-    - Károly Négyesi 'chx' http://drupal.org/user/9446
+    - Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 
 Database update system
 Database update system
-- Károly Négyesi 'chx' http://drupal.org/user/9446
-- Ashok Modi 'BTMash' http://drupal.org/user/60422
+- Ashok Modi 'BTMash' https://www.drupal.org/u/btmash
 
 
 Entity system
 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
+- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
+- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
 
 
 File system
 File system
-- Andrew Morton 'drewish' http://drupal.org/user/34869
-- Aaron Winborn 'aaron' http://drupal.org/user/33420
+- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
+- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
 
 
 Form system
 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
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
+- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
 
 
 Image system
 Image system
-- Andrew Morton 'drewish' http://drupal.org/user/34869
-- Nathan Haug 'quicksketch' http://drupal.org/user/35821
+- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
+- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
 
 
 Install system
 Install system
-- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
 
 
 JavaScript
 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
+- Théodore Biadala 'nod_' https://www.drupal.org/u/nod_
+- Steve De Jonghe 'seutje' https://www.drupal.org/u/seutje
 
 
 Language system
 Language system
-- Francesco Placella 'plach' http://drupal.org/user/183211
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Francesco Placella 'plach' https://www.drupal.org/u/plach
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 
 Lock system
 Lock system
-- Damien Tournoud 'DamZ' http://drupal.org/user/22211
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 
 Mail system
 Mail system
 - ?
 - ?
 
 
 Markup
 Markup
-- Jacine Luisi 'Jacine' http://drupal.org/user/88931
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Jacine Luisi 'Jacine' https://www.drupal.org/u/jacine
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 
 Menu system
 Menu system
-- Peter Wolanin 'pwolanin' http://drupal.org/user/49851
-- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
 
 
 Path system
 Path system
-- Dave Reid 'davereid' http://drupal.org/user/53892
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
 
 
 Render system
 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
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
 
 
 Theme system
 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
+- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Joon Park 'dvessel' https://www.drupal.org/u/dvessel
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 
 Token system
 Token system
-- Dave Reid 'davereid' http://drupal.org/user/53892
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 
 XML-RPC system
 XML-RPC system
-- Frederic G. Marand 'fgm' http://drupal.org/user/27985
+- Frederic G. Marand 'fgm' https://www.drupal.org/u/fgm
 
 
 
 
 Topic coordinators
 Topic coordinators
 ------------------
 ------------------
 
 
 Accessibility
 Accessibility
-- Everett Zufelt 'Everett Zufelt' http://drupal.org/user/406552
-- Brandon Bowersox-Johnson 'bowersox' http://drupal.org/user/186415
+- Everett Zufelt 'Everett Zufelt' https://www.drupal.org/u/everett-zufelt
+- Brandon Bowersox-Johnson 'bowersox' https://www.drupal.org/u/bowersox
 
 
 Documentation
 Documentation
-- Jennifer Hodgdon 'jhodgdon' http://drupal.org/user/155601
-
-Security
-- Greg Knaddison 'greggles' http://drupal.org/user/36762
+- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
 
 
 Translations
 Translations
-- Gerhard Killesreiter 'killes' http://drupal.org/user/83
+- Gerhard Killesreiter 'killes' https://www.drupal.org/u/gerhard-killesreiter
 
 
 User experience and usability
 User experience and usability
-- Roy Scholten 'yoroy' http://drupal.org/user/41502
-- Bojhan Somers 'Bojhan' http://drupal.org/user/87969
+- Roy Scholten 'yoroy' https://www.drupal.org/u/yoroy
+- Bojhan Somers 'Bojhan' https://www.drupal.org/u/bojhan
 
 
 Node Access
 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
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- Ken Rickard 'agentrickard' https://www.drupal.org/u/agentrickard
+
+
+Security team
+-----------------
+
+To report a security issue, see: https://www.drupal.org/security-team/report-issue
+
+The Drupal security team provides Security Advisories for vulnerabilities,
+assists developers in resolving security issues, and provides security
+documentation. See https://www.drupal.org/security-team for more information.
+The security team lead is:
+
+- Michael Hess 'mlhess' https://www.drupal.org/u/mlhess
+
 
 
 Module maintainers
 Module maintainers
 ------------------
 ------------------
@@ -161,143 +164,141 @@ Aggregator module
 - ?
 - ?
 
 
 Block module
 Block module
-- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 
 Blog module
 Blog module
 - ?
 - ?
 
 
 Book module
 Book module
-- Peter Wolanin 'pwolanin' http://drupal.org/user/49851
+- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
 
 
 Color module
 Color module
 - ?
 - ?
 
 
 Comment module
 Comment module
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
 
 
 Contact module
 Contact module
-- Dave Reid 'davereid' http://drupal.org/user/53892
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 
 Contextual module
 Contextual module
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 
 Dashboard module
 Dashboard module
 - ?
 - ?
 
 
 Database logging module
 Database logging module
-- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063
+- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
 
 
 Field module
 Field module
-- Yves Chedemois 'yched' http://drupal.org/user/39567
-- Barry Jaspan 'bjaspan' http://drupal.org/user/46413
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
+- Barry Jaspan 'bjaspan' https://www.drupal.org/u/bjaspan
 
 
 Field UI module
 Field UI module
-- Yves Chedemois 'yched' http://drupal.org/user/39567
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
 
 
 File module
 File module
-- Aaron Winborn 'aaron' http://drupal.org/user/33420
+- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
 
 
 Filter module
 Filter module
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 
 Forum module
 Forum module
-- Lee Rowlands 'larowlan' http://drupal.org/user/395439
+- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
 
 
 Help module
 Help module
 - ?
 - ?
 
 
 Image module
 Image module
-- Nathan Haug 'quicksketch' http://drupal.org/user/35821
+- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
 
 
 Locale module
 Locale module
-- Gábor Hojtsy 'Gábor Hojtsy' http://drupal.org/user/4166
+- Gábor Hojtsy 'Gábor Hojtsy' https://www.drupal.org/u/gábor-hojtsy
 
 
 Menu module
 Menu module
 - ?
 - ?
 
 
 Node module
 Node module
-- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
-- David Strauss 'David Strauss' http://drupal.org/user/93254
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 
 OpenID module
 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
+- Vojtech Kusy 'wojtha' https://www.drupal.org/u/wojtha
+- Christian Schmidt 'c960657' https://www.drupal.org/u/c960657
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 
 Overlay module
 Overlay module
-- Katherine Senzee 'ksenzee' http://drupal.org/user/139855
+- Katherine Senzee 'ksenzee' https://www.drupal.org/u/ksenzee
 
 
 Path module
 Path module
-- Dave Reid 'davereid' http://drupal.org/user/53892
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 
 PHP module
 PHP module
 - ?
 - ?
 
 
 Poll module
 Poll module
-- Andrei Mateescu 'amateescu' http://drupal.org/user/729614
+- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu
 
 
 Profile module
 Profile module
 - ?
 - ?
 
 
 RDF module
 RDF module
-- Stéphane Corlosquet 'scor' http://drupal.org/user/52142
+- Stéphane Corlosquet 'scor' https://www.drupal.org/u/scor
 
 
 Search module
 Search module
-- Doug Green 'douggreen' http://drupal.org/user/29191
+- Doug Green 'douggreen' https://www.drupal.org/u/douggreen
 
 
 Shortcut module
 Shortcut module
-- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
 
 
 Simpletest module
 Simpletest module
-- Jimmy Berry 'boombatower' http://drupal.org/user/214218
-- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Jimmy Berry 'boombatower' https://www.drupal.org/u/boombatower
 
 
 Statistics module
 Statistics module
-- Tim Millwood 'timmillwood' http://drupal.org/user/227849
+- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood
 
 
 Syslog module
 Syslog module
-- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063
+- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
 
 
 System module
 System module
 - ?
 - ?
 
 
 Taxonomy 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
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
+- Benjamin Doherty 'bangpound' https://www.drupal.org/u/bangpound
 
 
 Toolbar module
 Toolbar module
 - ?
 - ?
 
 
 Tracker module
 Tracker module
-- David Strauss 'David Strauss' http://drupal.org/user/93254
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 
 Translation module
 Translation module
-- Francesco Placella 'plach' http://drupal.org/user/183211
+- Francesco Placella 'plach' https://www.drupal.org/u/plach
 
 
 Trigger module
 Trigger module
 - ?
 - ?
 
 
 Update module
 Update module
-- Derek Wright 'dww' http://drupal.org/user/46549
+- Derek Wright 'dww' https://www.drupal.org/u/dww
 
 
 User module
 User module
-- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
-- David Strauss 'David Strauss' http://drupal.org/user/93254
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 
 
 
 Theme maintainers
 Theme maintainers
 -----------------
 -----------------
 
 
 Bartik theme
 Bartik theme
-- Jen Simmons 'jensimmons' http://drupal.org/user/140882
-- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393
+- Jen Simmons 'jensimmons' https://www.drupal.org/u/jensimmons
+- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
 
 
 Garland theme
 Garland theme
-- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 
 Seven theme
 Seven theme
-- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393
+- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
 
 
 Stark theme
 Stark theme
-- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin

+ 6 - 6
README.txt

@@ -71,12 +71,12 @@ profiles/your_site_profile/themes respectively to restrict their usage to only
 sites that were installed with that specific profile.
 sites that were installed with that specific profile.
 
 
 More about installation profiles and distributions:
 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
+ * 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
 APPEARANCE
 ----------
 ----------

+ 10 - 0
UPGRADE.txt

@@ -64,6 +64,9 @@ following the instructions in the INTRODUCTION section at the top of this file:
    Sometimes an update includes changes to default.settings.php (this will be
    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:
    noted in the release notes). If that's the case, follow these steps:
 
 
+   - Locate your settings.php file in the /sites/* directory. (Typically
+     sites/default.)
+
    - Make a backup copy of your settings.php file, with a different file name.
    - 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
    - Make a copy of the new default.settings.php file, and name the copy
@@ -74,6 +77,13 @@ following the instructions in the INTRODUCTION section at the top of this file:
      database information, and you will also want to copy in any other
      database information, and you will also want to copy in any other
      customizations you have added.
      customizations you have added.
 
 
+   You can find the release notes for your version at
+   https://www.drupal.org/project/drupal. At bottom of the project page under
+   "Downloads" use the link for your version of Drupal to view the release
+   notes. If your version is not listed, use the 'View all releases' link. From
+   this page you can scroll down or use the filter to find your version and its
+   release notes.
+
 4. Download the latest Drupal 7.x release from http://drupal.org to a
 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
    directory outside of your web root. Extract the archive and copy the files
    into your Drupal directory.
    into your Drupal directory.

+ 117 - 11
includes/ajax.inc

@@ -211,7 +211,7 @@
  *
  *
  * When returning an Ajax command array, it is often useful to have
  * When returning an Ajax command array, it is often useful to have
  * status messages rendered along with other tasks in the command array.
  * status messages rendered along with other tasks in the command array.
- * In that case the the Ajax commands array may be constructed like this:
+ * In that case the Ajax commands array may be constructed like this:
  * @code
  * @code
  *   $commands = array();
  *   $commands = array();
  *   $commands[] = ajax_command_replace(NULL, $output);
  *   $commands[] = ajax_command_replace(NULL, $output);
@@ -230,6 +230,10 @@
  *   functions.
  *   functions.
  */
  */
 function ajax_render($commands = array()) {
 function ajax_render($commands = array()) {
+  // Although ajax_deliver() does this, some contributed and custom modules
+  // render Ajax responses without using that delivery callback.
+  ajax_set_verification_header();
+
   // Ajax responses aren't rendered with html.tpl.php, so we have to call
   // 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
   // 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
   // during this request to be loaded by the page. We only want to send back
@@ -276,7 +280,7 @@ function ajax_render($commands = array()) {
 
 
   $extra_commands = array();
   $extra_commands = array();
   if (!empty($styles)) {
   if (!empty($styles)) {
-    $extra_commands[] = ajax_command_prepend('head', $styles);
+    $extra_commands[] = ajax_command_add_css($styles);
   }
   }
   if (!empty($scripts_header)) {
   if (!empty($scripts_header)) {
     $extra_commands[] = ajax_command_prepend('head', $scripts_header);
     $extra_commands[] = ajax_command_prepend('head', $scripts_header);
@@ -290,9 +294,10 @@ function ajax_render($commands = array()) {
 
 
   // Now add a command to merge changes and additions to Drupal.settings.
   // Now add a command to merge changes and additions to Drupal.settings.
   $scripts = drupal_add_js();
   $scripts = drupal_add_js();
+  drupal_alter('js', $scripts);
   if (!empty($scripts['settings'])) {
   if (!empty($scripts['settings'])) {
     $settings = $scripts['settings'];
     $settings = $scripts['settings'];
-    array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $settings['data']), TRUE));
+    array_unshift($commands, ajax_command_settings(drupal_array_merge_deep_array($settings['data']), TRUE));
   }
   }
 
 
   // Allow modules to alter any Ajax response.
   // Allow modules to alter any Ajax response.
@@ -308,10 +313,11 @@ function ajax_render($commands = array()) {
  * pulls the form info from $_POST.
  * pulls the form info from $_POST.
  *
  *
  * @return
  * @return
- *   An array containing the $form and $form_state. Use the list() function
- *   to break these apart:
+ *   An array containing the $form, $form_state, $form_id, $form_build_id and an
+ *   initial list of Ajax $commands. Use the list() function to break these
+ *   apart:
  *   @code
  *   @code
- *     list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
+ *     list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
  *   @endcode
  *   @endcode
  */
  */
 function ajax_get_form() {
 function ajax_get_form() {
@@ -331,6 +337,17 @@ function ajax_get_form() {
     drupal_exit();
     drupal_exit();
   }
   }
 
 
+  // When a page level cache is enabled, the form-build id might have been
+  // replaced from within form_get_cache. If this is the case, it is also
+  // necessary to update it in the browser by issuing an appropriate Ajax
+  // command.
+  $commands = array();
+  if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) {
+    // If the form build ID has changed, issue an Ajax command to update it.
+    $commands[] = ajax_command_update_build_id($form);
+    $form_build_id = $form['#build_id'];
+  }
+
   // Since some of the submit handlers are run, redirects need to be disabled.
   // Since some of the submit handlers are run, redirects need to be disabled.
   $form_state['no_redirect'] = TRUE;
   $form_state['no_redirect'] = TRUE;
 
 
@@ -345,7 +362,7 @@ function ajax_get_form() {
   $form_state['input'] = $_POST;
   $form_state['input'] = $_POST;
   $form_id = $form['#form_id'];
   $form_id = $form['#form_id'];
 
 
-  return array($form, $form_state, $form_id, $form_build_id);
+  return array($form, $form_state, $form_id, $form_build_id, $commands);
 }
 }
 
 
 /**
 /**
@@ -366,7 +383,7 @@ function ajax_get_form() {
  * @see system_menu()
  * @see system_menu()
  */
  */
 function ajax_form_callback() {
 function ajax_form_callback() {
-  list($form, $form_state) = ajax_get_form();
+  list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
   drupal_process_form($form['#form_id'], $form, $form_state);
   drupal_process_form($form['#form_id'], $form, $form_state);
 
 
   // We need to return the part of the form (or some other content) that needs
   // We need to return the part of the form (or some other content) that needs
@@ -378,8 +395,20 @@ function ajax_form_callback() {
   if (!empty($form_state['triggering_element'])) {
   if (!empty($form_state['triggering_element'])) {
     $callback = $form_state['triggering_element']['#ajax']['callback'];
     $callback = $form_state['triggering_element']['#ajax']['callback'];
   }
   }
-  if (!empty($callback) && function_exists($callback)) {
-    return $callback($form, $form_state);
+  if (!empty($callback) && is_callable($callback)) {
+    $result = $callback($form, $form_state);
+
+    if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) {
+      // Turn the response into a #type=ajax array if it isn't one already.
+      $result = array(
+        '#type' => 'ajax',
+        '#commands' => ajax_prepare_response($result),
+      );
+    }
+
+    $result['#commands'] = array_merge($commands, $result['#commands']);
+
+    return $result;
   }
   }
 }
 }
 
 
@@ -463,6 +492,9 @@ function ajax_deliver($page_callback_result) {
     }
     }
   }
   }
 
 
+  // Let ajax.js know that this response is safe to process.
+  ajax_set_verification_header();
+
   // Print the response.
   // Print the response.
   $commands = ajax_prepare_response($page_callback_result);
   $commands = ajax_prepare_response($page_callback_result);
   $json = ajax_render($commands);
   $json = ajax_render($commands);
@@ -552,6 +584,29 @@ function ajax_prepare_response($page_callback_result) {
   return $commands;
   return $commands;
 }
 }
 
 
+/**
+ * Sets a response header for ajax.js to trust the response body.
+ *
+ * It is not safe to invoke Ajax commands within user-uploaded files, so this
+ * header protects against those being invoked.
+ *
+ * @see Drupal.ajax.options.success()
+ */
+function ajax_set_verification_header() {
+  $added = &drupal_static(__FUNCTION__);
+
+  // User-uploaded files cannot set any response headers, so a custom header is
+  // used to indicate to ajax.js that this response is safe. Note that most
+  // Ajax requests bound using the Form API will be protected by having the URL
+  // flagged as trusted in Drupal.settings, so this header is used only for
+  // things like custom markup that gets Ajax behaviors attached.
+  if (empty($added)) {
+    drupal_add_http_header('X-Drupal-Ajax-Token', '1');
+    // Avoid sending the header twice.
+    $added = TRUE;
+  }
+}
+
 /**
 /**
  * Performs end-of-Ajax-request tasks.
  * Performs end-of-Ajax-request tasks.
  *
  *
@@ -740,7 +795,12 @@ function ajax_pre_render_element($element) {
 
 
     $element['#attached']['js'][] = array(
     $element['#attached']['js'][] = array(
       'type' => 'setting',
       'type' => 'setting',
-      'data' => array('ajax' => array($element['#id'] => $settings)),
+      'data' => array(
+        'ajax' => array($element['#id'] => $settings),
+        'urlIsAjaxTrusted' => array(
+          $settings['url'] => TRUE,
+        ),
+      ),
     );
     );
 
 
     // Indicate that Ajax processing was successful.
     // Indicate that Ajax processing was successful.
@@ -1210,3 +1270,49 @@ function ajax_command_restripe($selector) {
     'selector' => $selector,
     'selector' => $selector,
   );
   );
 }
 }
+
+/**
+ * Creates a Drupal Ajax 'update_build_id' command.
+ *
+ * This command updates the value of a hidden form_build_id input element on a
+ * form. It requires the form passed in to have keys for both the old build ID
+ * in #build_id_old and the new build ID in #build_id.
+ *
+ * The primary use case for this Ajax command is to serve a new build ID to a
+ * form served from the cache to an anonymous user, preventing one anonymous
+ * user from accessing the form state of another anonymous users on Ajax enabled
+ * forms.
+ *
+ * @param $form
+ *   The form array representing the form whose build ID should be updated.
+ */
+function ajax_command_update_build_id($form) {
+  return array(
+    'command' => 'updateBuildId',
+    'old' => $form['#build_id_old'],
+    'new' => $form['#build_id'],
+  );
+}
+
+/**
+ * Creates a Drupal Ajax 'add_css' command.
+ *
+ * This method will add css via ajax in a cross-browser compatible way.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.add_css()
+ * defined in misc/ajax.js.
+ *
+ * @param $styles
+ *   A string that contains the styles to be added.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ *
+ * @see misc/ajax.js
+ */
+function ajax_command_add_css($styles) {
+  return array(
+    'command' => 'add_css',
+    'data' => $styles,
+  );
+}

+ 9 - 10
includes/batch.inc

@@ -460,10 +460,10 @@ function _batch_finished() {
       if (isset($batch_set['file']) && is_file($batch_set['file'])) {
       if (isset($batch_set['file']) && is_file($batch_set['file'])) {
         include_once DRUPAL_ROOT . '/' . $batch_set['file'];
         include_once DRUPAL_ROOT . '/' . $batch_set['file'];
       }
       }
-      if (function_exists($batch_set['finished'])) {
+      if (is_callable($batch_set['finished'])) {
         $queue = _batch_queue($batch_set);
         $queue = _batch_queue($batch_set);
         $operations = $queue->getAllItems();
         $operations = $queue->getAllItems();
-        $batch_set['finished']($batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
+        call_user_func($batch_set['finished'], $batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
       }
       }
     }
     }
   }
   }
@@ -478,18 +478,17 @@ function _batch_finished() {
         $queue->deleteQueue();
         $queue->deleteQueue();
       }
       }
     }
     }
+    // Clean-up the session. Not needed for CLI updates.
+    if (isset($_SESSION)) {
+      unset($_SESSION['batches'][$batch['id']]);
+      if (empty($_SESSION['batches'])) {
+        unset($_SESSION['batches']);
+      }
+    }
   }
   }
   $_batch = $batch;
   $_batch = $batch;
   $batch = NULL;
   $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.
   // Redirect if needed.
   if ($_batch['progressive']) {
   if ($_batch['progressive']) {
     // Revert the 'destination' that was saved in batch_process().
     // Revert the 'destination' that was saved in batch_process().

File diff suppressed because it is too large
+ 516 - 130
includes/bootstrap.inc


+ 72 - 27
includes/cache.inc

@@ -14,6 +14,7 @@
  *
  *
  * @param $bin
  * @param $bin
  *   The cache bin for which the cache object should be returned.
  *   The cache bin for which the cache object should be returned.
+ *
  * @return DrupalCacheInterface
  * @return DrupalCacheInterface
  *   The cache object associated with the specified bin.
  *   The cache object associated with the specified bin.
  *
  *
@@ -98,9 +99,11 @@ function cache_get_multiple(array &$cids, $bin = 'cache') {
  * @param $data
  * @param $data
  *   The data to store in the cache. Complex data types will be automatically
  *   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
  *   serialized before insertion. Strings will be stored as plain text and are
- *   not serialized.
+ *   not serialized. Some storage engines only allow objects up to a maximum of
+ *   1MB in size to be stored by default. When caching large arrays or similar,
+ *   take care to ensure $data does not exceed this size.
  * @param $bin
  * @param $bin
- *   The cache bin to store the data in. Valid core values are:
+ *   (optional) The cache bin to store the data in. Valid core values are:
  *   - cache: (default) Generic cache storage bin (used for theme registry,
  *   - cache: (default) Generic cache storage bin (used for theme registry,
  *     locale date, list of simpletest tests, etc.).
  *     locale date, list of simpletest tests, etc.).
  *   - cache_block: Stores the content of various blocks.
  *   - cache_block: Stores the content of various blocks.
@@ -119,7 +122,12 @@ function cache_get_multiple(array &$cids, $bin = 'cache') {
  *     the administrator panel.
  *     the administrator panel.
  *   - cache_path: Stores the system paths that have an alias.
  *   - cache_path: Stores the system paths that have an alias.
  * @param $expire
  * @param $expire
- *   One of the following values:
+ *   (optional) Controls the maximum lifetime of this cache entry. Note that
+ *   caches might be subject to clearing at any time, so this setting does not
+ *   guarantee a minimum lifetime. With this in mind, the cache should not be
+ *   used for data that must be kept during a cache clear, like sessions.
+ *
+ *   Use one of the following values:
  *   - CACHE_PERMANENT: Indicates that the item should never be removed unless
  *   - CACHE_PERMANENT: Indicates that the item should never be removed unless
  *     explicitly told to using cache_clear_all() with a cache ID.
  *     explicitly told to using cache_clear_all() with a cache ID.
  *   - CACHE_TEMPORARY: Indicates that the item should be removed at the next
  *   - CACHE_TEMPORARY: Indicates that the item should be removed at the next
@@ -137,18 +145,20 @@ function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
 /**
 /**
  * Expires data from the cache.
  * Expires data from the cache.
  *
  *
- * If called without arguments, expirable entries will be cleared from the
- * cache_page and cache_block bins.
+ * If called with the arguments $cid and $bin set to NULL or omitted, then
+ * expirable entries will be cleared from the cache_page and cache_block bins,
+ * and the $wildcard argument is ignored.
  *
  *
  * @param $cid
  * @param $cid
- *   If set, the cache ID to delete. Otherwise, all cache entries that can
- *   expire are deleted.
+ *   If set, the cache ID or an array of cache IDs. Otherwise, all cache entries
+ *   that can expire are deleted. The $wildcard argument will be ignored if set
+ *   to NULL.
  * @param $bin
  * @param $bin
  *   If set, the cache bin to delete from. Mandatory argument if $cid is set.
  *   If set, the cache bin to delete from. Mandatory argument if $cid is set.
  * @param $wildcard
  * @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.
+ *   If TRUE, the $cid argument must contain a string value and 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 is emptied.
  */
  */
 function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
 function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
   if (!isset($cid) && !isset($bin)) {
   if (!isset($cid) && !isset($bin)) {
@@ -217,13 +227,6 @@ function cache_is_empty($bin) {
  * @see DrupalDatabaseCache
  * @see DrupalDatabaseCache
  */
  */
 interface DrupalCacheInterface {
 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.
    * Returns data from the persistent cache.
@@ -259,10 +262,17 @@ interface DrupalCacheInterface {
    *   The cache ID of the data to store.
    *   The cache ID of the data to store.
    * @param $data
    * @param $data
    *   The data to store in the cache. Complex data types will be automatically
    *   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.
+   *   serialized before insertion. Strings will be stored as plain text and not
+   *   serialized. Some storage engines only allow objects up to a maximum of
+   *   1MB in size to be stored by default. When caching large arrays or
+   *   similar, take care to ensure $data does not exceed this size.
    * @param $expire
    * @param $expire
-   *   One of the following values:
+   *   (optional) Controls the maximum lifetime of this cache entry. Note that
+   *   caches might be subject to clearing at any time, so this setting does not
+   *   guarantee a minimum lifetime. With this in mind, the cache should not be
+   *   used for data that must be kept during a cache clear, like sessions.
+   *
+   *   Use one of the following values:
    *   - CACHE_PERMANENT: Indicates that the item should never be removed unless
    *   - CACHE_PERMANENT: Indicates that the item should never be removed unless
    *     explicitly told to using cache_clear_all() with a cache ID.
    *     explicitly told to using cache_clear_all() with a cache ID.
    *   - CACHE_TEMPORARY: Indicates that the item should be removed at the next
    *   - CACHE_TEMPORARY: Indicates that the item should be removed at the next
@@ -280,12 +290,14 @@ interface DrupalCacheInterface {
    * cache_page and cache_block bins.
    * cache_page and cache_block bins.
    *
    *
    * @param $cid
    * @param $cid
-   *   If set, the cache ID to delete. Otherwise, all cache entries that can
-   *   expire are deleted.
+   *   If set, the cache ID or an array of cache IDs. Otherwise, all cache
+   *   entries that can expire are deleted. The $wildcard argument will be
+   *   ignored if set to NULL.
    * @param $wildcard
    * @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.
+   *   If TRUE, the $cid argument must contain a string value and 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 is emptied.
    */
    */
   function clear($cid = NULL, $wildcard = FALSE);
   function clear($cid = NULL, $wildcard = FALSE);
 
 
@@ -311,7 +323,10 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
   protected $bin;
   protected $bin;
 
 
   /**
   /**
-   * Constructs a new DrupalDatabaseCache object.
+   * Constructs a DrupalDatabaseCache object.
+   *
+   * @param $bin
+   *   The cache bin for which the object is created.
    */
    */
   function __construct($bin) {
   function __construct($bin) {
     $this->bin = $bin;
     $this->bin = $bin;
@@ -505,7 +520,16 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
     else {
     else {
       if ($wildcard) {
       if ($wildcard) {
         if ($cid == '*') {
         if ($cid == '*') {
-          db_truncate($this->bin)->execute();
+          // Check if $this->bin is a cache table before truncating. Other
+          // cache_clear_all() operations throw a PDO error in this situation,
+          // so we don't need to verify them first. This ensures that non-cache
+          // tables cannot be truncated accidentally.
+          if ($this->isValidBin()) {
+            db_truncate($this->bin)->execute();
+          }
+          else {
+            throw new Exception(t('Invalid or missing cache bin specified: %bin', array('%bin' => $this->bin)));
+          }
         }
         }
         else {
         else {
           db_delete($this->bin)
           db_delete($this->bin)
@@ -542,4 +566,25 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
       ->fetchField();
       ->fetchField();
     return empty($result);
     return empty($result);
   }
   }
+
+  /**
+   * Checks if $this->bin represents a valid cache table.
+   *
+   * This check is required to ensure that non-cache tables are not truncated
+   * accidentally when calling cache_clear_all().
+   *
+   * @return boolean
+   */
+  function isValidBin() {
+    if ($this->bin == 'cache' || substr($this->bin, 0, 6) == 'cache_') {
+      // Skip schema check for bins with standard table names.
+      return TRUE;
+    }
+    // These fields are required for any cache table.
+    $fields = array('cid', 'data', 'expire', 'created', 'serialized');
+    // Load the table schema.
+    $schema = drupal_get_schema($this->bin);
+    // Confirm that all fields are present.
+    return isset($schema['fields']) && !array_diff($fields, array_keys($schema['fields']));
+  }
 }
 }

File diff suppressed because it is too large
+ 351 - 138
includes/common.inc


+ 81 - 22
includes/database/database.inc

@@ -28,18 +28,21 @@
  * Most Drupal database SELECT queries are performed by a call to db_query() or
  * 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
  * db_query_range(). Module authors should also consider using the PagerDefault
  * Extender for queries that return results that need to be presented on
  * 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.
+ * multiple pages (see https://drupal.org/node/508796), and the TableSort
+ * Extender for generating appropriate queries for sortable tables
+ * (see https://drupal.org/node/1848372).
  *
  *
  * For example, one might wish to return a list of the most recent 10 nodes
  * 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
  * authored by a given user. Instead of directly issuing the SQL query
  * @code
  * @code
- * SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid LIMIT 0, 10;
+ * SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid
+ *   ORDER BY n.created DESC LIMIT 0, 10;
  * @endcode
  * @endcode
  * one would instead call the Drupal functions:
  * one would instead call the Drupal functions:
  * @code
  * @code
  * $result = db_query_range('SELECT n.nid, n.title, n.created
  * $result = db_query_range('SELECT n.nid, n.title, n.created
- *   FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid));
+ *   FROM {node} n WHERE n.uid = :uid
+ *   ORDER BY n.created DESC', 0, 10, array(':uid' => $uid));
  * foreach ($result as $record) {
  * foreach ($result as $record) {
  *   // Perform operations on $record->title, etc. here.
  *   // Perform operations on $record->title, etc. here.
  * }
  * }
@@ -179,7 +182,7 @@
  * concrete implementation of it to support special handling required by that
  * concrete implementation of it to support special handling required by that
  * database.
  * database.
  *
  *
- * @see http://php.net/manual/en/book.pdo.php
+ * @see http://php.net/manual/book.pdo.php
  */
  */
 abstract class DatabaseConnection extends PDO {
 abstract class DatabaseConnection extends PDO {
 
 
@@ -293,6 +296,20 @@ abstract class DatabaseConnection extends PDO {
    */
    */
   protected $prefixReplace = array();
   protected $prefixReplace = array();
 
 
+  /**
+   * List of escaped database, table, and field names, keyed by unescaped names.
+   *
+   * @var array
+   */
+  protected $escapedNames = array();
+
+  /**
+   * List of escaped aliases names, keyed by unescaped aliases.
+   *
+   * @var array
+   */
+  protected $escapedAliases = array();
+
   function __construct($dsn, $username, $password, $driver_options = array()) {
   function __construct($dsn, $username, $password, $driver_options = array()) {
     // Initialize and prepare the connection prefix.
     // Initialize and prepare the connection prefix.
     $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
     $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
@@ -623,7 +640,7 @@ abstract class DatabaseConnection extends PDO {
    *   A sanitized version of the query comment string.
    *   A sanitized version of the query comment string.
    */
    */
   protected function filterComment($comment = '') {
   protected function filterComment($comment = '') {
-    return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment);
+    return strtr($comment, array('*' => ' * '));
   }
   }
 
 
   /**
   /**
@@ -653,7 +670,7 @@ abstract class DatabaseConnection extends PDO {
    * @return DatabaseStatementInterface
    * @return DatabaseStatementInterface
    *   This method will return one of: the executed statement, the number of
    *   This method will return one of: the executed statement, the number of
    *   rows affected by the query (not the number matched), or the generated
    *   rows affected by the query (not the number matched), or the generated
-   *   insert IT of the last query, depending on the value of
+   *   insert ID of the last query, depending on the value of
    *   $options['return']. Typically that value will be set by default or a
    *   $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,
    *   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
    *   this method will return NULL and may throw an exception if
@@ -733,7 +750,7 @@ abstract class DatabaseConnection extends PDO {
     // to expand it out into a comma-delimited set of placeholders.
     // to expand it out into a comma-delimited set of placeholders.
     foreach (array_filter($args, 'is_array') as $key => $data) {
     foreach (array_filter($args, 'is_array') as $key => $data) {
       $new_keys = array();
       $new_keys = array();
-      foreach ($data as $i => $value) {
+      foreach (array_values($data) as $i => $value) {
         // This assumes that there are no other placeholders that use the same
         // This assumes that there are no other placeholders that use the same
         // name.  For example, if the array placeholder is defined as :example
         // name.  For example, if the array placeholder is defined as :example
         // and there is already an :example_2 placeholder, this will generate
         // and there is already an :example_2 placeholder, this will generate
@@ -916,11 +933,14 @@ abstract class DatabaseConnection extends PDO {
    * For some database drivers, it may also wrap the table name in
    * For some database drivers, it may also wrap the table name in
    * database-specific escape characters.
    * database-specific escape characters.
    *
    *
-   * @return
+   * @return string
    *   The sanitized table name string.
    *   The sanitized table name string.
    */
    */
   public function escapeTable($table) {
   public function escapeTable($table) {
-    return preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
+    if (!isset($this->escapedNames[$table])) {
+      $this->escapedNames[$table] = preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
+    }
+    return $this->escapedNames[$table];
   }
   }
 
 
   /**
   /**
@@ -930,11 +950,14 @@ abstract class DatabaseConnection extends PDO {
    * For some database drivers, it may also wrap the field name in
    * For some database drivers, it may also wrap the field name in
    * database-specific escape characters.
    * database-specific escape characters.
    *
    *
-   * @return
+   * @return string
    *   The sanitized field name string.
    *   The sanitized field name string.
    */
    */
   public function escapeField($field) {
   public function escapeField($field) {
-    return preg_replace('/[^A-Za-z0-9_.]+/', '', $field);
+    if (!isset($this->escapedNames[$field])) {
+      $this->escapedNames[$field] = preg_replace('/[^A-Za-z0-9_.]+/', '', $field);
+    }
+    return $this->escapedNames[$field];
   }
   }
 
 
   /**
   /**
@@ -945,11 +968,14 @@ abstract class DatabaseConnection extends PDO {
    * DatabaseConnection::escapeTable(), this doesn't allow the period (".")
    * DatabaseConnection::escapeTable(), this doesn't allow the period (".")
    * because that is not allowed in aliases.
    * because that is not allowed in aliases.
    *
    *
-   * @return
+   * @return string
    *   The sanitized field name string.
    *   The sanitized field name string.
    */
    */
   public function escapeAlias($field) {
   public function escapeAlias($field) {
-    return preg_replace('/[^A-Za-z0-9_]+/', '', $field);
+    if (!isset($this->escapedAliases[$field])) {
+      $this->escapedAliases[$field] = preg_replace('/[^A-Za-z0-9_]+/', '', $field);
+    }
+    return $this->escapedAliases[$field];
   }
   }
 
 
   /**
   /**
@@ -1310,6 +1336,39 @@ abstract class DatabaseConnection extends PDO {
    *   also larger than the $existing_id if one was passed in.
    *   also larger than the $existing_id if one was passed in.
    */
    */
   abstract public function nextId($existing_id = 0);
   abstract public function nextId($existing_id = 0);
+
+  /**
+   * Checks whether utf8mb4 support is configurable in settings.php.
+   *
+   * @return bool
+   */
+  public function utf8mb4IsConfigurable() {
+    // Since 4 byte UTF-8 is not supported by default, there is nothing to
+    // configure.
+    return FALSE;
+  }
+
+  /**
+   * Checks whether utf8mb4 support is currently active.
+   *
+   * @return bool
+   */
+  public function utf8mb4IsActive() {
+    // Since 4 byte UTF-8 is not supported by default, there is nothing to
+    // activate.
+    return FALSE;
+  }
+
+  /**
+   * Checks whether utf8mb4 support is available on the current database system.
+   *
+   * @return bool
+   */
+  public function utf8mb4IsSupported() {
+    // By default we assume that the database backend may not support 4 byte
+    // UTF-8.
+    return FALSE;
+  }
 }
 }
 
 
 /**
 /**
@@ -1986,7 +2045,7 @@ interface DatabaseStatementInterface extends Traversable {
   /**
   /**
    * Sets the default fetch mode for this statement.
    * Sets the default fetch mode for this statement.
    *
    *
-   * See http://php.net/manual/en/pdo.constants.php for the definition of the
+   * See http://php.net/manual/pdo.constants.php for the definition of the
    * constants used.
    * constants used.
    *
    *
    * @param $mode
    * @param $mode
@@ -2005,7 +2064,7 @@ interface DatabaseStatementInterface extends Traversable {
   /**
   /**
    * Fetches the next row from a result set.
    * Fetches the next row from a result set.
    *
    *
-   * See http://php.net/manual/en/pdo.constants.php for the definition of the
+   * See http://php.net/manual/pdo.constants.php for the definition of the
    * constants used.
    * constants used.
    *
    *
    * @param $mode
    * @param $mode
@@ -2380,14 +2439,14 @@ function db_query_range($query, $from, $count, array $args = array(), array $opt
 }
 }
 
 
 /**
 /**
- * Executes a query string and saves the result set to a temporary table.
+ * Executes a SELECT query string and saves the result set to a temporary table.
  *
  *
  * The execution of the query string happens against the active database.
  * The execution of the query string happens against the active database.
  *
  *
  * @param $query
  * @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.
+ *   The prepared SELECT 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
  * @param $args
  *   An array of values to substitute into the query. If the query uses named
  *   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
  *   placeholders, this is an associative array in any order. If the query uses
@@ -2829,7 +2888,7 @@ function db_drop_table($table) {
  *   will be set to the value of the key in all rows. This is most useful for
  *   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.
  *   creating NOT NULL columns with no default value in existing tables.
  * @param $keys_new
  * @param $keys_new
- *   Optional keys and indexes specification to be created on the table along
+ *   (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
  *   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
  *   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
  *   MUST specify at least one key or index including it in this array. See
@@ -3009,7 +3068,7 @@ function db_drop_index($table, $name) {
  * @param $spec
  * @param $spec
  *   The field specification for the new field.
  *   The field specification for the new field.
  * @param $keys_new
  * @param $keys_new
- *   Optional keys and indexes specification to be created on the table along
+ *   (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
  *   with changing the field. The format is the same as a table specification
  *   but without the 'fields' element.
  *   but without the 'fields' element.
  */
  */

+ 59 - 6
includes/database/mysql/database.inc

@@ -28,6 +28,12 @@ class DatabaseConnection_mysql extends DatabaseConnection {
 
 
     $this->connectionOptions = $connection_options;
     $this->connectionOptions = $connection_options;
 
 
+    $charset = 'utf8';
+    // Check if the charset is overridden to utf8mb4 in settings.php.
+    if ($this->utf8mb4IsActive()) {
+      $charset = 'utf8mb4';
+    }
+
     // The DSN should use either a socket or a host/port.
     // The DSN should use either a socket or a host/port.
     if (isset($connection_options['unix_socket'])) {
     if (isset($connection_options['unix_socket'])) {
       $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
       $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
@@ -36,6 +42,10 @@ class DatabaseConnection_mysql extends DatabaseConnection {
       // Default to TCP connection on port 3306.
       // Default to TCP connection on port 3306.
       $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
       $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
     }
     }
+    // Character set is added to dsn to ensure PDO uses the proper character
+    // set when escaping. This has security implications. See
+    // https://www.drupal.org/node/1201452 for further discussion.
+    $dsn .= ';charset=' . $charset;
     $dsn .= ';dbname=' . $connection_options['database'];
     $dsn .= ';dbname=' . $connection_options['database'];
     // Allow PDO options to be overridden.
     // Allow PDO options to be overridden.
     $connection_options += array(
     $connection_options += array(
@@ -47,6 +57,11 @@ class DatabaseConnection_mysql extends DatabaseConnection {
       // Because MySQL's prepared statements skip the query cache, because it's dumb.
       // Because MySQL's prepared statements skip the query cache, because it's dumb.
       PDO::ATTR_EMULATE_PREPARES => TRUE,
       PDO::ATTR_EMULATE_PREPARES => TRUE,
     );
     );
+    if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
+      // An added connection option in PHP 5.5.21+ to optionally limit SQL to a
+      // single statement like mysqli.
+      $connection_options['pdo'] += array(PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE);
+    }
 
 
     parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
     parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
 
 
@@ -54,10 +69,10 @@ class DatabaseConnection_mysql extends DatabaseConnection {
     // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
     // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
     // for UTF-8.
     // for UTF-8.
     if (!empty($connection_options['collation'])) {
     if (!empty($connection_options['collation'])) {
-      $this->exec('SET NAMES utf8 COLLATE ' . $connection_options['collation']);
+      $this->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
     }
     }
     else {
     else {
-      $this->exec('SET NAMES utf8');
+      $this->exec('SET NAMES ' . $charset);
     }
     }
 
 
     // Set MySQL init_commands if not already defined.  Default Drupal's MySQL
     // Set MySQL init_commands if not already defined.  Default Drupal's MySQL
@@ -72,10 +87,12 @@ class DatabaseConnection_mysql extends DatabaseConnection {
       'init_commands' => array(),
       'init_commands' => array(),
     );
     );
     $connection_options['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'",
+      'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,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']));
+    // Execute initial commands.
+    foreach ($connection_options['init_commands'] as $sql) {
+      $this->exec($sql);
+    }
   }
   }
 
 
   public function __destruct() {
   public function __destruct() {
@@ -90,7 +107,7 @@ class DatabaseConnection_mysql extends DatabaseConnection {
 
 
   public function queryTemporary($query, array $args = array(), array $options = array()) {
   public function queryTemporary($query, array $args = array(), array $options = array()) {
     $tablename = $this->generateTemporaryTableName();
     $tablename = $this->generateTemporaryTableName();
-    $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY SELECT', $query), $args, $options);
+    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
     return $tablename;
     return $tablename;
   }
   }
 
 
@@ -195,6 +212,42 @@ class DatabaseConnection_mysql extends DatabaseConnection {
       }
       }
     }
     }
   }
   }
+
+  public function utf8mb4IsConfigurable() {
+    return TRUE;
+  }
+
+  public function utf8mb4IsActive() {
+    return isset($this->connectionOptions['charset']) && $this->connectionOptions['charset'] === 'utf8mb4';
+  }
+
+  public function utf8mb4IsSupported() {
+    // Ensure that the MySQL driver supports utf8mb4 encoding.
+    $version = $this->getAttribute(PDO::ATTR_CLIENT_VERSION);
+    if (strpos($version, 'mysqlnd') !== FALSE) {
+      // The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
+      $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
+      if (version_compare($version, '5.0.9', '<')) {
+        return FALSE;
+      }
+    }
+    else {
+      // The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
+      if (version_compare($version, '5.5.3', '<')) {
+        return FALSE;
+      }
+    }
+
+    // Ensure that the MySQL server supports large prefixes and utf8mb4.
+    try {
+      $this->query("CREATE TABLE {drupal_utf8mb4_test} (id VARCHAR(255), PRIMARY KEY(id(255))) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ROW_FORMAT=DYNAMIC ENGINE=INNODB");
+    }
+    catch (Exception $e) {
+      return FALSE;
+    }
+    $this->query("DROP TABLE {drupal_utf8mb4_test}");
+    return TRUE;
+  }
 }
 }
 
 
 
 

+ 3 - 16
includes/database/mysql/query.inc

@@ -51,7 +51,8 @@ class InsertQuery_mysql extends InsertQuery {
     // If we're selecting from a SelectQuery, finish building the query and
     // If we're selecting from a SelectQuery, finish building the query and
     // pass it back, as any remaining options are irrelevant.
     // pass it back, as any remaining options are irrelevant.
     if (!empty($this->fromQuery)) {
     if (!empty($this->fromQuery)) {
-      return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
+      return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
     }
     }
 
 
     $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
     $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
@@ -86,21 +87,7 @@ class InsertQuery_mysql extends InsertQuery {
   }
   }
 }
 }
 
 
-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();
-    }
-  }
-}
+class TruncateQuery_mysql extends TruncateQuery { }
 
 
 /**
 /**
  * @} End of "addtogroup database".
  * @} End of "addtogroup database".

+ 25 - 17
includes/database/mysql/schema.inc

@@ -39,8 +39,8 @@ class DatabaseSchema_mysql extends DatabaseSchema {
       $info['table'] = substr($table, ++$pos);
       $info['table'] = substr($table, ++$pos);
     }
     }
     else {
     else {
-      $db_info = Database::getConnectionInfo();
-      $info['database'] = $db_info['default']['database'];
+      $db_info = $this->connection->getConnectionOptions();
+      $info['database'] = $db_info['database'];
       $info['table'] = $table;
       $info['table'] = $table;
     }
     }
     return $info;
     return $info;
@@ -81,7 +81,8 @@ class DatabaseSchema_mysql extends DatabaseSchema {
     // Provide defaults if needed.
     // Provide defaults if needed.
     $table += array(
     $table += array(
       'mysql_engine' => 'InnoDB',
       'mysql_engine' => 'InnoDB',
-      'mysql_character_set' => 'utf8',
+      // Allow the default charset to be overridden in settings.php.
+      'mysql_character_set' => $this->connection->utf8mb4IsActive() ? 'utf8mb4' : 'utf8',
     );
     );
 
 
     $sql = "CREATE TABLE {" . $name . "} (\n";
     $sql = "CREATE TABLE {" . $name . "} (\n";
@@ -109,6 +110,13 @@ class DatabaseSchema_mysql extends DatabaseSchema {
       $sql .= ' COLLATE ' . $info['collation'];
       $sql .= ' COLLATE ' . $info['collation'];
     }
     }
 
 
+    // The row format needs to be either DYNAMIC or COMPRESSED in order to allow
+    // for the innodb_large_prefix setting to take effect, see
+    // https://dev.mysql.com/doc/refman/5.6/en/create-table.html
+    if ($this->connection->utf8mb4IsActive()) {
+      $sql .= ' ROW_FORMAT=DYNAMIC';
+    }
+
     // Add table comment.
     // Add table comment.
     if (!empty($table['description'])) {
     if (!empty($table['description'])) {
       $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
       $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
@@ -301,10 +309,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function renameTable($table, $new_name) {
   public function renameTable($table, $new_name) {
     if (!$this->tableExists($table)) {
     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)));
+      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)) {
     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)));
+      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);
     $info = $this->getPrefixInfo($new_name);
@@ -322,10 +330,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function addField($table, $field, $spec, $keys_new = array()) {
   public function addField($table, $field, $spec, $keys_new = array()) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
     }
     }
     if ($this->fieldExists($table, $field)) {
     if ($this->fieldExists($table, $field)) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
     }
     }
 
 
     $fixnull = FALSE;
     $fixnull = FALSE;
@@ -361,7 +369,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function fieldSetDefault($table, $field, $default) {
   public function fieldSetDefault($table, $field, $default) {
     if (!$this->fieldExists($table, $field)) {
     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)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
     }
     }
 
 
     if (!isset($default)) {
     if (!isset($default)) {
@@ -376,7 +384,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function fieldSetNoDefault($table, $field) {
   public function fieldSetNoDefault($table, $field) {
     if (!$this->fieldExists($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)));
+      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');
     $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT');
@@ -391,10 +399,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function addPrimaryKey($table, $fields) {
   public function addPrimaryKey($table, $fields) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
     }
     }
     if ($this->indexExists($table, 'PRIMARY')) {
     if ($this->indexExists($table, 'PRIMARY')) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+      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) . ')');
     $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
@@ -411,10 +419,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function addUniqueKey($table, $name, $fields) {
   public function addUniqueKey($table, $name, $fields) {
     if (!$this->tableExists($table)) {
     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)));
+      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)) {
     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)));
+      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) . ')');
     $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
@@ -431,10 +439,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function addIndex($table, $name, $fields) {
   public function addIndex($table, $name, $fields) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+      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)) {
     if ($this->indexExists($table, $name)) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $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) . ')');
     $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
@@ -451,10 +459,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
 
 
   public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
   public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
     if (!$this->fieldExists($table, $field)) {
     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)));
+      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)) {
     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)));
+      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));
     $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));

+ 15 - 7
includes/database/pgsql/database.inc

@@ -11,7 +11,7 @@
  */
  */
 
 
 /**
 /**
- * The name by which to obtain a lock for retrive the next insert id.
+ * The name by which to obtain a lock for retrieving the next insert id.
  */
  */
 define('POSTGRESQL_NEXTID_LOCK', 1000);
 define('POSTGRESQL_NEXTID_LOCK', 1000);
 
 
@@ -55,7 +55,7 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
     $connection_options['pdo'] += array(
     $connection_options['pdo'] += array(
       // Prepared statements are most effective for performance when queries
       // Prepared statements are most effective for performance when queries
       // are recycled (used several times). However, if they are not re-used,
       // are recycled (used several times). However, if they are not re-used,
-      // prepared statements become ineffecient. Since most of Drupal's
+      // prepared statements become inefficient. Since most of Drupal's
       // prepared queries are not re-used, it should be faster to emulate
       // prepared queries are not re-used, it should be faster to emulate
       // the preparation than to actually ready statements for re-use. If in
       // the preparation than to actually ready statements for re-use. If in
       // doubt, reset to FALSE and measure performance.
       // doubt, reset to FALSE and measure performance.
@@ -146,7 +146,7 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
 
 
   public function queryTemporary($query, array $args = array(), array $options = array()) {
   public function queryTemporary($query, array $args = array(), array $options = array()) {
     $tablename = $this->generateTemporaryTableName();
     $tablename = $this->generateTemporaryTableName();
-    $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} AS SELECT', $query), $args, $options);
+    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
     return $tablename;
     return $tablename;
   }
   }
 
 
@@ -175,14 +175,14 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
   }
   }
 
 
   /**
   /**
-   * Retrive a the next id in a sequence.
+   * Retrieve the next id in a sequence.
    *
    *
    * PostgreSQL has built in sequences. We'll use these instead of inserting
    * PostgreSQL has built in sequences. We'll use these instead of inserting
    * and updating a sequences table.
    * and updating a sequences table.
    */
    */
   public function nextId($existing = 0) {
   public function nextId($existing = 0) {
 
 
-    // Retrive the name of the sequence. This information cannot be cached
+    // Retrieve the name of the sequence. This information cannot be cached
     // because the prefix may change, for example, like it does in simpletests.
     // because the prefix may change, for example, like it does in simpletests.
     $sequence_name = $this->makeSequenceName('sequences', 'value');
     $sequence_name = $this->makeSequenceName('sequences', 'value');
 
 
@@ -194,7 +194,7 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
     }
     }
 
 
     // PostgreSQL advisory locks are simply locks to be used by an
     // PostgreSQL advisory locks are simply locks to be used by an
-    // application such as Drupal. This will prevent other Drupal proccesses
+    // application such as Drupal. This will prevent other Drupal processes
     // from altering the sequence while we are.
     // from altering the sequence while we are.
     $this->query("SELECT pg_advisory_lock(" . POSTGRESQL_NEXTID_LOCK . ")");
     $this->query("SELECT pg_advisory_lock(" . POSTGRESQL_NEXTID_LOCK . ")");
 
 
@@ -209,13 +209,21 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
     // Reset the sequence to a higher value than the existing id.
     // Reset the sequence to a higher value than the existing id.
     $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
     $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.
+    // Retrieve the next id. We know this will be as high as we want it.
     $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
     $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
 
 
     $this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")");
     $this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")");
 
 
     return $id;
     return $id;
   }
   }
+
+  public function utf8mb4IsActive() {
+    return TRUE;
+  }
+
+  public function utf8mb4IsSupported() {
+    return TRUE;
+  }
 }
 }
 
 
 /**
 /**

+ 1 - 1
includes/database/pgsql/install.inc

@@ -165,7 +165,7 @@ class DatabaseTasks_pgsql extends DatabaseTasks {
         LANGUAGE \'sql\''
         LANGUAGE \'sql\''
       );
       );
 
 
-      // Using || to concatenate in Drupal is not recommeneded because there are
+      // Using || to concatenate in Drupal is not recommended because there are
       // database drivers for Drupal that do not support the syntax, however
       // database drivers for Drupal that do not support the syntax, however
       // they do support CONCAT(item1, item2) which we can replicate in
       // they do support CONCAT(item1, item2) which we can replicate in
       // PostgreSQL. PostgreSQL requires the function to be defined for each
       // PostgreSQL. PostgreSQL requires the function to be defined for each

+ 2 - 1
includes/database/pgsql/query.inc

@@ -112,7 +112,8 @@ class InsertQuery_pgsql extends InsertQuery {
     // If we're selecting from a SelectQuery, finish building the query and
     // If we're selecting from a SelectQuery, finish building the query and
     // pass it back, as any remaining options are irrelevant.
     // pass it back, as any remaining options are irrelevant.
     if (!empty($this->fromQuery)) {
     if (!empty($this->fromQuery)) {
-      return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
+      return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
     }
     }
 
 
     $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
     $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';

+ 14 - 14
includes/database/pgsql/schema.inc

@@ -314,10 +314,10 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   function renameTable($table, $new_name) {
   function renameTable($table, $new_name) {
     if (!$this->tableExists($table)) {
     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)));
+      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)) {
     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)));
+      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.
     // Get the schema and tablename for the old table.
@@ -351,10 +351,10 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   public function addField($table, $field, $spec, $new_keys = array()) {
   public function addField($table, $field, $spec, $new_keys = array()) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
     }
     }
     if ($this->fieldExists($table, $field)) {
     if ($this->fieldExists($table, $field)) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
     }
     }
 
 
     $fixnull = FALSE;
     $fixnull = FALSE;
@@ -393,7 +393,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   public function fieldSetDefault($table, $field, $default) {
   public function fieldSetDefault($table, $field, $default) {
     if (!$this->fieldExists($table, $field)) {
     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)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
     }
     }
 
 
     if (!isset($default)) {
     if (!isset($default)) {
@@ -408,7 +408,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   public function fieldSetNoDefault($table, $field) {
   public function fieldSetNoDefault($table, $field) {
     if (!$this->fieldExists($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)));
+      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');
     $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
@@ -435,10 +435,10 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   public function addPrimaryKey($table, $fields) {
   public function addPrimaryKey($table, $fields) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
     }
     }
     if ($this->constraintExists($table, 'pkey')) {
     if ($this->constraintExists($table, 'pkey')) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+      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) . ')');
     $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . implode(',', $fields) . ')');
@@ -455,10 +455,10 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   function addUniqueKey($table, $name, $fields) {
   function addUniqueKey($table, $name, $fields) {
     if (!$this->tableExists($table)) {
     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)));
+      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')) {
     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)));
+      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) . ')');
     $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
@@ -475,10 +475,10 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   public function addIndex($table, $name, $fields) {
   public function addIndex($table, $name, $fields) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+      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)) {
     if ($this->indexExists($table, $name)) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $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));
     $this->connection->query($this->_createIndexSql($table, $name, $fields));
@@ -495,10 +495,10 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
 
 
   public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
   public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
     if (!$this->fieldExists($table, $field)) {
     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)));
+      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)) {
     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)));
+      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);
     $spec = $this->processField($spec);

+ 2 - 2
includes/database/pgsql/select.inc

@@ -80,7 +80,7 @@ class SelectQuery_pgsql extends SelectQuery {
     }
     }
 
 
     // If a table loads all fields, it can not be added again. It would
     // 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
+    // result in an ambiguous alias error because that field would be loaded
     // twice: Once through table_alias.* and once directly. If the field
     // twice: Once through table_alias.* and once directly. If the field
     // actually belongs to a different table, it must be added manually.
     // actually belongs to a different table, it must be added manually.
     foreach ($this->tables as $table) {
     foreach ($this->tables as $table) {
@@ -90,7 +90,7 @@ class SelectQuery_pgsql extends SelectQuery {
     }
     }
 
 
     // If $field contains an characters which are not allowed in a field name
     // If $field contains an characters which are not allowed in a field name
-    // it is considered an expression, these can't be handeld automatically
+    // it is considered an expression, these can't be handled automatically
     // either.
     // either.
     if ($this->connection->escapeField($field) != $field) {
     if ($this->connection->escapeField($field) != $field) {
       return $return;
       return $return;

+ 63 - 64
includes/database/query.inc

@@ -83,7 +83,7 @@ interface QueryConditionInterface {
 
 
   /**
   /**
    * Sets a condition that the specified subquery returns values.
    * Sets a condition that the specified subquery returns values.
-   * 
+   *
    * @param SelectQueryInterface $select
    * @param SelectQueryInterface $select
    *   The subquery that must contain results.
    *   The subquery that must contain results.
    *
    *
@@ -91,10 +91,10 @@ interface QueryConditionInterface {
    *   The called object.
    *   The called object.
    */
    */
   public function exists(SelectQueryInterface $select);
   public function exists(SelectQueryInterface $select);
-  
+
   /**
   /**
    * Sets a condition that the specified subquery returns no values.
    * Sets a condition that the specified subquery returns no values.
-   * 
+   *
    * @param SelectQueryInterface $select
    * @param SelectQueryInterface $select
    *   The subquery that must not contain results.
    *   The subquery that must not contain results.
    *
    *
@@ -102,7 +102,7 @@ interface QueryConditionInterface {
    *   The called object.
    *   The called object.
    */
    */
   public function notExists(SelectQueryInterface $select);
   public function notExists(SelectQueryInterface $select);
-  
+
   /**
   /**
    * Gets a complete list of all conditions in this conditional clause.
    * Gets a complete list of all conditions in this conditional clause.
    *
    *
@@ -283,14 +283,14 @@ abstract class Query implements QueryPlaceholderInterface {
 
 
   /**
   /**
    * The target of the connection object.
    * The target of the connection object.
-   * 
+   *
    * @var string
    * @var string
    */
    */
   protected $connectionTarget;
   protected $connectionTarget;
 
 
   /**
   /**
    * The key of the connection object.
    * The key of the connection object.
-   * 
+   *
    * @var string
    * @var string
    */
    */
   protected $connectionKey;
   protected $connectionKey;
@@ -710,10 +710,11 @@ class InsertQuery extends Query {
       // first call to fields() does have an effect.
       // first call to fields() does have an effect.
       $this->fields(array_merge(array_keys($this->fromQuery->getFields()), array_keys($this->fromQuery->getExpressions())));
       $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.');
+    else {
+      // 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 no values have been added, silently ignore this query. This can happen
@@ -804,7 +805,7 @@ class DeleteQuery extends Query implements QueryConditionInterface {
     $this->condition->notExists($select);
     $this->condition->notExists($select);
     return $this;
     return $this;
   }
   }
-  
+
   /**
   /**
    * Implements QueryConditionInterface::conditions().
    * Implements QueryConditionInterface::conditions().
    */
    */
@@ -844,8 +845,8 @@ class DeleteQuery extends Query implements QueryConditionInterface {
   /**
   /**
    * Executes the DELETE query.
    * Executes the DELETE query.
    *
    *
-   * @return
-   *   The return value is dependent on the database connection.
+   * @return int
+   *   The number of rows affected by the delete query.
    */
    */
   public function execute() {
   public function execute() {
     $values = array();
     $values = array();
@@ -942,7 +943,17 @@ class TruncateQuery extends Query {
     // Create a sanitized comment string to prepend to the query.
     // Create a sanitized comment string to prepend to the query.
     $comments = $this->connection->makeComment($this->comments);
     $comments = $this->connection->makeComment($this->comments);
 
 
-    return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} ';
+    // In most cases, TRUNCATE is not a transaction safe statement as it is a
+    // DDL statement which results in an implicit COMMIT. When we are in a
+    // transaction, fallback to the slower, but transactional, DELETE.
+    // PostgreSQL also locks the entire table for a TRUNCATE strongly reducing
+    // the concurrency with other transactions.
+    if ($this->connection->inTransaction()) {
+      return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}';
+    }
+    else {
+      return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} ';
+    }
   }
   }
 }
 }
 
 
@@ -1053,7 +1064,7 @@ class UpdateQuery extends Query implements QueryConditionInterface {
     $this->condition->notExists($select);
     $this->condition->notExists($select);
     return $this;
     return $this;
   }
   }
-  
+
   /**
   /**
    * Implements QueryConditionInterface::conditions().
    * Implements QueryConditionInterface::conditions().
    */
    */
@@ -1231,7 +1242,7 @@ class UpdateQuery extends Query implements QueryConditionInterface {
  * MergeQuery::updateFields() and MergeQuery::insertFields() needs to be called
  * MergeQuery::updateFields() and MergeQuery::insertFields() needs to be called
  * instead. MergeQuery::fields() can also be called which calls both of these
  * 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
  * 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
+ * INSERT and UPDATE. However, this is not mandatory. Another convenient
  * wrapper is MergeQuery::key() which adds the same column-value pairs to the
  * wrapper is MergeQuery::key() which adds the same column-value pairs to the
  * condition and the INSERT query part.
  * condition and the INSERT query part.
  *
  *
@@ -1545,7 +1556,7 @@ class MergeQuery extends Query implements QueryConditionInterface {
     $this->condition->notExists($select);
     $this->condition->notExists($select);
     return $this;
     return $this;
   }
   }
-  
+
   /**
   /**
    * Implements QueryConditionInterface::conditions().
    * Implements QueryConditionInterface::conditions().
    */
    */
@@ -1595,55 +1606,43 @@ class MergeQuery extends Query implements QueryConditionInterface {
   }
   }
 
 
   public function execute() {
   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 (!count($this->condition)) {
+      throw new InvalidMergeQueryException(t('Invalid merge query: no conditions'));
+    }
+    $select = $this->connection->select($this->conditionTable)
+      ->condition($this->condition);
+    $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 self::STATUS_INSERT;
       }
       }
-      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']);
-          }
+      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;
         }
         }
-        $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.
+    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 self::STATUS_UPDATE;
+     }
   }
   }
 }
 }
 
 
@@ -1695,7 +1694,7 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
    * Implements Countable::count().
    * Implements Countable::count().
    *
    *
    * Returns the size of this conditional. The size of the conditional is the
    * 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
+   * size of its conditional array minus one, because one element is the
    * conjunction.
    * conjunction.
    */
    */
   public function count() {
   public function count() {
@@ -1762,14 +1761,14 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
   public function exists(SelectQueryInterface $select) {
   public function exists(SelectQueryInterface $select) {
     return $this->condition('', $select, 'EXISTS');
     return $this->condition('', $select, 'EXISTS');
   }
   }
-  
+
   /**
   /**
    * Implements QueryConditionInterface::notExists().
    * Implements QueryConditionInterface::notExists().
    */
    */
   public function notExists(SelectQueryInterface $select) {
   public function notExists(SelectQueryInterface $select) {
     return $this->condition('', $select, 'NOT EXISTS');
     return $this->condition('', $select, 'NOT EXISTS');
   }
   }
-  
+
   /**
   /**
    * Implements QueryConditionInterface::conditions().
    * Implements QueryConditionInterface::conditions().
    */
    */

+ 11 - 5
includes/database/schema.inc

@@ -92,7 +92,8 @@ require_once dirname(__FILE__) . '/query.inc';
  *    specification). Each specification is an array containing the name of
  *    specification). Each specification is an array containing the name of
  *    the referenced table ('table'), and an array of column mappings
  *    the referenced table ('table'), and an array of column mappings
  *    ('columns'). Column mappings are defined by key pairs ('source_column' =>
  *    ('columns'). Column mappings are defined by key pairs ('source_column' =>
- *    'referenced_column').
+ *    'referenced_column'). This key is for documentation purposes only; foreign
+ *    keys are not created in the database, nor are they enforced by Drupal.
  *  - 'indexes':  An associative array of indexes ('indexname' =>
  *  - 'indexes':  An associative array of indexes ('indexname' =>
  *    specification). Each specification is an array of one or more
  *    specification). Each specification is an array of one or more
  *    key column specifiers (see below) that form an index on the
  *    key column specifiers (see below) that form an index on the
@@ -144,6 +145,8 @@ require_once dirname(__FILE__) . '/query.inc';
  *   'unique keys' => array(
  *   'unique keys' => array(
  *     'vid' => array('vid'),
  *     'vid' => array('vid'),
  *   ),
  *   ),
+ *   // For documentation purposes only; foreign keys are not created in the
+ *   // database.
  *   'foreign keys' => array(
  *   'foreign keys' => array(
  *     'node_revision' => array(
  *     'node_revision' => array(
  *       'table' => 'node_revision',
  *       'table' => 'node_revision',
@@ -161,6 +164,9 @@ require_once dirname(__FILE__) . '/query.inc';
  * @see drupal_install_schema()
  * @see drupal_install_schema()
  */
  */
 
 
+/**
+ * Base class for database schema definitions.
+ */
 abstract class DatabaseSchema implements QueryPlaceholderInterface {
 abstract class DatabaseSchema implements QueryPlaceholderInterface {
 
 
   protected $connection;
   protected $connection;
@@ -288,7 +294,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
   protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
   protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
     $info = $this->connection->getConnectionOptions();
     $info = $this->connection->getConnectionOptions();
 
 
-    // Retrive the table name and schema
+    // Retrieve the table name and schema
     $table_info = $this->getPrefixInfo($table_name, $add_prefix);
     $table_info = $this->getPrefixInfo($table_name, $add_prefix);
 
 
     $condition = new DatabaseCondition('AND');
     $condition = new DatabaseCondition('AND');
@@ -416,7 +422,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   This is most useful for creating NOT NULL columns with no default
    *   This is most useful for creating NOT NULL columns with no default
    *   value in existing tables.
    *   value in existing tables.
    * @param $keys_new
    * @param $keys_new
-   *   Optional keys and indexes specification to be created on the
+   *   (optional) Keys and indexes specification to be created on the
    *   table along with adding the field. The format is the same as a
    *   table along with adding the field. The format is the same as a
    *   table specification but without the 'fields' element. If you are
    *   table specification but without the 'fields' element. If you are
    *   adding a type 'serial' field, you MUST specify at least one key
    *   adding a type 'serial' field, you MUST specify at least one key
@@ -630,7 +636,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    * @param $spec
    * @param $spec
    *   The field specification for the new field.
    *   The field specification for the new field.
    * @param $keys_new
    * @param $keys_new
-   *   Optional keys and indexes specification to be created on the
+   *   (optional) Keys and indexes specification to be created on the
    *   table along with changing the field. The format is the same as a
    *   table along with changing the field. The format is the same as a
    *   table specification but without the 'fields' element.
    *   table specification but without the 'fields' element.
    *
    *
@@ -654,7 +660,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    */
    */
   public function createTable($name, $table) {
   public function createTable($name, $table) {
     if ($this->tableExists($name)) {
     if ($this->tableExists($name)) {
-      throw new DatabaseSchemaObjectExistsException(t('Table %name already exists.', array('%name' => $name)));
+      throw new DatabaseSchemaObjectExistsException(t('Table @name already exists.', array('@name' => $name)));
     }
     }
     $statements = $this->createTableSql($name, $table);
     $statements = $this->createTableSql($name, $table);
     foreach ($statements as $statement) {
     foreach ($statements as $statement) {

+ 20 - 2
includes/database/select.inc

@@ -377,7 +377,8 @@ interface SelectQueryInterface extends QueryConditionInterface, QueryAlterableIn
    * @param $field
    * @param $field
    *   The field on which to order.
    *   The field on which to order.
    * @param $direction
    * @param $direction
-   *   The direction to sort. Legal values are "ASC" and "DESC".
+   *   The direction to sort. Legal values are "ASC" and "DESC". Any other value
+   *   will be converted to "ASC".
    * @return SelectQueryInterface
    * @return SelectQueryInterface
    *   The called object.
    *   The called object.
    */
    */
@@ -596,7 +597,7 @@ class SelectQueryExtender implements SelectQueryInterface {
 
 
   public function hasAnyTag() {
   public function hasAnyTag() {
     $args = func_get_args();
     $args = func_get_args();
-    return call_user_func_array(array($this->query, 'hasAnyTags'), $args);
+    return call_user_func_array(array($this->query, 'hasAnyTag'), $args);
   }
   }
 
 
   public function addMetaData($key, $object) {
   public function addMetaData($key, $object) {
@@ -1230,6 +1231,21 @@ class SelectQuery extends Query implements SelectQueryInterface {
 
 
     // Modules may alter all queries or only those having a particular tag.
     // Modules may alter all queries or only those having a particular tag.
     if (isset($this->alterTags)) {
     if (isset($this->alterTags)) {
+      // Many contrib modules assume that query tags used for access-checking
+      // purposes follow the pattern $entity_type . '_access'. But this is
+      // not the case for taxonomy terms, since core used to add term_access
+      // instead of taxonomy_term_access to its queries. Provide backwards
+      // compatibility by adding both tags here instead of attempting to fix
+      // all contrib modules in a coordinated effort.
+      // TODO:
+      // - Extract this mechanism into a hook as part of a public (non-security)
+      //   issue.
+      // - Emit E_USER_DEPRECATED if term_access is used.
+      //   https://www.drupal.org/node/2575081
+      $term_access_tags = array('term_access' => 1, 'taxonomy_term_access' => 1);
+      if (array_intersect_key($this->alterTags, $term_access_tags)) {
+        $this->alterTags += $term_access_tags;
+      }
       $hooks = array('query');
       $hooks = array('query');
       foreach ($this->alterTags as $tag => $value) {
       foreach ($this->alterTags as $tag => $value) {
         $hooks[] = 'query_' . $tag;
         $hooks[] = 'query_' . $tag;
@@ -1384,6 +1400,8 @@ class SelectQuery extends Query implements SelectQueryInterface {
   }
   }
 
 
   public function orderBy($field, $direction = 'ASC') {
   public function orderBy($field, $direction = 'ASC') {
+    // Only allow ASC and DESC, default to ASC.
+    $direction = strtoupper($direction) == 'DESC' ? 'DESC' : 'ASC';
     $this->order[$field] = $direction;
     $this->order[$field] = $direction;
     return $this;
     return $this;
   }
   }

+ 9 - 1
includes/database/sqlite/database.inc

@@ -250,7 +250,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
     $prefixes[$tablename] = '';
     $prefixes[$tablename] = '';
     $this->setPrefix($prefixes);
     $this->setPrefix($prefixes);
 
 
-    $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options);
+    $this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
     return $tablename;
     return $tablename;
   }
   }
 
 
@@ -378,6 +378,14 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
     }
     }
   }
   }
 
 
+  public function utf8mb4IsActive() {
+    return TRUE;
+  }
+
+  public function utf8mb4IsSupported() {
+    return TRUE;
+  }
+
 }
 }
 
 
 /**
 /**

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

@@ -14,8 +14,6 @@ class DatabaseTasks_sqlite extends DatabaseTasks {
 
 
   /**
   /**
    * Minimum engine version.
    * Minimum engine version.
-   *
-   * @todo: consider upping to 3.6.8 in Drupal 8 to get SAVEPOINT support.
    */
    */
   public function minimumVersion() {
   public function minimumVersion() {
     return '3.3.7';
     return '3.3.7';

+ 8 - 8
includes/database/sqlite/query.inc

@@ -41,7 +41,8 @@ class InsertQuery_sqlite extends InsertQuery {
     // If we're selecting from a SelectQuery, finish building the query and
     // If we're selecting from a SelectQuery, finish building the query and
     // pass it back, as any remaining options are irrelevant.
     // pass it back, as any remaining options are irrelevant.
     if (!empty($this->fromQuery)) {
     if (!empty($this->fromQuery)) {
-      return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') ' . $this->fromQuery;
+      $insert_fields_string = $this->insertFields ? ' (' . implode(', ', $this->insertFields) . ') ' : ' ';
+      return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
     }
     }
 
 
     return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')';
     return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')';
@@ -98,16 +99,15 @@ class UpdateQuery_sqlite extends UpdateQuery {
 
 
 /**
 /**
  * SQLite specific implementation of DeleteQuery.
  * 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 {
 class DeleteQuery_sqlite extends DeleteQuery {
   public function execute() {
   public function execute() {
+    // 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. But we want to return
+    // the number of rows affected, so we calculate it directly.
     if (!count($this->condition)) {
     if (!count($this->condition)) {
       $total_rows = $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '}')->fetchField();
       $total_rows = $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '}')->fetchField();
       parent::execute();
       parent::execute();

+ 15 - 15
includes/database/sqlite/schema.inc

@@ -232,10 +232,10 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function renameTable($table, $new_name) {
   public function renameTable($table, $new_name) {
     if (!$this->tableExists($table)) {
     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)));
+      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)) {
     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)));
+      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);
     $schema = $this->introspectSchema($table);
@@ -244,7 +244,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
     // database. So the syntax '...RENAME TO database.table' would fail.
     // database. So the syntax '...RENAME TO database.table' would fail.
     // So we must determine the full table name here rather than surrounding
     // So we must determine the full table name here rather than surrounding
     // the table with curly braces incase the db_prefix contains a reference
     // the table with curly braces incase the db_prefix contains a reference
-    // to a database outside of our existsing database.
+    // to a database outside of our existing database.
     $info = $this->getPrefixInfo($new_name);
     $info = $this->getPrefixInfo($new_name);
     $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
     $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
 
 
@@ -278,10 +278,10 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function addField($table, $field, $specification, $keys_new = array()) {
   public function addField($table, $field, $specification, $keys_new = array()) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
     }
     }
     if ($this->fieldExists($table, $field)) {
     if ($this->fieldExists($table, $field)) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+      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
     // SQLite doesn't have a full-featured ALTER TABLE statement. It only
@@ -494,10 +494,10 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
   public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
     if (!$this->fieldExists($table, $field)) {
     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)));
+      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)) {
     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)));
+      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);
     $old_schema = $this->introspectSchema($table);
@@ -559,10 +559,10 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function addIndex($table, $name, $fields) {
   public function addIndex($table, $name, $fields) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+      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)) {
     if ($this->indexExists($table, $name)) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
     }
     }
 
 
     $schema['indexes'][$name] = $fields;
     $schema['indexes'][$name] = $fields;
@@ -591,10 +591,10 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function addUniqueKey($table, $name, $fields) {
   public function addUniqueKey($table, $name, $fields) {
     if (!$this->tableExists($table)) {
     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)));
+      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)) {
     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)));
+      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;
     $schema['unique keys'][$name] = $fields;
@@ -617,14 +617,14 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function addPrimaryKey($table, $fields) {
   public function addPrimaryKey($table, $fields) {
     if (!$this->tableExists($table)) {
     if (!$this->tableExists($table)) {
-      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
+      throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
     }
     }
 
 
     $old_schema = $this->introspectSchema($table);
     $old_schema = $this->introspectSchema($table);
     $new_schema = $old_schema;
     $new_schema = $old_schema;
 
 
     if (!empty($new_schema['primary key'])) {
     if (!empty($new_schema['primary key'])) {
-      throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+      throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
     }
     }
 
 
     $new_schema['primary key'] = $fields;
     $new_schema['primary key'] = $fields;
@@ -646,7 +646,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function fieldSetDefault($table, $field, $default) {
   public function fieldSetDefault($table, $field, $default) {
     if (!$this->fieldExists($table, $field)) {
     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)));
+      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);
     $old_schema = $this->introspectSchema($table);
@@ -658,7 +658,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
 
   public function fieldSetNoDefault($table, $field) {
   public function fieldSetNoDefault($table, $field) {
     if (!$this->fieldExists($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)));
+      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);
     $old_schema = $this->introspectSchema($table);

+ 10 - 10
includes/date.inc

@@ -12,11 +12,6 @@ function system_default_date_formats() {
   $formats = array();
   $formats = array();
 
 
   // Short date formats.
   // Short date formats.
-  $formats[] = array(
-    'type' => 'short',
-    'format' => 'Y-m-d H:i',
-    'locales' => array(),
-  );
   $formats[] = array(
   $formats[] = array(
     'type' => 'short',
     'type' => 'short',
     'format' => 'm/d/Y - H:i',
     'format' => 'm/d/Y - H:i',
@@ -37,6 +32,11 @@ function system_default_date_formats() {
     'format' => 'd.m.Y - H:i',
     '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'),
     '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' => 'Y-m-d H:i',
+    'locales' => array(),
+  );
   $formats[] = array(
   $formats[] = array(
     'type' => 'short',
     'type' => 'short',
     'format' => 'm/d/Y - g:ia',
     'format' => 'm/d/Y - g:ia',
@@ -84,11 +84,6 @@ function system_default_date_formats() {
   );
   );
 
 
   // Medium date formats.
   // Medium date formats.
-  $formats[] = array(
-    'type' => 'medium',
-    'format' => 'D, Y-m-d H:i',
-    'locales' => array(),
-  );
   $formats[] = array(
   $formats[] = array(
     'type' => 'medium',
     'type' => 'medium',
     'format' => 'D, m/d/Y - H:i',
     'format' => 'D, m/d/Y - H:i',
@@ -104,6 +99,11 @@ function system_default_date_formats() {
     'format' => 'D, Y/m/d - H:i',
     'format' => 'D, Y/m/d - H:i',
     'locales' => array('en-ca', 'fr-ca', 'no-no', 'sv-se'),
     'locales' => array('en-ca', 'fr-ca', 'no-no', 'sv-se'),
   );
   );
+  $formats[] = array(
+    'type' => 'medium',
+    'format' => 'D, Y-m-d H:i',
+    'locales' => array(),
+  );
   $formats[] = array(
   $formats[] = array(
     'type' => 'medium',
     'type' => 'medium',
     'format' => 'F j, Y - H:i',
     'format' => 'F j, Y - H:i',

+ 96 - 25
includes/entity.inc

@@ -13,14 +13,6 @@
  */
  */
 interface DrupalEntityControllerInterface {
 interface DrupalEntityControllerInterface {
 
 
-  /**
-   * Constructor.
-   *
-   * @param $entityType
-   *   The entity type for which the instance is created.
-   */
-  public function __construct($entityType);
-
   /**
   /**
    * Resets the internal, static entity cache.
    * Resets the internal, static entity cache.
    *
    *
@@ -36,7 +28,9 @@ interface DrupalEntityControllerInterface {
    * @param $ids
    * @param $ids
    *   An array of entity IDs, or FALSE to load all entities.
    *   An array of entity IDs, or FALSE to load all entities.
    * @param $conditions
    * @param $conditions
-   *   An array of conditions in the form 'field' => $value.
+   *   An array of conditions. Keys are field names on the entity's base table.
+   *   Values will be compared for equality. All the comparisons will be ANDed
+   *   together. This parameter is deprecated; use an EntityFieldQuery instead.
    *
    *
    * @return
    * @return
    *   An array of entity objects indexed by their ids. When no results are
    *   An array of entity objects indexed by their ids. When no results are
@@ -54,7 +48,7 @@ interface DrupalEntityControllerInterface {
 class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
 class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
 
 
   /**
   /**
-   * Static cache of entities.
+   * Static cache of entities, keyed by entity ID.
    *
    *
    * @var array
    * @var array
    */
    */
@@ -119,6 +113,9 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
 
 
   /**
   /**
    * Constructor: sets basic variables.
    * Constructor: sets basic variables.
+   *
+   * @param $entityType
+   *   The entity type for which the instance is created.
    */
    */
   public function __construct($entityType) {
   public function __construct($entityType) {
     $this->entityType = $entityType;
     $this->entityType = $entityType;
@@ -186,6 +183,11 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
       }
       }
     }
     }
 
 
+    // Ensure integer entity IDs are valid.
+    if (!empty($ids)) {
+      $this->cleanIds($ids);
+    }
+
     // Load any remaining entities from the database. This is the case if $ids
     // 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
     // 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.
     // load, if loading a revision, or if $conditions was passed without $ids.
@@ -226,6 +228,35 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     return $entities;
     return $entities;
   }
   }
 
 
+  /**
+   * Ensures integer entity IDs are valid.
+   *
+   * The identifier sanitization provided by this method has been introduced
+   * as Drupal used to rely on the database to facilitate this, which worked
+   * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
+   *
+   * @param array $ids
+   *   The entity IDs to verify. Non-integer IDs are removed from this array if
+   *   the entity type requires IDs to be integers.
+   */
+  protected function cleanIds(&$ids) {
+    $entity_info = entity_get_info($this->entityType);
+    if (isset($entity_info['base table field types'])) {
+      $id_type = $entity_info['base table field types'][$this->idKey];
+      if ($id_type == 'serial' || $id_type == 'int') {
+        $ids = array_filter($ids, array($this, 'filterId'));
+        $ids = array_map('intval', $ids);
+      }
+    }
+  }
+
+  /**
+   * Callback for array_filter that removes non-integer IDs.
+   */
+  protected function filterId($id) {
+    return is_numeric($id) && $id == (int) $id;
+  }
+
   /**
   /**
    * Builds the query to load the entity.
    * Builds the query to load the entity.
    *
    *
@@ -241,7 +272,9 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
    * @param $ids
    * @param $ids
    *   An array of entity IDs, or FALSE to load all entities.
    *   An array of entity IDs, or FALSE to load all entities.
    * @param $conditions
    * @param $conditions
-   *   An array of conditions in the form 'field' => $value.
+   *   An array of conditions. Keys are field names on the entity's base table.
+   *   Values will be compared for equality. All the comparisons will be ANDed
+   *   together. This parameter is deprecated; use an EntityFieldQuery instead.
    * @param $revision_id
    * @param $revision_id
    *   The ID of the revision to load, or FALSE if this query is asking for the
    *   The ID of the revision to load, or FALSE if this query is asking for the
    *   most current revision(s).
    *   most current revision(s).
@@ -365,9 +398,23 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     // This ensures the same behavior whether loading from memory or database.
     // This ensures the same behavior whether loading from memory or database.
     if ($conditions) {
     if ($conditions) {
       foreach ($entities as $entity) {
       foreach ($entities as $entity) {
-        $entity_values = (array) $entity;
-        if (array_diff_assoc($conditions, $entity_values)) {
-          unset($entities[$entity->{$this->idKey}]);
+        // Iterate over all conditions and compare them to the entity
+        // properties. We cannot use array_diff_assoc() here since the
+        // conditions can be nested arrays, too.
+        foreach ($conditions as $property_name => $condition) {
+          if (is_array($condition)) {
+            // Multiple condition values for one property are treated as OR
+            // operation: only if the value is not at all in the condition array
+            // we remove the entity.
+            if (!in_array($entity->{$property_name}, $condition)) {
+              unset($entities[$entity->{$this->idKey}]);
+              continue 2;
+            }
+          }
+          elseif ($condition != $entity->{$property_name}) {
+            unset($entities[$entity->{$this->idKey}]);
+            continue 2;
+          }
         }
         }
       }
       }
     }
     }
@@ -399,7 +446,7 @@ class EntityFieldQueryException extends Exception {}
  *
  *
  * This class allows finding entities based on entity properties (for example,
  * This class allows finding entities based on entity properties (for example,
  * node->changed), field values, and generic entity meta data (bundle,
  * node->changed), field values, and generic entity meta data (bundle,
- * entity type, entity id, and revision ID). It is not possible to query across
+ * 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
  * 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
  * nodes written by users created in the last hour, as this would require
  * querying both node->status and user->created.
  * querying both node->status and user->created.
@@ -634,21 +681,43 @@ class EntityFieldQuery {
 
 
   /**
   /**
    * Adds a condition on field values.
    * Adds a condition on field values.
-   * 
+   *
    * Note that entities with empty field values will be excluded from the
    * Note that entities with empty field values will be excluded from the
    * EntityFieldQuery results when using this method.
    * EntityFieldQuery results when using this method.
    *
    *
    * @param $field
    * @param $field
    *   Either a field name or a field array.
    *   Either a field name or a field array.
    * @param $column
    * @param $column
-   *   The column that should hold the value to be matched.
+   *   The column that should hold the value to be matched, defined in the
+   *   hook_field_schema() of this field. If this is omitted then all of the
+   *   other parameters are ignored, except $field, and this call will just be
+   *   adding a condition that says that the field has a value, rather than
+   *   testing the value itself.
    * @param $value
    * @param $value
-   *   The value to test the column value against.
+   *   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
    * @param $operator
-   *   The operator to be used to test the given value.
+   *   The operator to be used to test the given value. The possible values are:
+   *   - '=', '<>', '>', '>=', '<', '<=', '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
    * @param $delta_group
    *   An arbitrary identifier: conditions in the same group must have the same
    *   An arbitrary identifier: conditions in the same group must have the same
-   *   $delta_group.
+   *   $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
    * @param $language_group
    *   An arbitrary identifier: conditions in the same group must have the same
    *   An arbitrary identifier: conditions in the same group must have the same
    *   $language_group.
    *   $language_group.
@@ -723,9 +792,11 @@ class EntityFieldQuery {
    * @param $field
    * @param $field
    *   Either a field name or a field array.
    *   Either a field name or a field array.
    * @param $column
    * @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.
+   *   The column that should hold the value to be matched, defined in the
+   *   hook_field_schema() of this field. If this is omitted then all of the
+   *   other parameters are ignored, except $field, and this call will just be
+   *   adding a condition that says that the field has a value, rather than
+   *   testing the value itself.
    * @param $value
    * @param $value
    *   The value to test the column value against. In most cases, this is a
    *   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
    *   scalar. For more complex options, it is an array. The meaning of each
@@ -744,10 +815,10 @@ class EntityFieldQuery {
    * @param $delta_group
    * @param $delta_group
    *   An arbitrary identifier: conditions in the same group must have the same
    *   An arbitrary identifier: conditions in the same group must have the same
    *   $delta_group. For example, let's presume a multivalue field which has
    *   $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
+   *   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
    *   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
    *   corresponding to 'red circle', however if you pass 'red' and 'circle' as
-   *   conditions, it will appear in the  results - by default queries will run
+   *   conditions, it will appear in the results -- by default queries will run
    *   against any combination of deltas. By passing the conditions with the
    *   against any combination of deltas. By passing the conditions with the
    *   same $delta_group it will ensure that only values attached to the same
    *   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.
    *   delta are matched, and entity 1 would then be excluded from the results.

+ 15 - 6
includes/errors.inc

@@ -9,7 +9,7 @@
  * Maps PHP error constants to watchdog severity levels.
  * Maps PHP error constants to watchdog severity levels.
  *
  *
  * The error constants are documented at
  * The error constants are documented at
- * http://php.net/manual/en/errorfunc.constants.php
+ * http://php.net/manual/errorfunc.constants.php
  *
  *
  * @ingroup logging_severity_levels
  * @ingroup logging_severity_levels
  */
  */
@@ -66,7 +66,7 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
     _drupal_log_error(array(
     _drupal_log_error(array(
       '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
       '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
       // The standard PHP error handler considers that the error messages
       // The standard PHP error handler considers that the error messages
-      // are HTML. We mimick this behavior here.
+      // are HTML. We mimic this behavior here.
       '!message' => filter_xss_admin($message),
       '!message' => filter_xss_admin($message),
       '%function' => $caller['function'],
       '%function' => $caller['function'],
       '%file' => $caller['file'],
       '%file' => $caller['file'],
@@ -114,7 +114,7 @@ function _drupal_decode_exception($exception) {
   return array(
   return array(
     '%type' => get_class($exception),
     '%type' => get_class($exception),
     // The standard PHP exception handler considers that the exception message
     // The standard PHP exception handler considers that the exception message
-    // is plain-text. We mimick this behavior here.
+    // is plain-text. We mimic this behavior here.
     '!message' => check_plain($message),
     '!message' => check_plain($message),
     '%function' => $caller['function'],
     '%function' => $caller['function'],
     '%file' => $caller['file'],
     '%file' => $caller['file'],
@@ -169,7 +169,7 @@ function error_displayable($error = NULL) {
  *   TRUE if the error is fatal.
  *   TRUE if the error is fatal.
  */
  */
 function _drupal_log_error($error, $fatal = FALSE) {
 function _drupal_log_error($error, $fatal = FALSE) {
-  // Initialize a maintenance theme if the boostrap was not complete.
+  // Initialize a maintenance theme if the bootstrap was not complete.
   // Do it early because drupal_set_message() triggers a drupal_theme_initialize().
   // Do it early because drupal_set_message() triggers a drupal_theme_initialize().
   if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) {
   if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) {
     unset($GLOBALS['theme']);
     unset($GLOBALS['theme']);
@@ -199,7 +199,16 @@ function _drupal_log_error($error, $fatal = FALSE) {
     $number++;
     $number++;
   }
   }
 
 
-  watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
+  // Log the error immediately, unless this is a non-fatal error which has been
+  // triggered via drupal_trigger_error_with_delayed_logging(); in that case
+  // trigger it in a shutdown function. Fatal errors are always triggered
+  // immediately since for a fatal error the page request will end here anyway.
+  if (!$fatal && drupal_static('_drupal_trigger_error_with_delayed_logging')) {
+    drupal_register_shutdown_function('watchdog', 'php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
+  }
+  else {
+    watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
+  }
 
 
   if ($fatal) {
   if ($fatal) {
     drupal_add_http_header('Status', '500 Service unavailable (with message)');
     drupal_add_http_header('Status', '500 Service unavailable (with message)');
@@ -224,7 +233,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
   }
   }
   else {
   else {
     // Display the message if the current error reporting level allows this type
     // Display the message if the current error reporting level allows this type
-    // of message to be displayed, and unconditionnaly in update.php.
+    // of message to be displayed, and unconditionally in update.php.
     if (error_displayable($error)) {
     if (error_displayable($error)) {
       $class = 'error';
       $class = 'error';
 
 

+ 249 - 99
includes/file.inc

@@ -273,7 +273,9 @@ function file_default_scheme() {
  *   The normalized URI.
  *   The normalized URI.
  */
  */
 function file_stream_wrapper_uri_normalize($uri) {
 function file_stream_wrapper_uri_normalize($uri) {
-  $scheme = file_uri_scheme($uri);
+  // Inline file_uri_scheme() function call for performance reasons.
+  $position = strpos($uri, '://');
+  $scheme = $position ? substr($uri, 0, $position) : FALSE;
 
 
   if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
   if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
     $target = file_uri_target($uri);
     $target = file_uri_target($uri);
@@ -470,8 +472,11 @@ function file_ensure_htaccess() {
  * @param $private
  * @param $private
  *   FALSE indicates that $directory should be an open and public directory.
  *   FALSE indicates that $directory should be an open and public directory.
  *   The default is TRUE which indicates a private and protected directory.
  *   The default is TRUE which indicates a private and protected directory.
+ * @param $force_overwrite
+ *   Set to TRUE to attempt to overwrite the existing .htaccess file if one is
+ *   already present. Defaults to FALSE.
  */
  */
-function file_create_htaccess($directory, $private = TRUE) {
+function file_create_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
   if (file_uri_scheme($directory)) {
   if (file_uri_scheme($directory)) {
     $directory = file_stream_wrapper_uri_normalize($directory);
     $directory = file_stream_wrapper_uri_normalize($directory);
   }
   }
@@ -480,19 +485,12 @@ function file_create_htaccess($directory, $private = TRUE) {
   }
   }
   $htaccess_path =  $directory . '/.htaccess';
   $htaccess_path =  $directory . '/.htaccess';
 
 
-  if (file_exists($htaccess_path)) {
+  if (file_exists($htaccess_path) && !$force_overwrite) {
     // Short circuit if the .htaccess file already exists.
     // Short circuit if the .htaccess file already exists.
     return;
     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";
-  }
+  $htaccess_lines = file_htaccess_lines($private);
 
 
   // Write the .htaccess file.
   // Write the .htaccess file.
   if (file_put_contents($htaccess_path, $htaccess_lines)) {
   if (file_put_contents($htaccess_path, $htaccess_lines)) {
@@ -504,6 +502,59 @@ function file_create_htaccess($directory, $private = TRUE) {
   }
   }
 }
 }
 
 
+/**
+ * Returns the standard .htaccess lines that Drupal writes to file directories.
+ *
+ * @param $private
+ *   (Optional) Set to FALSE to return the .htaccess lines for an open and
+ *   public directory. The default is TRUE, which returns the .htaccess lines
+ *   for a private and protected directory.
+ *
+ * @return
+ *   A string representing the desired contents of the .htaccess file.
+ *
+ * @see file_create_htaccess()
+ */
+function file_htaccess_lines($private = TRUE) {
+  $lines = <<<EOF
+# Turn off all options we don't need.
+Options None
+Options +FollowSymLinks
+
+# Set the catch-all handler to prevent scripts from being executed.
+SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
+<Files *>
+  # Override the handler again if we're run later in the evaluation list.
+  SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
+</Files>
+
+# If we know how to do it safely, disable the PHP engine entirely.
+<IfModule mod_php5.c>
+  php_flag engine off
+</IfModule>
+<IfModule mod_php7.c>
+  php_flag engine off
+</IfModule>
+EOF;
+
+  if ($private) {
+    $lines = <<<EOF
+# Deny all requests from Apache 2.4+.
+<IfModule mod_authz_core.c>
+  Require all denied
+</IfModule>
+
+# Deny all requests from Apache 2.0-2.2.
+<IfModule !mod_authz_core.c>
+  Deny from all
+</IfModule>
+EOF
+    . "\n\n" . $lines;
+  }
+
+  return $lines;
+}
+
 /**
 /**
  * Loads file objects from the database.
  * Loads file objects from the database.
  *
  *
@@ -586,7 +637,11 @@ function file_save(stdClass $file) {
     module_invoke_all('entity_update', $file, 'file');
     module_invoke_all('entity_update', $file, 'file');
   }
   }
 
 
+  // Clear internal properties.
   unset($file->original);
   unset($file->original);
+  // Clear the static loading cache.
+  entity_get_controller('file')->resetCache(array($file->fid));
+
   return $file;
   return $file;
 }
 }
 
 
@@ -719,10 +774,11 @@ function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $c
  * stored in the database. This is a powerful function that in many ways
  * stored in the database. This is a powerful function that in many ways
  * performs like an advanced version of copy().
  * performs like an advanced version of copy().
  * - Checks if $source and $destination are valid and readable/writable.
  * - 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,
  * - If file already exists in $destination either the call will error out,
  *   replace the file or rename the file based on the $replace parameter.
  *   replace the file or rename the file based on the $replace parameter.
+ * - If the $source and $destination are equal, the behavior depends on the
+ *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
+ *   will rename the file until the $destination is unique.
  * - Adds the new file to the files database. If the source file is a
  * - 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
  *   temporary file, the resulting file will also be a temporary file. See
  *   file_save_upload() for details on temporary files.
  *   file_save_upload() for details on temporary files.
@@ -817,10 +873,11 @@ function file_valid_uri($uri) {
  * This is a powerful function that in many ways performs like an advanced
  * This is a powerful function that in many ways performs like an advanced
  * version of copy().
  * version of copy().
  * - Checks if $source and $destination are valid and readable/writable.
  * - 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,
  * - If file already exists in $destination either the call will error out,
  *   replace the file or rename the file based on the $replace parameter.
  *   replace the file or rename the file based on the $replace parameter.
+ * - If the $source and $destination are equal, the behavior depends on the
+ *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
+ *   will rename the file until the $destination is unique.
  * - Provides a fallback using realpaths if the move fails using stream
  * - Provides a fallback using realpaths if the move fails using stream
  *   wrappers. This can occur because PHP's copy() function does not properly
  *   wrappers. This can occur because PHP's copy() function does not properly
  *   support streams if safe_mode or open_basedir are enabled. See
  *   support streams if safe_mode or open_basedir are enabled. See
@@ -846,7 +903,6 @@ function file_valid_uri($uri) {
  */
  */
 function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
 function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
   $original_source = $source;
   $original_source = $source;
-  $original_destination = $destination;
 
 
   // Assert that the source file actually exists.
   // Assert that the source file actually exists.
   if (!file_exists($source)) {
   if (!file_exists($source)) {
@@ -940,8 +996,15 @@ function file_build_uri($path) {
  * @return
  * @return
  *   The destination filepath, or FALSE if the file already exists
  *   The destination filepath, or FALSE if the file already exists
  *   and FILE_EXISTS_ERROR is specified.
  *   and FILE_EXISTS_ERROR is specified.
+ *
+ * @throws RuntimeException
+ *   Thrown if the filename contains invalid UTF-8.
  */
  */
 function file_destination($destination, $replace) {
 function file_destination($destination, $replace) {
+  $basename = drupal_basename($destination);
+  if (!drupal_validate_utf8($basename)) {
+    throw new RuntimeException(sprintf("Invalid filename '%s'", $basename));
+  }
   if (file_exists($destination)) {
   if (file_exists($destination)) {
     switch ($replace) {
     switch ($replace) {
       case FILE_EXISTS_REPLACE:
       case FILE_EXISTS_REPLACE:
@@ -949,7 +1012,6 @@ function file_destination($destination, $replace) {
         break;
         break;
 
 
       case FILE_EXISTS_RENAME:
       case FILE_EXISTS_RENAME:
-        $basename = drupal_basename($destination);
         $directory = drupal_dirname($destination);
         $directory = drupal_dirname($destination);
         $destination = file_create_filename($basename, $directory);
         $destination = file_create_filename($basename, $directory);
         break;
         break;
@@ -1108,10 +1170,10 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
 
 
   // Allow potentially insecure uploads for very savvy users and admin
   // Allow potentially insecure uploads for very savvy users and admin
   if (!variable_get('allow_insecure_uploads', 0)) {
   if (!variable_get('allow_insecure_uploads', 0)) {
-    // Remove any null bytes. See http://php.net/manual/en/security.filesystem.nullbytes.php
+    // Remove any null bytes. See http://php.net/manual/security.filesystem.nullbytes.php
     $filename = str_replace(chr(0), '', $filename);
     $filename = str_replace(chr(0), '', $filename);
 
 
-    $whitelist = array_unique(explode(' ', trim($extensions)));
+    $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
 
 
     // Split the filename up by periods. The first part becomes the basename
     // Split the filename up by periods. The first part becomes the basename
     // the last part the final extension.
     // the last part the final extension.
@@ -1124,7 +1186,7 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
     // of allowed extensions.
     // of allowed extensions.
     foreach ($filename_parts as $filename_part) {
     foreach ($filename_parts as $filename_part) {
       $new_filename .= '.' . $filename_part;
       $new_filename .= '.' . $filename_part;
-      if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
+      if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
         $new_filename .= '_';
         $new_filename .= '_';
       }
       }
     }
     }
@@ -1165,11 +1227,20 @@ function file_unmunge_filename($filename) {
  * @return
  * @return
  *   File path consisting of $directory and a unique filename based off
  *   File path consisting of $directory and a unique filename based off
  *   of $basename.
  *   of $basename.
+ *
+ * @throws RuntimeException
+ *   Thrown if the $basename is not valid UTF-8 or another error occurs
+ *   stripping control characters.
  */
  */
 function file_create_filename($basename, $directory) {
 function file_create_filename($basename, $directory) {
+  $original = $basename;
   // Strip control characters (ASCII value < 32). Though these are allowed in
   // Strip control characters (ASCII value < 32). Though these are allowed in
   // some filesystems, not many applications handle them well.
   // some filesystems, not many applications handle them well.
   $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
   $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
+  if (preg_last_error() !== PREG_NO_ERROR) {
+    throw new RuntimeException(sprintf("Invalid filename '%s'", $original));
+  }
+
   if (substr(PHP_OS, 0, 3) == 'WIN') {
   if (substr(PHP_OS, 0, 3) == 'WIN') {
     // These characters are not allowed in Windows filenames
     // These characters are not allowed in Windows filenames
     $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
     $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
@@ -1256,6 +1327,7 @@ function file_delete(stdClass $file, $force = FALSE) {
   if (file_unmanaged_delete($file->uri)) {
   if (file_unmanaged_delete($file->uri)) {
     db_delete('file_managed')->condition('fid', $file->fid)->execute();
     db_delete('file_managed')->condition('fid', $file->fid)->execute();
     db_delete('file_usage')->condition('fid', $file->fid)->execute();
     db_delete('file_usage')->condition('fid', $file->fid)->execute();
+    entity_get_controller('file')->resetCache();
     return TRUE;
     return TRUE;
   }
   }
   return FALSE;
   return FALSE;
@@ -1365,8 +1437,9 @@ function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
  * Temporary files are periodically cleaned. To make the file a permanent file,
  * Temporary files are periodically cleaned. To make the file a permanent file,
  * assign the status and use file_save() to save the changes.
  * 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 $form_field_name
+ *   A string that is the associative array key of the upload form element in
+ *   the form array.
  * @param $validators
  * @param $validators
  *   An optional, associative array of callback functions used to validate the
  *   An optional, associative array of callback functions used to validate the
  *   file. See file_validate() for a full discussion of the array format.
  *   file. See file_validate() for a full discussion of the array format.
@@ -1377,9 +1450,9 @@ function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
  *   (Beware: this is not safe and should only be allowed for trusted users, if
  *   (Beware: this is not safe and should only be allowed for trusted users, if
  *   at all).
  *   at all).
  * @param $destination
  * @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://").
+ *   A string containing the URI that the file 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
  * @param $replace
  *   Replace behavior when the destination file already exists:
  *   Replace behavior when the destination file already exists:
  *   - FILE_EXISTS_REPLACE: Replace the existing file.
  *   - FILE_EXISTS_REPLACE: Replace the existing file.
@@ -1397,45 +1470,45 @@ function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
  *   - source: Path to the file before it is moved.
  *   - source: Path to the file before it is moved.
  *   - destination: Path to the file after it is moved (same as 'uri').
  *   - 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) {
+function file_save_upload($form_field_name, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
   global $user;
   global $user;
   static $upload_cache;
   static $upload_cache;
 
 
   // Return cached objects without processing since the file will have
   // Return cached objects without processing since the file will have
   // already been processed and the paths in _FILES will be invalid.
   // already been processed and the paths in _FILES will be invalid.
-  if (isset($upload_cache[$source])) {
-    return $upload_cache[$source];
+  if (isset($upload_cache[$form_field_name])) {
+    return $upload_cache[$form_field_name];
   }
   }
 
 
   // Make sure there's an upload to process.
   // Make sure there's an upload to process.
-  if (empty($_FILES['files']['name'][$source])) {
+  if (empty($_FILES['files']['name'][$form_field_name])) {
     return NULL;
     return NULL;
   }
   }
 
 
   // Check for file upload errors and return FALSE if a lower level system
   // Check for file upload errors and return FALSE if a lower level system
   // error occurred. For a complete list of errors:
   // error occurred. For a complete list of errors:
-  // See http://php.net/manual/en/features.file-upload.errors.php.
-  switch ($_FILES['files']['error'][$source]) {
+  // See http://php.net/manual/features.file-upload.errors.php.
+  switch ($_FILES['files']['error'][$form_field_name]) {
     case UPLOAD_ERR_INI_SIZE:
     case UPLOAD_ERR_INI_SIZE:
     case UPLOAD_ERR_FORM_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');
+      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'][$form_field_name], '%maxsize' => format_size(file_upload_max_size()))), 'error');
       return FALSE;
       return FALSE;
 
 
     case UPLOAD_ERR_PARTIAL:
     case UPLOAD_ERR_PARTIAL:
     case UPLOAD_ERR_NO_FILE:
     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');
+      drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $_FILES['files']['name'][$form_field_name])), 'error');
       return FALSE;
       return FALSE;
 
 
     case UPLOAD_ERR_OK:
     case UPLOAD_ERR_OK:
       // Final check that this is a valid upload, if it isn't, use the
       // Final check that this is a valid upload, if it isn't, use the
       // default error handler.
       // default error handler.
-      if (is_uploaded_file($_FILES['files']['tmp_name'][$source])) {
+      if (is_uploaded_file($_FILES['files']['tmp_name'][$form_field_name])) {
         break;
         break;
       }
       }
 
 
     // Unknown error
     // Unknown error
     default:
     default:
-      drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source])), 'error');
+      drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$form_field_name])), 'error');
       return FALSE;
       return FALSE;
   }
   }
 
 
@@ -1443,10 +1516,10 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
   $file = new stdClass();
   $file = new stdClass();
   $file->uid      = $user->uid;
   $file->uid      = $user->uid;
   $file->status   = 0;
   $file->status   = 0;
-  $file->filename = trim(drupal_basename($_FILES['files']['name'][$source]), '.');
-  $file->uri      = $_FILES['files']['tmp_name'][$source];
+  $file->filename = trim(drupal_basename($_FILES['files']['name'][$form_field_name]), '.');
+  $file->uri      = $_FILES['files']['tmp_name'][$form_field_name];
   $file->filemime = file_get_mimetype($file->filename);
   $file->filemime = file_get_mimetype($file->filename);
-  $file->filesize = $_FILES['files']['size'][$source];
+  $file->filesize = $_FILES['files']['size'][$form_field_name];
 
 
   $extensions = '';
   $extensions = '';
   if (isset($validators['file_validate_extensions'])) {
   if (isset($validators['file_validate_extensions'])) {
@@ -1479,9 +1552,9 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
   // rename filename.php.foo and filename.php to filename.php.foo.txt and
   // rename filename.php.foo and filename.php to filename.php.foo.txt and
   // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
   // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
   // evaluates to TRUE.
   // 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')) {
+  if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|phar|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
     $file->filemime = 'text/plain';
     $file->filemime = 'text/plain';
-    $file->uri .= '.txt';
+    // The destination filename will also later be used to create the URI.
     $file->filename .= '.txt';
     $file->filename .= '.txt';
     // The .txt extension may not be in the allowed list of extensions. We have
     // 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.
     // to add it here or else the file upload will fail.
@@ -1503,20 +1576,26 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
     return FALSE;
     return FALSE;
   }
   }
 
 
-  $file->source = $source;
+  $file->source = $form_field_name;
   // A URI may already have a trailing slash or look like "public://".
   // A URI may already have a trailing slash or look like "public://".
   if (substr($destination, -1) != '/') {
   if (substr($destination, -1) != '/') {
     $destination .= '/';
     $destination .= '/';
   }
   }
-  $file->destination = file_destination($destination . $file->filename, $replace);
+  try {
+    $file->destination = file_destination($destination . $file->filename, $replace);
+  }
+  catch (RuntimeException $e) {
+    drupal_set_message(t('The file %source could not be uploaded because the name is invalid.', array('%source' => $form_field_name)), 'error');
+    return FALSE;
+  }
   // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
   // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
   // there's an existing file so we need to bail.
   // there's an existing file so we need to bail.
   if ($file->destination === FALSE) {
   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');
+    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' => $form_field_name, '%directory' => $destination)), 'error');
     return FALSE;
     return FALSE;
   }
   }
 
 
-  // Add in our check of the the file name length.
+  // Add in our check of the file name length.
   $validators['file_validate_name_length'] = array();
   $validators['file_validate_name_length'] = array();
 
 
   // Call the validation functions specified by this function's caller.
   // Call the validation functions specified by this function's caller.
@@ -1531,7 +1610,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
     else {
     else {
       $message .= ' ' . array_pop($errors);
       $message .= ' ' . array_pop($errors);
     }
     }
-    form_set_error($source, $message);
+    form_set_error($form_field_name, $message);
     return FALSE;
     return FALSE;
   }
   }
 
 
@@ -1539,8 +1618,8 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
   // directory. This overcomes open_basedir restrictions for future file
   // directory. This overcomes open_basedir restrictions for future file
   // operations.
   // operations.
   $file->uri = $file->destination;
   $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.'));
+  if (!drupal_move_uploaded_file($_FILES['files']['tmp_name'][$form_field_name], $file->uri)) {
+    form_set_error($form_field_name, 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));
     watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
     return FALSE;
     return FALSE;
   }
   }
@@ -1559,8 +1638,22 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
 
 
   // If we made it this far it's safe to record this file in the database.
   // If we made it this far it's safe to record this file in the database.
   if ($file = file_save($file)) {
   if ($file = file_save($file)) {
+    // Track non-public files in the session if they were uploaded by an
+    // anonymous user. This allows modules such as the File module to only
+    // grant view access to the specific anonymous user who uploaded the file.
+    // See file_file_download().
+    // The 'file_public_schema' variable is used to allow other publicly
+    // accessible file schemes to be treated the same as the public:// scheme
+    // provided by Drupal core and to avoid adding unnecessary data to the
+    // session (and the resulting bypass of the page cache) in those cases. For
+    // security reasons, only schemes that are completely publicly accessible,
+    // with no download restrictions, should be added to this variable. See
+    // file_managed_file_value().
+    if (!$user->uid && !in_array($destination_scheme, variable_get('file_public_schema', array('public')))) {
+      $_SESSION['anonymous_allowed_file_ids'][$file->fid] = $file->fid;
+    }
     // Add file to the cache.
     // Add file to the cache.
-    $upload_cache[$source] = $file;
+    $upload_cache[$form_field_name] = $file;
     return $file;
     return $file;
   }
   }
   return FALSE;
   return FALSE;
@@ -1686,8 +1779,6 @@ function file_validate_extensions(stdClass $file, $extensions) {
 /**
 /**
  * Checks that the file's size is below certain limits.
  * Checks that the file's size is below certain limits.
  *
  *
- * This check is not enforced for the user #1.
- *
  * @param $file
  * @param $file
  *   A Drupal file object.
  *   A Drupal file object.
  * @param $file_limit
  * @param $file_limit
@@ -1705,20 +1796,17 @@ function file_validate_extensions(stdClass $file, $extensions) {
  */
  */
 function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) {
 function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) {
   global $user;
   global $user;
-
   $errors = array();
   $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)));
-    }
+  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)));
-    }
+  // 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;
   return $errors;
 }
 }
 
 
@@ -1747,7 +1835,7 @@ function file_validate_is_image(stdClass $file) {
 /**
 /**
  * Verifies that image dimensions are within the specified maximum and minimum.
  * 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
+ * Non-image files will be ignored. If an image toolkit is available the image
  * will be scaled to fit within the desired maximum dimensions.
  * will be scaled to fit within the desired maximum dimensions.
  *
  *
  * @param $file
  * @param $file
@@ -1956,23 +2044,7 @@ function file_download() {
   $target = implode('/', $args);
   $target = implode('/', $args);
   $uri = $scheme . '://' . $target;
   $uri = $scheme . '://' . $target;
   if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
   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);
-      }
-    }
+    $headers = file_download_headers($uri);
     if (count($headers)) {
     if (count($headers)) {
       file_transfer($uri, $headers);
       file_transfer($uri, $headers);
     }
     }
@@ -1984,6 +2056,69 @@ function file_download() {
   drupal_exit();
   drupal_exit();
 }
 }
 
 
+/**
+ * Retrieves headers for a private file download.
+ *
+ * Calls all module implementations of hook_file_download() to retrieve headers
+ * for files by the module that originally provided the file. The presence of
+ * returned headers indicates the current user has access to the file.
+ *
+ * @param $uri
+ *   The URI for the file whose headers should be retrieved.
+ *
+ * @return
+ *   If access is allowed, headers for the file, suitable for passing to
+ *   file_transfer(). If access is not allowed, an empty array will be returned.
+ *
+ * @see file_transfer()
+ * @see file_download_access()
+ * @see hook_file_download()
+ */
+function file_download_headers($uri) {
+  // Let other modules provide headers and control 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);
+    }
+  }
+  return $headers;
+}
+
+/**
+ * Checks that the current user has access to a particular file.
+ *
+ * The return value of this function hinges on the return value from
+ * file_download_headers(), which is the function responsible for collecting
+ * access information through hook_file_download().
+ *
+ * If immediately transferring the file to the browser and the headers will
+ * need to be retrieved, the return value of file_download_headers() should be
+ * used to determine access directly, so that access checks will not be run
+ * twice.
+ *
+ * @param $uri
+ *   The URI for the file whose access should be retrieved.
+ *
+ * @return
+ *   Boolean TRUE if access is allowed. FALSE if access is not allowed.
+ *
+ * @see file_download_headers()
+ * @see hook_file_download()
+ */
+function file_download_access($uri) {
+  return count(file_download_headers($uri)) > 0;
+}
 
 
 /**
 /**
  * Finds all files that match a given mask in a given directory.
  * Finds all files that match a given mask in a given directory.
@@ -2019,9 +2154,33 @@ function file_download() {
  *   'filename', and 'name' members corresponding to the matching files.
  *   'filename', and 'name' members corresponding to the matching files.
  */
  */
 function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
 function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
+  // Default nomask option.
+  $nomask = '/(\.\.?|CVS)$/';
+
+  // Overrides the $nomask variable accordingly if $options['nomask'] is set.
+  //
+  // Allow directories specified in settings.php to be ignored. You can use this
+  // to not check for files in common special-purpose directories. For example,
+  // node_modules and bower_components. Ignoring irrelevant directories is a
+  // performance boost.
+  if (!isset($options['nomask'])) {
+    $ignore_directories = variable_get(
+      'file_scan_ignore_directories',
+      array()
+    );
+
+    foreach ($ignore_directories as $index => $ignore_directory) {
+      $ignore_directories[$index] = preg_quote($ignore_directory, '/');
+    }
+
+    if (!empty($ignore_directories)) {
+      $nomask = '/^(\.\.?)|CVS|' . implode('|', $ignore_directories) . '$/';
+    }
+  }
+
   // Merge in defaults.
   // Merge in defaults.
   $options += array(
   $options += array(
-    'nomask' => '/(\.\.?|CVS)$/',
+    'nomask' => $nomask,
     'callback' => 0,
     'callback' => 0,
     'recurse' => TRUE,
     'recurse' => TRUE,
     'key' => 'uri',
     'key' => 'uri',
@@ -2177,7 +2336,7 @@ function drupal_chmod($uri, $mode = NULL) {
  * @param $uri
  * @param $uri
  *   A URI or pathname.
  *   A URI or pathname.
  * @param $context
  * @param $context
- *   Refer to http://php.net/manual/en/ref.stream.php
+ *   Refer to http://php.net/manual/ref.stream.php
  *
  *
  * @return
  * @return
  *   Boolean TRUE on success, or FALSE on failure.
  *   Boolean TRUE on success, or FALSE on failure.
@@ -2199,29 +2358,21 @@ function drupal_unlink($uri, $context = NULL) {
 }
 }
 
 
 /**
 /**
- * Returns the absolute local filesystem path of a stream URI.
+ * Resolves the absolute filepath of a local URI or filepath.
  *
  *
- * 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.
+ * The use of drupal_realpath() is discouraged, because it does not work for
+ * remote URIs. Except in rare cases, URIs should not be manually resolved.
  *
  *
  * Only use this function if you know that the stream wrapper in the URI uses
  * 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
  * the local file system, and you need to pass an absolute path to a function
  * that is incompatible with stream URIs.
  * 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.
+ * @param string $uri
+ *   A stream wrapper URI or a filepath, possibly including one or more symbolic
+ *   links.
  *
  *
- * @todo This function is deprecated, and should be removed wherever possible.
+ * @return string|false
+ *   The absolute local filepath (with no symbolic links), or FALSE on failure.
  *
  *
  * @see DrupalStreamWrapperInterface::realpath()
  * @see DrupalStreamWrapperInterface::realpath()
  * @see http://php.net/manual/function.realpath.php
  * @see http://php.net/manual/function.realpath.php
@@ -2318,7 +2469,7 @@ function drupal_basename($uri, $suffix = NULL) {
  * @param $recursive
  * @param $recursive
  *   Default to FALSE.
  *   Default to FALSE.
  * @param $context
  * @param $context
- *   Refer to http://php.net/manual/en/ref.stream.php
+ *   Refer to http://php.net/manual/ref.stream.php
  *
  *
  * @return
  * @return
  *   Boolean TRUE on success, or FALSE on failure.
  *   Boolean TRUE on success, or FALSE on failure.
@@ -2349,7 +2500,7 @@ function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
  * @param $uri
  * @param $uri
  *   A URI or pathname.
  *   A URI or pathname.
  * @param $context
  * @param $context
- *   Refer to http://php.net/manual/en/ref.stream.php
+ *   Refer to http://php.net/manual/ref.stream.php
  *
  *
  * @return
  * @return
  *   Boolean TRUE on success, or FALSE on failure.
  *   Boolean TRUE on success, or FALSE on failure.
@@ -2474,7 +2625,6 @@ function file_directory_temp() {
  *   An associative array of headers, as expected by file_transfer().
  *   An associative array of headers, as expected by file_transfer().
  */
  */
 function file_get_content_headers($file) {
 function file_get_content_headers($file) {
-  $name = mime_header_encode($file->filename);
   $type = mime_header_encode($file->filemime);
   $type = mime_header_encode($file->filemime);
 
 
   return array(
   return array(

+ 20 - 0
includes/file.mimetypes.inc

@@ -43,6 +43,7 @@ function file_default_mimetype_mapping() {
       4 => 'application/cap',
       4 => 'application/cap',
       5 => 'application/cu-seeme',
       5 => 'application/cu-seeme',
       6 => 'application/dsptype',
       6 => 'application/dsptype',
+      350 => 'application/epub+zip',
       7 => 'application/hta',
       7 => 'application/hta',
       8 => 'application/java-archive',
       8 => 'application/java-archive',
       9 => 'application/java-serialized-object',
       9 => 'application/java-serialized-object',
@@ -64,6 +65,7 @@ function file_default_mimetype_mapping() {
       25 => 'application/rss+xml',
       25 => 'application/rss+xml',
       26 => 'application/rtf',
       26 => 'application/rtf',
       27 => 'application/smil',
       27 => 'application/smil',
+      349 => 'application/vnd.amazon.ebook',
       28 => 'application/vnd.cinderella',
       28 => 'application/vnd.cinderella',
       29 => 'application/vnd.google-earth.kml+xml',
       29 => 'application/vnd.google-earth.kml+xml',
       30 => 'application/vnd.google-earth.kmz',
       30 => 'application/vnd.google-earth.kmz',
@@ -183,6 +185,8 @@ function file_default_mimetype_mapping() {
       144 => 'application/x-lzx',
       144 => 'application/x-lzx',
       145 => 'application/x-maker',
       145 => 'application/x-maker',
       146 => 'application/x-mif',
       146 => 'application/x-mif',
+      351 => 'application/x-mobipocket-ebook',
+      352 => 'application/x-mobipocket-ebook',
       147 => 'application/x-ms-wmd',
       147 => 'application/x-ms-wmd',
       148 => 'application/x-ms-wmz',
       148 => 'application/x-ms-wmz',
       149 => 'application/x-msdos-program',
       149 => 'application/x-msdos-program',
@@ -228,8 +232,10 @@ function file_default_mimetype_mapping() {
       188 => 'audio/mpeg',
       188 => 'audio/mpeg',
       189 => 'audio/ogg',
       189 => 'audio/ogg',
       190 => 'audio/prs.sid',
       190 => 'audio/prs.sid',
+      356 => 'audio/webm',
       191 => 'audio/x-aiff',
       191 => 'audio/x-aiff',
       192 => 'audio/x-gsm',
       192 => 'audio/x-gsm',
+      354 => 'audio/x-matroska',
       193 => 'audio/x-mpegurl',
       193 => 'audio/x-mpegurl',
       194 => 'audio/x-ms-wax',
       194 => 'audio/x-ms-wax',
       195 => 'audio/x-ms-wma',
       195 => 'audio/x-ms-wma',
@@ -301,6 +307,7 @@ function file_default_mimetype_mapping() {
       261 => 'image/vnd.djvu',
       261 => 'image/vnd.djvu',
       262 => 'image/vnd.microsoft.icon',
       262 => 'image/vnd.microsoft.icon',
       263 => 'image/vnd.wap.wbmp',
       263 => 'image/vnd.wap.wbmp',
+      355 => 'image/webp',
       264 => 'image/x-cmu-raster',
       264 => 'image/x-cmu-raster',
       265 => 'image/x-coreldraw',
       265 => 'image/x-coreldraw',
       266 => 'image/x-coreldrawpattern',
       266 => 'image/x-coreldrawpattern',
@@ -337,6 +344,7 @@ function file_default_mimetype_mapping() {
       297 => 'text/vnd.sun.j2me.app-descriptor',
       297 => 'text/vnd.sun.j2me.app-descriptor',
       298 => 'text/vnd.wap.wml',
       298 => 'text/vnd.wap.wml',
       299 => 'text/vnd.wap.wmlscript',
       299 => 'text/vnd.wap.wmlscript',
+      358 => 'text/vtt',
       300 => 'text/x-bibtex',
       300 => 'text/x-bibtex',
       301 => 'text/x-boo',
       301 => 'text/x-boo',
       302 => 'text/x-c++hdr',
       302 => 'text/x-c++hdr',
@@ -371,9 +379,11 @@ function file_default_mimetype_mapping() {
       331 => 'video/ogg',
       331 => 'video/ogg',
       332 => 'video/quicktime',
       332 => 'video/quicktime',
       333 => 'video/vnd.mpegurl',
       333 => 'video/vnd.mpegurl',
+      357 => 'video/webm',
       347 => 'video/x-flv',
       347 => 'video/x-flv',
       334 => 'video/x-la-asf',
       334 => 'video/x-la-asf',
       348 => 'video/x-m4v',
       348 => 'video/x-m4v',
+      353 => 'video/x-matroska',
       335 => 'video/x-mng',
       335 => 'video/x-mng',
       336 => 'video/x-ms-asf',
       336 => 'video/x-ms-asf',
       337 => 'video/x-ms-wm',
       337 => 'video/x-ms-wm',
@@ -854,6 +864,16 @@ function file_default_mimetype_mapping() {
       'f4b' => 346,
       'f4b' => 346,
       'flv' => 347,
       'flv' => 347,
       'm4v' => 348,
       'm4v' => 348,
+      'azw' => 349,
+      'epub' => 350,
+      'mobi' => 351,
+      'prc' => 352,
+      'mkv' => 353,
+      'mka' => 354,
+      'webp' => 355,
+      'weba' => 356,
+      'webm' => 357,
+      'vtt' => 358,
     ),
     ),
   );
   );
 }
 }

+ 55 - 0
includes/file.phar.inc

@@ -0,0 +1,55 @@
+<?php
+
+use Drupal\Core\Security\PharExtensionInterceptor;
+use TYPO3\PharStreamWrapper\Manager as PharStreamWrapperManager;
+use TYPO3\PharStreamWrapper\Behavior as PharStreamWrapperBehavior;
+use TYPO3\PharStreamWrapper\PharStreamWrapper;
+
+/**
+ * Registers a phar stream wrapper that is more secure than PHP's built-in one.
+ *
+ * @see file_get_stream_wrappers()
+ */
+function file_register_phar_wrapper() {
+  $directory = DRUPAL_ROOT . '/misc/typo3/phar-stream-wrapper/src';
+  include_once $directory . '/Assertable.php';
+  include_once $directory . '/Behavior.php';
+  include_once $directory . '/Exception.php';
+  include_once $directory . '/Helper.php';
+  include_once $directory . '/Manager.php';
+  include_once $directory . '/PharStreamWrapper.php';
+  include_once $directory . '/Collectable.php';
+  include_once $directory . '/Interceptor/ConjunctionInterceptor.php';
+  include_once $directory . '/Interceptor/PharMetaDataInterceptor.php';
+  include_once $directory . '/Phar/Container.php';
+  include_once $directory . '/Phar/DeserializationException.php';
+  include_once $directory . '/Phar/Manifest.php';
+  include_once $directory . '/Phar/Reader.php';
+  include_once $directory . '/Phar/ReaderException.php';
+  include_once $directory . '/Phar/Stub.php';
+  include_once $directory . '/Resolvable.php';
+  include_once $directory . '/Resolver/PharInvocation.php';
+  include_once $directory . '/Resolver/PharInvocationCollection.php';
+  include_once $directory . '/Resolver/PharInvocationResolver.php';
+  include_once DRUPAL_ROOT . '/misc/typo3/drupal-security/PharExtensionInterceptor.php';
+  include_once DRUPAL_ROOT . '/misc/brumann/polyfill-unserialize/src/Unserialize.php';
+
+  // Set up a stream wrapper to handle insecurities due to PHP's built-in
+  // phar stream wrapper.
+  try {
+    $behavior = new PharStreamWrapperBehavior();
+    PharStreamWrapperManager::initialize(
+      $behavior->withAssertion(new PharExtensionInterceptor())
+    );
+  }
+  catch (\LogicException $e) {
+    // Continue if the PharStreamWrapperManager is already initialized.
+    // For example, this occurs following a drupal_static_reset(), such
+    // as during tests.
+  };
+
+  // To prevent file_stream_wrapper_valid_scheme() treating "phar" as a valid
+  // scheme, this is registered with PHP only, not with  hook_stream_wrappers()
+  // or the internal storage of file_get_stream_wrappers().
+  stream_wrapper_register('phar', '\\TYPO3\\PharStreamWrapper\\PharStreamWrapper');
+}

+ 1 - 1
includes/filetransfer/filetransfer.inc

@@ -301,7 +301,7 @@ abstract class FileTransfer {
     $parts = explode('/', $path);
     $parts = explode('/', $path);
     $chroot = '';
     $chroot = '';
     while (count($parts)) {
     while (count($parts)) {
-      $check = implode($parts, '/');
+      $check = implode('/', $parts);
       if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
       if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
         // Remove the trailing slash.
         // Remove the trailing slash.
         return substr($chroot, 0, -1);
         return substr($chroot, 0, -1);

+ 3 - 3
includes/filetransfer/ftp.inc

@@ -82,11 +82,11 @@ class FileTransferFTPExtension extends FileTransferFTP implements FileTransferCh
     if (!$list) {
     if (!$list) {
       $list = array();
       $list = array();
     }
     }
-    foreach ($list as $item){
+    foreach ($list as $item) {
       if ($item == '.' || $item == '..') {
       if ($item == '.' || $item == '..') {
         continue;
         continue;
       }
       }
-      if (@ftp_chdir($this->connection, $item)){
+      if (@ftp_chdir($this->connection, $item)) {
         ftp_cdup($this->connection);
         ftp_cdup($this->connection);
         $this->removeDirectory(ftp_pwd($this->connection) . '/' . $item);
         $this->removeDirectory(ftp_pwd($this->connection) . '/' . $item);
       }
       }
@@ -122,7 +122,7 @@ class FileTransferFTPExtension extends FileTransferFTP implements FileTransferCh
 
 
   function chmodJailed($path, $mode, $recursive) {
   function chmodJailed($path, $mode, $recursive) {
     if (!ftp_chmod($this->connection, $mode, $path)) {
     if (!ftp_chmod($this->connection, $mode, $path)) {
-      throw new FileTransferException("Unable to set permissions on %file", NULL, array ('%file' => $path));
+      throw new FileTransferException("Unable to set permissions on %file", NULL, array('%file' => $path));
     }
     }
     if ($this->isDirectory($path) && $recursive) {
     if ($this->isDirectory($path) && $recursive) {
       $filelist = @ftp_nlist($this->connection, $path);
       $filelist = @ftp_nlist($this->connection, $path);

+ 4 - 2
includes/filetransfer/ssh.inc

@@ -72,7 +72,8 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
         return TRUE;
         return TRUE;
       }
       }
       return FALSE;
       return FALSE;
-    } else {
+    }
+    else {
       throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
       throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
     }
     }
   }
   }
@@ -85,7 +86,8 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
         return TRUE;
         return TRUE;
       }
       }
       return FALSE;
       return FALSE;
-    } else {
+    }
+    else {
       throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
       throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
     }
     }
   }
   }

+ 271 - 65
includes/form.inc

@@ -15,10 +15,9 @@
  * reference the form builder function using \@see. For examples, of this see
  * reference the form builder function using \@see. For examples, of this see
  * system_modules_uninstall() or user_pass(), the latter of which has the
  * system_modules_uninstall() or user_pass(), the latter of which has the
  * following in its doxygen documentation:
  * following in its doxygen documentation:
- *
- * \@ingroup forms
- * \@see user_pass_validate().
- * \@see user_pass_submit().
+ * - \@ingroup forms
+ * - \@see user_pass_validate()
+ * - \@see user_pass_submit()
  *
  *
  * @}
  * @}
  */
  */
@@ -106,7 +105,8 @@
  *   generate the same form (or very similar forms) using different $form_ids
  *   generate the same form (or very similar forms) using different $form_ids
  *   can implement hook_forms(), which maps different $form_id values to the
  *   can implement hook_forms(), which maps different $form_id values to the
  *   proper form constructor function. Examples may be found in node_forms(),
  *   proper form constructor function. Examples may be found in node_forms(),
- *   and search_forms().
+ *   and search_forms(). hook_forms() can also be used to define forms in
+ *   classes.
  * @param ...
  * @param ...
  *   Any additional arguments are passed on to the functions called by
  *   Any additional arguments are passed on to the functions called by
  *   drupal_get_form(), including the unique form constructor function. For
  *   drupal_get_form(), including the unique form constructor function. For
@@ -168,6 +168,12 @@ function drupal_get_form($form_id) {
  *       processed.
  *       processed.
  *     - base_form_id: Identification for a base form, as declared in a
  *     - base_form_id: Identification for a base form, as declared in a
  *       hook_forms() implementation.
  *       hook_forms() implementation.
+ *     - immutable: If this flag is set to TRUE, a new form build id is
+ *       generated when the form is loaded from the cache. If it is subsequently
+ *       saved to the cache again, it will have another cache id and therefore
+ *       the original form and form-state will remain unaltered. This is
+ *       important when page caching is enabled in order to prevent form state
+ *       from leaking between anonymous users.
  *   - rebuild_info: Internal. Similar to 'build_info', but pertaining to
  *   - rebuild_info: Internal. Similar to 'build_info', but pertaining to
  *     drupal_rebuild_form().
  *     drupal_rebuild_form().
  *   - rebuild: Normally, after the entire form processing is completed and
  *   - rebuild: Normally, after the entire form processing is completed and
@@ -235,6 +241,12 @@ function drupal_get_form($form_id) {
  *     likely to occur during Ajax operations.
  *     likely to occur during Ajax operations.
  *   - programmed: If TRUE, the form was submitted programmatically, usually
  *   - programmed: If TRUE, the form was submitted programmatically, usually
  *     invoked via drupal_form_submit(). Defaults to FALSE.
  *     invoked via drupal_form_submit(). Defaults to FALSE.
+ *   - programmed_bypass_access_check: If TRUE, programmatic form submissions
+ *     are processed without taking #access into account. Set this to FALSE
+ *     when submitting a form programmatically with values that may have been
+ *     input by the user executing the current request; this will cause #access
+ *     to be respected as it would on a normal form submission. Defaults to
+ *     TRUE.
  *   - process_input: Boolean flag. TRUE signifies correct form submission.
  *   - process_input: Boolean flag. TRUE signifies correct form submission.
  *     This is always TRUE for programmed forms coming from drupal_form_submit()
  *     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
  *     (see 'programmed' key), or if the form_id coming from the $_POST data is
@@ -402,6 +414,7 @@ function form_state_defaults() {
     'submitted' => FALSE,
     'submitted' => FALSE,
     'executed' => FALSE,
     'executed' => FALSE,
     'programmed' => FALSE,
     'programmed' => FALSE,
+    'programmed_bypass_access_check' => TRUE,
     'cache'=> FALSE,
     'cache'=> FALSE,
     'method' => 'post',
     'method' => 'post',
     'groups' => array(),
     'groups' => array(),
@@ -452,17 +465,25 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
   $form = drupal_retrieve_form($form_id, $form_state);
   $form = drupal_retrieve_form($form_id, $form_state);
 
 
   // If only parts of the form will be returned to the browser (e.g., Ajax or
   // 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.
+  // RIA clients), or if the form already had a new build ID regenerated when it
+  // was retrieved from the form cache, reuse the existing #build_id.
   // Otherwise, a new #build_id is generated, to not clobber the previous
   // 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
   // build's data in the form cache; also allowing the user to go back to an
   // earlier build, make changes, and re-submit.
   // earlier build, make changes, and re-submit.
   // @see drupal_prepare_form()
   // @see drupal_prepare_form()
-  if (isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id'])) {
+  $enforce_old_build_id = isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id']);
+  $old_form_is_mutable_copy = isset($old_form['#build_id_old']);
+  if ($enforce_old_build_id || $old_form_is_mutable_copy) {
     $form['#build_id'] = $old_form['#build_id'];
     $form['#build_id'] = $old_form['#build_id'];
+    if ($old_form_is_mutable_copy) {
+      $form['#build_id_old'] = $old_form['#build_id_old'];
+    }
   }
   }
   else {
   else {
-    $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
+    if (isset($old_form['#build_id'])) {
+      $form['#build_id_old'] = $old_form['#build_id'];
+    }
+    $form['#build_id'] = 'form-' . drupal_random_key();
   }
   }
 
 
   // #action defaults to request_uri(), but in case of Ajax and other partial
   // #action defaults to request_uri(), but in case of Ajax and other partial
@@ -516,6 +537,15 @@ function form_get_cache($form_build_id, &$form_state) {
           }
           }
         }
         }
       }
       }
+      // Generate a new #build_id if the cached form was rendered on a cacheable
+      // page.
+      if (!empty($form_state['build_info']['immutable'])) {
+        $form['#build_id_old'] = $form['#build_id'];
+        $form['#build_id'] = 'form-' . drupal_random_key();
+        $form['form_build_id']['#value'] = $form['#build_id'];
+        $form['form_build_id']['#id'] = $form['#build_id'];
+        unset($form_state['build_info']['immutable']);
+      }
       return $form;
       return $form;
     }
     }
   }
   }
@@ -525,18 +555,33 @@ function form_get_cache($form_build_id, &$form_state) {
  * Stores a form in the cache.
  * Stores a form in the cache.
  */
  */
 function form_set_cache($form_build_id, $form, $form_state) {
 function form_set_cache($form_build_id, $form, $form_state) {
-  // 6 hours cache life time for forms should be plenty.
-  $expire = 21600;
+  // The default cache_form expiration is 6 hours. On busy sites, the cache_form
+  // table can become very large. A shorter cache lifetime can help to keep the
+  // table's size under control.
+  $expire = variable_get('form_cache_expiration', 21600);
+
+  // Ensure that the form build_id embedded in the form structure is the same as
+  // the one passed in as a parameter. This is an additional safety measure to
+  // prevent legacy code operating directly with form_get_cache and
+  // form_set_cache from accidentally overwriting immutable form state.
+  if ($form['#build_id'] != $form_build_id) {
+    watchdog('form', 'Form build-id mismatch detected while attempting to store a form in the cache.', array(), WATCHDOG_ERROR);
+    return;
+  }
 
 
   // Cache form structure.
   // Cache form structure.
   if (isset($form)) {
   if (isset($form)) {
     if ($GLOBALS['user']->uid) {
     if ($GLOBALS['user']->uid) {
       $form['#cache_token'] = drupal_get_token();
       $form['#cache_token'] = drupal_get_token();
     }
     }
+    unset($form['#build_id_old']);
     cache_set('form_' . $form_build_id, $form, 'cache_form', REQUEST_TIME + $expire);
     cache_set('form_' . $form_build_id, $form, 'cache_form', REQUEST_TIME + $expire);
   }
   }
 
 
   // Cache form state.
   // Cache form state.
+  if (variable_get('cache', 0) && drupal_page_is_cacheable()) {
+    $form_state['build_info']['immutable'] = TRUE;
+  }
   if ($data = array_diff_key($form_state, array_flip(form_state_keys_no_cache()))) {
   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);
     cache_set('form_state_' . $form_build_id, $data, 'cache_form', REQUEST_TIME + $expire);
   }
   }
@@ -727,8 +772,9 @@ function drupal_retrieve_form($form_id, &$form_state) {
   // Record the filepath of the include file containing the original form, so
   // 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
   // the form builder callbacks can be loaded when the form is being rebuilt
   // from cache on a different path (such as 'system/ajax'). See
   // from cache on a different path (such as 'system/ajax'). See
-  // form_get_cache().
-  // $menu_get_item() is not available during installation.
+  // form_get_cache(). Don't do this in maintenance mode as Drupal may not be
+  // fully bootstrapped (i.e. during installation) in which case
+  // menu_get_item() is not available.
   if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) {
   if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) {
     $item = menu_get_item();
     $item = menu_get_item();
     if (!empty($item['include_file'])) {
     if (!empty($item['include_file'])) {
@@ -766,7 +812,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
     }
     }
     if (isset($form_definition['callback'])) {
     if (isset($form_definition['callback'])) {
       $callback = $form_definition['callback'];
       $callback = $form_definition['callback'];
-      $form_state['build_info']['base_form_id'] = $callback;
+      $form_state['build_info']['base_form_id'] = isset($form_definition['base_form_id']) ? $form_definition['base_form_id'] : $callback;
     }
     }
     // In case $form_state['wrapper_callback'] is not defined already, we also
     // In case $form_state['wrapper_callback'] is not defined already, we also
     // allow hook_forms() to define one.
     // allow hook_forms() to define one.
@@ -787,7 +833,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
   // the actual form builder function ($callback) expects. This allows for
   // the actual form builder function ($callback) expects. This allows for
   // pre-populating a form with common elements for certain forms, such as
   // 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().
   // 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'])) {
+  if (isset($form_state['wrapper_callback']) && is_callable($form_state['wrapper_callback'])) {
     $form = call_user_func_array($form_state['wrapper_callback'], $args);
     $form = call_user_func_array($form_state['wrapper_callback'], $args);
     // Put the prepopulated $form into $args.
     // Put the prepopulated $form into $args.
     $args[0] = $form;
     $args[0] = $form;
@@ -895,7 +941,7 @@ function drupal_process_form($form_id, &$form, &$form_state) {
         // after the batch is processed.
         // after the batch is processed.
       }
       }
 
 
-      // Set a flag to indicate the the form has been processed and executed.
+      // Set a flag to indicate that the form has been processed and executed.
       $form_state['executed'] = TRUE;
       $form_state['executed'] = TRUE;
 
 
       // Redirect the form based on values in $form_state.
       // Redirect the form based on values in $form_state.
@@ -976,7 +1022,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
   // @see drupal_build_form()
   // @see drupal_build_form()
   // @see drupal_rebuild_form()
   // @see drupal_rebuild_form()
   if (!isset($form['#build_id'])) {
   if (!isset($form['#build_id'])) {
-    $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
+    $form['#build_id'] = 'form-' . drupal_random_key();
   }
   }
   $form['form_build_id'] = array(
   $form['form_build_id'] = array(
     '#type' => 'hidden',
     '#type' => 'hidden',
@@ -1085,6 +1131,13 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
   drupal_alter($hooks, $form, $form_state, $form_id);
   drupal_alter($hooks, $form, $form_state, $form_id);
 }
 }
 
 
+/**
+ * Helper function to call form_set_error() if there is a token error.
+ */
+function _drupal_invalid_token_set_form_error() {
+  // Setting this error will cause the form to fail validation.
+  form_set_error('form_token', t('The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.'));
+}
 
 
 /**
 /**
  * Validates user-submitted form data in the $form_state array.
  * Validates user-submitted form data in the $form_state array.
@@ -1119,15 +1172,21 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
   }
   }
 
 
   // If the session token was set by drupal_prepare_form(), ensure that it
   // 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)));
+  // matches the current user's session. This is duplicate to code in
+  // form_builder() but left to protect any custom form handling code.
+  if (!empty($form['#token'])) {
+    if (!drupal_valid_token($form_state['values']['form_token'], $form['#token']) || !empty($form_state['invalid_token'])) {
+      _drupal_invalid_token_set_form_error();
+      // Ignore all submitted values.
+      $form_state['input'] = array();
+      $_POST = array();
+      // Make sure file uploads do not get processed.
+      $_FILES = array();
+      // Stop here and don't run any further validation handlers, because they
+      // could invoke non-safe operations which opens the door for CSRF
+      // vulnerabilities.
+      $validated_forms[$form_id] = TRUE;
+      return;
     }
     }
   }
   }
 
 
@@ -1382,10 +1441,12 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
       // length if it's a string, and the item count if it's an array.
       // 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
       // An unchecked checkbox has a #value of integer 0, different than string
       // '0', which could be a valid value.
       // '0', which could be a valid value.
-      $is_empty_multiple = (!count($elements['#value']));
+      $is_countable = is_array($elements['#value']) || $elements['#value'] instanceof Countable;
+      $is_empty_multiple = $is_countable && count($elements['#value']) == 0;
       $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0);
       $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0);
       $is_empty_value = ($elements['#value'] === 0);
       $is_empty_value = ($elements['#value'] === 0);
-      if ($is_empty_multiple || $is_empty_string || $is_empty_value) {
+      $is_empty_null = is_null($elements['#value']);
+      if ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_null) {
         // Although discouraged, a #title is not mandatory for form elements. In
         // Although discouraged, a #title is not mandatory for form elements. In
         // case there is no #title, we cannot set a form error message.
         // case there is no #title, we cannot set a form error message.
         // Instead of setting no #title, form constructors are encouraged to set
         // Instead of setting no #title, form constructors are encouraged to set
@@ -1778,6 +1839,23 @@ function form_builder($form_id, &$element, &$form_state) {
     // from the POST data is set and matches the current form_id.
     // 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)))) {
     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;
       $form_state['process_input'] = TRUE;
+      // If the session token was set by drupal_prepare_form(), ensure that it
+      // matches the current user's session.
+      $form_state['invalid_token'] = FALSE;
+      if (!empty($element['#token'])) {
+        if (empty($form_state['input']['form_token']) || !drupal_valid_token($form_state['input']['form_token'], $element['#token'])) {
+          // Set an early form error to block certain input processing since that
+          // opens the door for CSRF vulnerabilities.
+          _drupal_invalid_token_set_form_error();
+          // This value is checked in _form_builder_handle_input_element().
+          $form_state['invalid_token'] = TRUE;
+          // Ignore all submitted values.
+          $form_state['input'] = array();
+          $_POST = array();
+          // Make sure file uploads do not get processed.
+          $_FILES = array();
+        }
+      }
     }
     }
     else {
     else {
       $form_state['process_input'] = FALSE;
       $form_state['process_input'] = FALSE;
@@ -1881,6 +1959,18 @@ function form_builder($form_id, &$element, &$form_state) {
       $element['#attributes']['enctype'] = 'multipart/form-data';
       $element['#attributes']['enctype'] = 'multipart/form-data';
     }
     }
 
 
+    // Allow Ajax submissions to the form action to bypass verification. This is
+    // especially useful for multipart forms, which cannot be verified via a
+    // response header.
+    $element['#attached']['js'][] = array(
+      'type' => 'setting',
+      'data' => array(
+        'urlIsAjaxTrusted' => array(
+          $element['#action'] => TRUE,
+        ),
+      ),
+    );
+
     // If a form contains a single textfield, and the ENTER key is pressed
     // If a form contains a single textfield, and the ENTER key is pressed
     // within it, Internet Explorer submits the form with no POST data
     // within it, Internet Explorer submits the form with no POST data
     // identifying any submit button. Other browsers submit POST data as though
     // identifying any submit button. Other browsers submit POST data as though
@@ -1929,6 +2019,19 @@ function form_builder($form_id, &$element, &$form_state) {
  * Adds the #name and #value properties of an input element before rendering.
  * Adds the #name and #value properties of an input element before rendering.
  */
  */
 function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
 function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
+  static $safe_core_value_callbacks = array(
+    'form_type_token_value',
+    'form_type_textarea_value',
+    'form_type_textfield_value',
+    'form_type_checkbox_value',
+    'form_type_checkboxes_value',
+    'form_type_radios_value',
+    'form_type_password_confirm_value',
+    'form_type_select_value',
+    'form_type_tableselect_value',
+    'list_boolean_allowed_values_callback',
+  );
+
   if (!isset($element['#name'])) {
   if (!isset($element['#name'])) {
     $name = array_shift($element['#parents']);
     $name = array_shift($element['#parents']);
     $element['#name'] = $name;
     $element['#name'] = $name;
@@ -1978,7 +2081,7 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
   // #access=FALSE on an element usually allow access for some users, so forms
   // #access=FALSE on an element usually allow access for some users, so forms
   // submitted with drupal_form_submit() may bypass access restriction and be
   // submitted with drupal_form_submit() may bypass access restriction and be
   // treated as high-privilege users instead.
   // treated as high-privilege users instead.
-  $process_input = empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])));
+  $process_input = empty($element['#disabled']) && (($form_state['programmed'] && $form_state['programmed_bypass_access_check']) || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])));
 
 
   // Set the element's #value property.
   // Set the element's #value property.
   if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
   if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
@@ -2007,7 +2110,14 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
       // property, optionally filtered through $value_callback.
       // property, optionally filtered through $value_callback.
       if ($input_exists) {
       if ($input_exists) {
         if (function_exists($value_callback)) {
         if (function_exists($value_callback)) {
-          $element['#value'] = $value_callback($element, $input, $form_state);
+          // Skip all value callbacks except safe ones like text if the CSRF
+          // token was invalid.
+          if (empty($form_state['invalid_token']) || in_array($value_callback, $safe_core_value_callbacks)) {
+            $element['#value'] = $value_callback($element, $input, $form_state);
+          }
+          else {
+            $input = NULL;
+          }
         }
         }
         if (!isset($element['#value']) && isset($input)) {
         if (!isset($element['#value']) && isset($input)) {
           $element['#value'] = $input;
           $element['#value'] = $input;
@@ -2402,6 +2512,17 @@ function form_type_password_confirm_value($element, $input = FALSE) {
     $element += array('#default_value' => array());
     $element += array('#default_value' => array());
     return $element['#default_value'] + array('pass1' => '', 'pass2' => '');
     return $element['#default_value'] + array('pass1' => '', 'pass2' => '');
   }
   }
+  $value = array('pass1' => '', 'pass2' => '');
+  // Throw out all invalid array keys; we only allow pass1 and pass2.
+  foreach ($value as $allowed_key => $default) {
+    // These should be strings, but allow other scalars since they might be
+    // valid input in programmatic form submissions. Any nested array values
+    // are ignored.
+    if (isset($input[$allowed_key]) && is_scalar($input[$allowed_key])) {
+      $value[$allowed_key] = (string) $input[$allowed_key];
+    }
+  }
+  return $value;
 }
 }
 
 
 /**
 /**
@@ -2445,6 +2566,27 @@ function form_type_select_value($element, $input = FALSE) {
   }
   }
 }
 }
 
 
+/**
+ * Determines the value for a textarea form element.
+ *
+ * @param array $element
+ *   The form element whose value is being populated.
+ * @param mixed $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return string
+ *   The data that will appear in the $element_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_textarea_value($element, $input = FALSE) {
+  if ($input !== FALSE && $input !== NULL) {
+    // This should be a string, but allow other scalars since they might be
+    // valid input in programmatic form submissions.
+    return is_scalar($input) ? (string) $input : '';
+  }
+}
+
 /**
 /**
  * Determines the value for a textfield form element.
  * Determines the value for a textfield form element.
  *
  *
@@ -2460,9 +2602,12 @@ function form_type_select_value($element, $input = FALSE) {
  */
  */
 function form_type_textfield_value($element, $input = FALSE) {
 function form_type_textfield_value($element, $input = FALSE) {
   if ($input !== FALSE && $input !== NULL) {
   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);
+    // This should be a string, but allow other scalars since they might be
+    // valid input in programmatic form submissions.
+    if (!is_scalar($input)) {
+      $input = '';
+    }
+    return str_replace(array("\r", "\n"), '', (string) $input);
   }
   }
 }
 }
 
 
@@ -2578,8 +2723,8 @@ function _form_options_flatten($array) {
  *   - #required: (optional) Whether the user needs to select an option (TRUE)
  *   - #required: (optional) Whether the user needs to select an option (TRUE)
  *     or not (FALSE). Defaults to FALSE.
  *     or not (FALSE). Defaults to FALSE.
  *   - #empty_option: (optional) The label to show for the first default option.
  *   - #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.
+ *     By default, the label is automatically set to "- Select -" for a required
+ *     field and "- None -" for an optional field.
  *   - #empty_value: (optional) The value for the first default option, which is
  *   - #empty_value: (optional) The value for the first default option, which is
  *     used to determine whether the user submitted a value or not.
  *     used to determine whether the user submitted a value or not.
  *     - If #required is TRUE, this defaults to '' (an empty string).
  *     - If #required is TRUE, this defaults to '' (an empty string).
@@ -2650,17 +2795,43 @@ function theme_select($variables) {
 }
 }
 
 
 /**
 /**
- * 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.
+ * Converts an array of options into HTML, for use in select list form elements.
+ *
+ * This function calls itself recursively to obtain the values for each optgroup
+ * within the list of options and when the function encounters an object with
+ * an 'options' property inside $element['#options'].
+ *
+ * @param array $element
+ *   An associative array containing the following key-value pairs:
+ *   - #multiple: Optional Boolean indicating if the user may select more than
+ *     one item.
+ *   - #options: An associative array of options to render as HTML. Each array
+ *     value can be a string, an array, or an object with an 'option' property:
+ *     - A string or integer key whose value is a translated string is
+ *       interpreted as a single HTML option element. Do not use placeholders
+ *       that sanitize data: doing so will lead to double-escaping. Note that
+ *       the key will be visible in the HTML and could be modified by malicious
+ *       users, so don't put sensitive information in it.
+ *     - A translated string key whose value is an array indicates a group of
+ *       options. The translated string is used as the label attribute for the
+ *       optgroup. Do not use placeholders to sanitize data: doing so will lead
+ *       to double-escaping. The array should contain the options you wish to
+ *       group and should follow the syntax of $element['#options'].
+ *     - If the function encounters a string or integer key whose value is an
+ *       object with an 'option' property, the key is ignored, the contents of
+ *       the option property are interpreted as $element['#options'], and the
+ *       resulting HTML is added to the output.
+ *   - #value: Optional integer, string, or array representing which option(s)
+ *     to pre-select when the list is first displayed. The integer or string
+ *     must match the key of an option in the '#options' list. If '#multiple' is
+ *     TRUE, this can be an array of integers or strings.
+ * @param array|null $choices
+ *   (optional) Either an associative array of options in the same format as
+ *   $element['#options'] above, or NULL. This parameter is only used internally
+ *   and is not intended to be passed in to the initial function call.
+ *
+ * @return string
+ *   An HTML string of options and optgroups for use in a select form element.
  */
  */
 function form_select_options($element, $choices = NULL) {
 function form_select_options($element, $choices = NULL) {
   if (!isset($choices)) {
   if (!isset($choices)) {
@@ -2673,7 +2844,7 @@ function form_select_options($element, $choices = NULL) {
   $options = '';
   $options = '';
   foreach ($choices as $key => $choice) {
   foreach ($choices as $key => $choice) {
     if (is_array($choice)) {
     if (is_array($choice)) {
-      $options .= '<optgroup label="' . $key . '">';
+      $options .= '<optgroup label="' . check_plain($key) . '">';
       $options .= form_select_options($element, $choice);
       $options .= form_select_options($element, $choice);
       $options .= '</optgroup>';
       $options .= '</optgroup>';
     }
     }
@@ -2866,7 +3037,7 @@ function form_process_password_confirm($element) {
 function password_confirm_validate($element, &$element_state) {
 function password_confirm_validate($element, &$element_state) {
   $pass1 = trim($element['pass1']['#value']);
   $pass1 = trim($element['pass1']['#value']);
   $pass2 = trim($element['pass2']['#value']);
   $pass2 = trim($element['pass2']['#value']);
-  if (!empty($pass1) || !empty($pass2)) {
+  if (strlen($pass1) > 0 || strlen($pass2) > 0) {
     if (strcmp($pass1, $pass2)) {
     if (strcmp($pass1, $pass2)) {
       form_error($element, t('The specified passwords do not match.'));
       form_error($element, t('The specified passwords do not match.'));
     }
     }
@@ -3051,8 +3222,7 @@ function form_process_radios($element) {
  * @param $variables
  * @param $variables
  *   An associative array containing:
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *   - element: An associative array containing the properties of the element.
- *     Properties used: #title, #value, #return_value, #description, #required,
- *     #attributes, #checked.
+ *     Properties used: #id, #name, #attributes, #checked, #return_value.
  *
  *
  * @ingroup themeable
  * @ingroup themeable
  */
  */
@@ -3224,9 +3394,12 @@ function form_process_container($element, &$form_state) {
 /**
 /**
  * Returns HTML to wrap child elements in a container.
  * Returns HTML to wrap child elements in a container.
  *
  *
- * Used for grouped form items. Can also be used as a #theme_wrapper for any
+ * 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
  * renderable element, to surround it with a <div> and add attributes such as
- * classes or an HTML id.
+ * classes or an HTML ID.
+ *
+ * See the @link forms_api_reference.html Form API reference @endlink for more
+ * information on the #theme_wrappers render array property.
  *
  *
  * @param $variables
  * @param $variables
  *   An associative array containing:
  *   An associative array containing:
@@ -3237,6 +3410,8 @@ function form_process_container($element, &$form_state) {
  */
  */
 function theme_container($variables) {
 function theme_container($variables) {
   $element = $variables['element'];
   $element = $variables['element'];
+  // Ensure #attributes is set.
+  $element += array('#attributes' => array());
 
 
   // Special handling for form elements.
   // Special handling for form elements.
   if (isset($element['#array_parents'])) {
   if (isset($element['#array_parents'])) {
@@ -3379,6 +3554,7 @@ function form_process_tableselect($element) {
             '#return_value' => $key,
             '#return_value' => $key,
             '#default_value' => isset($value[$key]) ? $key : NULL,
             '#default_value' => isset($value[$key]) ? $key : NULL,
             '#attributes' => $element['#attributes'],
             '#attributes' => $element['#attributes'],
+            '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
           );
           );
         }
         }
         else {
         else {
@@ -3799,6 +3975,34 @@ function theme_hidden($variables) {
   return '<input' . drupal_attributes($element['#attributes']) . " />\n";
   return '<input' . drupal_attributes($element['#attributes']) . " />\n";
 }
 }
 
 
+/**
+ * Process function to prepare autocomplete data.
+ *
+ * @param $element
+ *   A textfield or other element with a #autocomplete_path.
+ *
+ * @return array
+ *   The processed form element.
+ */
+function form_process_autocomplete($element) {
+  $element['#autocomplete_input'] = array();
+  if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) {
+    $element['#autocomplete_input']['#id'] = $element['#id'] .'-autocomplete';
+    // Force autocomplete to use non-clean URLs since this protects against the
+    // browser interpreting the path plus search string as an actual file.
+    $current_clean_url = isset($GLOBALS['conf']['clean_url']) ? $GLOBALS['conf']['clean_url'] : NULL;
+    $GLOBALS['conf']['clean_url'] = 0;
+    // Force the script path to 'index.php', in case the server is not
+    // configured to find it automatically. Normally it is the responsibility
+    // of the site to do this themselves using hook_url_outbound_alter() (see
+    // url()) but since this code is forcing non-clean URLs on sites that don't
+    // normally use them, it is done here instead.
+    $element['#autocomplete_input']['#url_value'] = url($element['#autocomplete_path'], array('absolute' => TRUE, 'script' => 'index.php'));
+    $GLOBALS['conf']['clean_url'] = $current_clean_url;
+  }
+  return $element;
+}
+
 /**
 /**
  * Returns HTML for a textfield form element.
  * Returns HTML for a textfield form element.
  *
  *
@@ -3817,14 +4021,14 @@ function theme_textfield($variables) {
   _form_set_class($element, array('form-text'));
   _form_set_class($element, array('form-text'));
 
 
   $extra = '';
   $extra = '';
-  if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) {
+  if ($element['#autocomplete_path'] && !empty($element['#autocomplete_input'])) {
     drupal_add_library('system', 'drupal.autocomplete');
     drupal_add_library('system', 'drupal.autocomplete');
     $element['#attributes']['class'][] = 'form-autocomplete';
     $element['#attributes']['class'][] = 'form-autocomplete';
 
 
     $attributes = array();
     $attributes = array();
     $attributes['type'] = 'hidden';
     $attributes['type'] = 'hidden';
-    $attributes['id'] = $element['#attributes']['id'] . '-autocomplete';
-    $attributes['value'] = url($element['#autocomplete_path'], array('absolute' => TRUE));
+    $attributes['id'] = $element['#autocomplete_input']['#id'];
+    $attributes['value'] = $element['#autocomplete_input']['#url_value'];
     $attributes['disabled'] = 'disabled';
     $attributes['disabled'] = 'disabled';
     $attributes['class'][] = 'autocomplete';
     $attributes['class'][] = 'autocomplete';
     $extra = '<input' . drupal_attributes($attributes) . ' />';
     $extra = '<input' . drupal_attributes($attributes) . ' />';
@@ -4244,7 +4448,7 @@ function element_validate_number($element, &$form_state) {
  * returns any user input in the 'results' or 'message' keys of $context,
  * returns any user input in the 'results' or 'message' keys of $context,
  * it must also sanitize them first.
  * it must also sanitize them first.
  *
  *
- * Sample batch operations:
+ * Sample callback_batch_operation():
  * @code
  * @code
  * // Simple and artificial: load a node of a given type for a given user
  * // Simple and artificial: load a node of a given type for a given user
  * function my_function_1($uid, $type, &$context) {
  * function my_function_1($uid, $type, &$context) {
@@ -4296,9 +4500,9 @@ function element_validate_number($element, &$form_state) {
  * }
  * }
  * @endcode
  * @endcode
  *
  *
- * Sample 'finished' callback:
+ * Sample callback_batch_finished():
  * @code
  * @code
- * function batch_test_finished($success, $results, $operations) {
+ * function my_finished_callback($success, $results, $operations) {
  *   // The 'success' parameter means no fatal PHP errors were detected. All
  *   // The 'success' parameter means no fatal PHP errors were detected. All
  *   // other error management should be handled using 'results'.
  *   // other error management should be handled using 'results'.
  *   if ($success) {
  *   if ($success) {
@@ -4335,12 +4539,14 @@ function element_validate_number($element, &$form_state) {
  * @param $batch_definition
  * @param $batch_definition
  *   An associative array defining the batch, with the following elements (all
  *   An associative array defining the batch, with the following elements (all
  *   are optional except as noted):
  *   are optional except as noted):
- *   - operations: (required) Array of function calls to be performed.
+ *   - operations: (required) Array of operations to be performed, where each
+ *     item is an array consisting of the name of an implementation of
+ *     callback_batch_operation() and an array of parameter.
  *     Example:
  *     Example:
  *     @code
  *     @code
  *     array(
  *     array(
- *       array('my_function_1', array($arg1)),
- *       array('my_function_2', array($arg2_1, $arg2_2)),
+ *       array('callback_batch_operation_1', array($arg1)),
+ *       array('callback_batch_operation_2', array($arg2_1, $arg2_2)),
  *     )
  *     )
  *     @endcode
  *     @endcode
  *   - title: A safe, translated string to use as the title for the progress
  *   - title: A safe, translated string to use as the title for the progress
@@ -4352,10 +4558,10 @@ function element_validate_number($element, &$form_state) {
  *     @elapsed. Defaults to t('Completed @current of @total.').
  *     @elapsed. Defaults to t('Completed @current of @total.').
  *   - error_message: Message displayed if an error occurred while processing
  *   - error_message: Message displayed if an error occurred while processing
  *     the batch. Defaults to t('An error has occurred.').
  *     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.
+ *   - finished: Name of an implementation of callback_batch_finished(). This is
+ *     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
  *   - file: Path to the file containing the definitions of the 'operations' and
  *     'finished' functions, for instance if they don't reside in the main
  *     '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
  *     .module file. The path should be relative to base_path(), and thus should

+ 44 - 4
includes/install.core.inc

@@ -362,7 +362,8 @@ function install_run_tasks(&$install_state) {
  * Runs an individual installation task.
  * Runs an individual installation task.
  *
  *
  * @param $task
  * @param $task
- *   An array of information about the task to be run.
+ *   An array of information about the task to be run as returned by
+ *   hook_install_tasks().
  * @param $install_state
  * @param $install_state
  *   An array of information about the current installation state. This is
  *   An array of information about the current installation state. This is
  *   passed in by reference so that it can be modified by the task.
  *   passed in by reference so that it can be modified by the task.
@@ -478,11 +479,15 @@ function install_run_task($task, &$install_state) {
  * the page request evolves (for example, if an installation profile hasn't
  * the page request evolves (for example, if an installation profile hasn't
  * been selected yet, we don't yet know which profile tasks need to be run).
  * been selected yet, we don't yet know which profile tasks need to be run).
  *
  *
+ * You can override this using hook_install_tasks() or
+ * hook_install_tasks_alter().
+ *
  * @param $install_state
  * @param $install_state
  *   An array of information about the current installation state.
  *   An array of information about the current installation state.
  *
  *
  * @return
  * @return
- *   A list of tasks to be performed, with associated metadata.
+ *   A list of tasks to be performed, with associated metadata as returned by
+ *   hook_install_tasks().
  */
  */
 function install_tasks_to_perform($install_state) {
 function install_tasks_to_perform($install_state) {
   // Start with a list of all currently available tasks.
   // Start with a list of all currently available tasks.
@@ -692,6 +697,21 @@ function install_full_redirect_url($install_state) {
  */
  */
 function install_display_output($output, $install_state) {
 function install_display_output($output, $install_state) {
   drupal_page_header();
   drupal_page_header();
+
+  // Prevent install.php from being indexed when installed in a sub folder.
+  // robots.txt rules are not read if the site is within domain.com/subfolder
+  // resulting in /subfolder/install.php being found through search engines.
+  // When settings.php is writeable this can be used via an external database
+  // leading a malicious user to gain php access to the server.
+  $noindex_meta_tag = array(
+    '#tag' => 'meta',
+    '#attributes' => array(
+      'name' => 'robots',
+      'content' => 'noindex, nofollow',
+    ),
+  );
+  drupal_add_html_head($noindex_meta_tag, 'install_meta_robots');
+
   // Only show the task list if there is an active task; otherwise, the page
   // Only show the task list if there is an active task; otherwise, the page
   // request has ended before tasks have even been started, so there is nothing
   // request has ended before tasks have even been started, so there is nothing
   // meaningful to show.
   // meaningful to show.
@@ -766,6 +786,15 @@ function install_system_module(&$install_state) {
   // Install system.module.
   // Install system.module.
   drupal_install_system();
   drupal_install_system();
 
 
+  // Call file_ensure_htaccess() to ensure that all of Drupal's standard
+  // directories (e.g., the public and private files directories) have
+  // appropriate .htaccess files. These directories will have already been
+  // created by this point in the installer, since Drupal creates them during
+  // the install_verify_requirements() task. Note that we cannot call
+  // file_ensure_htaccess() any earlier than this, since it relies on
+  // system.module in order to work.
+  file_ensure_htaccess();
+
   // Enable the user module so that sessions can be recorded during the
   // Enable the user module so that sessions can be recorded during the
   // upcoming bootstrap step.
   // upcoming bootstrap step.
   module_enable(array('user'), FALSE);
   module_enable(array('user'), FALSE);
@@ -780,6 +809,13 @@ function install_system_module(&$install_state) {
 
 
   variable_set('install_profile_modules', array_diff($modules, array('system')));
   variable_set('install_profile_modules', array_diff($modules, array('system')));
   $install_state['database_tables_exist'] = TRUE;
   $install_state['database_tables_exist'] = TRUE;
+
+  // Prevent the hook_requirements() check from telling us to convert the
+  // database to utf8mb4.
+  $connection = Database::getConnection();
+  if ($connection->utf8mb4IsConfigurable() && $connection->utf8mb4IsActive()) {
+    variable_set('drupal_all_databases_are_utf8mb4', TRUE);
+  }
 }
 }
 
 
 /**
 /**
@@ -981,7 +1017,7 @@ function install_settings_form_submit($form, &$form_state) {
     'required' => TRUE,
     'required' => TRUE,
   );
   );
   $settings['drupal_hash_salt'] = array(
   $settings['drupal_hash_salt'] = array(
-    'value'    => drupal_hash_base64(drupal_random_bytes(55)),
+    'value'    => drupal_random_key(),
     'required' => TRUE,
     'required' => TRUE,
   );
   );
   drupal_rewrite_settings($settings);
   drupal_rewrite_settings($settings);
@@ -1561,7 +1597,9 @@ function install_finished(&$install_state) {
 }
 }
 
 
 /**
 /**
- * Batch callback for batch installation of modules.
+ * Implements callback_batch_operation().
+ *
+ * Performs batch installation of modules.
  */
  */
 function _install_module_batch($module, $module_name, &$context) {
 function _install_module_batch($module, $module_name, &$context) {
   // Install and enable the module right away, so that the module will be
   // Install and enable the module right away, so that the module will be
@@ -1574,6 +1612,8 @@ function _install_module_batch($module, $module_name, &$context) {
 }
 }
 
 
 /**
 /**
+ * Implements callback_batch_finished().
+ *
  * 'Finished' callback for module installation batch.
  * 'Finished' callback for module installation batch.
  */
  */
 function _install_profile_modules_finished($success, $results, $operations) {
 function _install_profile_modules_finished($success, $results, $operations) {

+ 31 - 17
includes/install.inc

@@ -420,7 +420,7 @@ abstract class DatabaseTasks {
       }
       }
     }
     }
     if (!empty($message)) {
     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;
+      $message = 'Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="http://drupal.org/getting-started/install">installation handbook</a>, or contact your hosting provider.' . $message;
       throw new DatabaseTaskException($message);
       throw new DatabaseTaskException($message);
     }
     }
   }
   }
@@ -653,6 +653,13 @@ function drupal_rewrite_settings($settings = array(), $prefix = '') {
     if ($fp && fwrite($fp, $buffer) === FALSE) {
     if ($fp && fwrite($fp, $buffer) === FALSE) {
       throw new Exception(st('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file)));
       throw new Exception(st('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file)));
     }
     }
+    else {
+      // The existing settings.php file might have been included already. In
+      // case an opcode cache is enabled, the rewritten contents of the file
+      // will not be reflected in this process. Ensure to invalidate the file
+      // in case an opcode cache is enabled.
+      drupal_clear_opcode_cache(DRUPAL_ROOT . '/' . $settings_file);
+    }
   }
   }
   else {
   else {
     throw new Exception(st('Failed to open %settings. Verify the file permissions.', array('%settings' => $default_settings)));
     throw new Exception(st('Failed to open %settings. Verify the file permissions.', array('%settings' => $default_settings)));
@@ -741,20 +748,28 @@ function drupal_install_system() {
 }
 }
 
 
 /**
 /**
- * Uninstalls a given list of modules.
+ * Uninstalls a given list of disabled 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.
+ * @param string[] $module_list
+ *   The modules to uninstall. It is the caller's responsibility to ensure that
+ *   all modules in this list have already been disabled before this function
+ *   is called.
+ * @param bool $uninstall_dependents
+ *   (optional) 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.
+ *   Defaults to TRUE.
  *
  *
- * @return
- *   FALSE if one or more dependent modules are missing from the list, TRUE
- *   otherwise.
+ * @return bool
+ *   Returns TRUE if the operation succeeds or FALSE if it aborts due to an
+ *   unsafe condition, namely, $uninstall_dependents is TRUE and a module in
+ *   $module_list has dependents which are not already uninstalled and not also
+ *   included in $module_list).
+ *
+ * @see module_disable()
+ * @see module_enable()
  */
  */
 function drupal_uninstall_modules($module_list = array(), $uninstall_dependents = TRUE) {
 function drupal_uninstall_modules($module_list = array(), $uninstall_dependents = TRUE) {
   if ($uninstall_dependents) {
   if ($uninstall_dependents) {
@@ -764,9 +779,9 @@ function drupal_uninstall_modules($module_list = array(), $uninstall_dependents
     $module_list = array_flip(array_values($module_list));
     $module_list = array_flip(array_values($module_list));
 
 
     $profile = drupal_get_profile();
     $profile = drupal_get_profile();
-    while (list($module) = each($module_list)) {
+    foreach (array_keys($module_list) as $module) {
       if (!isset($module_data[$module]) || drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) {
       if (!isset($module_data[$module]) || drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) {
-        // This module doesn't exist or is already uninstalled, skip it.
+        // This module doesn't exist or is already uninstalled. Skip it.
         unset($module_list[$module]);
         unset($module_list[$module]);
         continue;
         continue;
       }
       }
@@ -799,7 +814,7 @@ function drupal_uninstall_modules($module_list = array(), $uninstall_dependents
   }
   }
 
 
   if (!empty($module_list)) {
   if (!empty($module_list)) {
-    // Call hook_module_uninstall to let other modules act
+    // Let other modules react.
     module_invoke_all('modules_uninstalled', $module_list);
     module_invoke_all('modules_uninstalled', $module_list);
   }
   }
 
 
@@ -1127,7 +1142,6 @@ function st($string, array $args = array(), array $options = array()) {
     }
     }
   }
   }
 
 
-  require_once DRUPAL_ROOT . '/includes/theme.inc';
   // Transform arguments before inserting them
   // Transform arguments before inserting them
   foreach ($args as $key => $value) {
   foreach ($args as $key => $value) {
     switch ($key[0]) {
     switch ($key[0]) {

+ 4 - 1
includes/iso.inc

@@ -53,6 +53,7 @@ function _country_get_predefined_list() {
     'BM' => $t('Bermuda'),
     'BM' => $t('Bermuda'),
     'BN' => $t('Brunei'),
     'BN' => $t('Brunei'),
     'BO' => $t('Bolivia'),
     'BO' => $t('Bolivia'),
+    'BQ' => $t('Caribbean Netherlands'),
     'BR' => $t('Brazil'),
     'BR' => $t('Brazil'),
     'BS' => $t('Bahamas'),
     'BS' => $t('Bahamas'),
     'BT' => $t('Bhutan'),
     'BT' => $t('Bhutan'),
@@ -74,8 +75,8 @@ function _country_get_predefined_list() {
     'CO' => $t('Colombia'),
     'CO' => $t('Colombia'),
     'CR' => $t('Costa Rica'),
     'CR' => $t('Costa Rica'),
     'CU' => $t('Cuba'),
     'CU' => $t('Cuba'),
-    'CW' => $t('Curaçao'),
     'CV' => $t('Cape Verde'),
     'CV' => $t('Cape Verde'),
+    'CW' => $t('Curaçao'),
     'CX' => $t('Christmas Island'),
     'CX' => $t('Christmas Island'),
     'CY' => $t('Cyprus'),
     'CY' => $t('Cyprus'),
     'CZ' => $t('Czech Republic'),
     'CZ' => $t('Czech Republic'),
@@ -230,8 +231,10 @@ function _country_get_predefined_list() {
     'SN' => $t('Senegal'),
     'SN' => $t('Senegal'),
     'SO' => $t('Somalia'),
     'SO' => $t('Somalia'),
     'SR' => $t('Suriname'),
     'SR' => $t('Suriname'),
+    'SS' => $t('South Sudan'),
     'ST' => $t('Sao Tome and Principe'),
     'ST' => $t('Sao Tome and Principe'),
     'SV' => $t('El Salvador'),
     'SV' => $t('El Salvador'),
+    'SX' => $t('Sint Maarten'),
     'SY' => $t('Syria'),
     'SY' => $t('Syria'),
     'SZ' => $t('Swaziland'),
     'SZ' => $t('Swaziland'),
     'TC' => $t('Turks and Caicos Islands'),
     'TC' => $t('Turks and Caicos Islands'),

+ 3 - 4
includes/language.inc

@@ -78,7 +78,7 @@ define('LANGUAGE_NEGOTIATION_DEFAULT', 'language-default');
  * function mymodule_language_negotiation_info_alter(&$negotiation_info) {
  * function mymodule_language_negotiation_info_alter(&$negotiation_info) {
  *   // Replace the core function with our own function.
  *   // Replace the core function with our own function.
  *   module_load_include('language', 'inc', 'language.negotiation');
  *   module_load_include('language', 'inc', 'language.negotiation');
- *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['callbacks']['negotiation'] = 'mymodule_from_url';
+ *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['callbacks']['language'] = 'mymodule_from_url';
  *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['file'] = drupal_get_path('module', 'mymodule') . '/mymodule.module';
  *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['file'] = drupal_get_path('module', 'mymodule') . '/mymodule.module';
  * }
  * }
  *
  *
@@ -94,7 +94,6 @@ define('LANGUAGE_NEGOTIATION_DEFAULT', 'language-default');
  *   }
  *   }
  *   return $langcode;
  *   return $langcode;
  * }
  * }
- * ?>
  * @endcode
  * @endcode
  *
  *
  * For more information, see
  * For more information, see
@@ -298,7 +297,7 @@ function language_negotiation_get_switch_links($type, $path) {
       // Add support for WCAG 2.0's Language of Parts to add language identifiers.
       // 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
       // http://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html
       foreach ($result as $langcode => $link) {
       foreach ($result as $langcode => $link) {
-        $result[$langcode]['attributes']['lang'] = $langcode;
+        $result[$langcode]['attributes']['xml:lang'] = $langcode;
       }
       }
 
 
       if (!empty($result)) {
       if (!empty($result)) {
@@ -314,7 +313,7 @@ function language_negotiation_get_switch_links($type, $path) {
 }
 }
 
 
 /**
 /**
- * Removes any unused language negotation providers from the configuration.
+ * Removes any unused language negotiation providers from the configuration.
  */
  */
 function language_negotiation_purge() {
 function language_negotiation_purge() {
   // Ensure that we are getting the defined language negotiation information. An
   // Ensure that we are getting the defined language negotiation information. An

+ 58 - 8
includes/locale.inc

@@ -398,7 +398,7 @@ function locale_language_switcher_session($type, $path) {
       $links[$langcode]['query'][$param] = $langcode;
       $links[$langcode]['query'][$param] = $langcode;
     }
     }
     else {
     else {
-      $links[$langcode]['attributes']['class'][] = ' session-active';
+      $links[$langcode]['attributes']['class'][] = 'session-active';
     }
     }
   }
   }
 
 
@@ -435,6 +435,13 @@ function locale_language_url_rewrite_url(&$path, &$options) {
     switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
     switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
       case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
       case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
         if ($options['language']->domain) {
         if ($options['language']->domain) {
+          // Save the original base URL. If it contains a port, we need to
+          // retain it below.
+          if (!empty($options['base_url'])) {
+            // The colon in the URL scheme messes up the port checking below.
+            $normalized_base_url = str_replace(array('https://', 'http://'), '', $options['base_url']);
+          }
+
           // Ask for an absolute URL with our modified base_url.
           // Ask for an absolute URL with our modified base_url.
           global $is_https;
           global $is_https;
           $url_scheme = ($is_https) ? 'https://' : 'http://';
           $url_scheme = ($is_https) ? 'https://' : 'http://';
@@ -449,6 +456,19 @@ function locale_language_url_rewrite_url(&$path, &$options) {
 
 
           // Apply the appropriate protocol to the URL.
           // Apply the appropriate protocol to the URL.
           $options['base_url'] = $url_scheme . $host;
           $options['base_url'] = $url_scheme . $host;
+
+          // In case either the original base URL or the HTTP host contains a
+          // port, retain it.
+          $http_host = $_SERVER['HTTP_HOST'];
+          if (isset($normalized_base_url) && strpos($normalized_base_url, ':') !== FALSE) {
+            list($host, $port) = explode(':', $normalized_base_url);
+            $options['base_url'] .= ':' . $port;
+          }
+          elseif (strpos($http_host, ':') !== FALSE) {
+            list($host, $port) = explode(':', $http_host);
+            $options['base_url'] .= ':' . $port;
+          }
+
           if (isset($options['https']) && variable_get('https', FALSE)) {
           if (isset($options['https']) && variable_get('https', FALSE)) {
             if ($options['https'] === TRUE) {
             if ($options['https'] === TRUE) {
               $options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
               $options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
@@ -523,6 +543,22 @@ function locale_language_url_rewrite_session(&$path, &$options) {
  * possible attack vector (img).
  * possible attack vector (img).
  */
  */
 function locale_string_is_safe($string) {
 function locale_string_is_safe($string) {
+  // Some strings have tokens in them. For tokens in the first part of href or
+  // src HTML attributes, filter_xss() removes part of the token, the part
+  // before the first colon.  filter_xss() assumes it could be an attempt to
+  // inject javascript. When filter_xss() removes part of tokens, it causes the
+  // string to not be translatable when it should be translatable. See
+  // LocaleStringIsSafeTest::testLocaleStringIsSafe().
+  //
+  // We can recognize tokens since they are wrapped with brackets and are only
+  // composed of alphanumeric characters, colon, underscore, and dashes. We can
+  // be sure these strings are safe to strip out before the string is checked in
+  // filter_xss() because no dangerous javascript will match that pattern.
+  //
+  // @todo Do not strip out the token. Fix filter_xss() to not incorrectly
+  //   alter the string. https://www.drupal.org/node/2372127
+  $string = preg_replace('/\[[a-z0-9_-]+(:[a-z0-9_-]+)+\]/i', '', $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')));
   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')));
 }
 }
 
 
@@ -631,9 +667,6 @@ function locale_add_language($langcode, $name = NULL, $native = NULL, $direction
  *   translations).
  *   translations).
  */
  */
 function _locale_import_po($file, $langcode, $mode, $group = NULL) {
 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.
   // 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()) {
   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');
     drupal_set_message(t('The language selected for import is not supported.'), 'error');
@@ -717,6 +750,12 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group =
   $lineno = 0;
   $lineno = 0;
 
 
   while (!feof($fd)) {
   while (!feof($fd)) {
+    // Refresh the time limit every 10 parsed rows to ensure there is always
+    // enough time to import the data for large PO files.
+    if (!($lineno % 10)) {
+      drupal_set_time_limit(30);
+    }
+
     // A line should not be longer than 10 * 1024.
     // A line should not be longer than 10 * 1024.
     $line = fgets($fd, 10 * 1024);
     $line = fgets($fd, 10 * 1024);
 
 
@@ -1931,7 +1970,7 @@ function _locale_translate_seek() {
       $groups[$string['group']],
       $groups[$string['group']],
       array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
       array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
       $string['context'],
       $string['context'],
-      array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'),
+      array('data' => _locale_translate_language_list($string, $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('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')),
       array('data' => l(t('delete'), "admin/config/regional/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')),
     );
     );
@@ -2126,16 +2165,21 @@ function _locale_rebuild_js($langcode = NULL) {
 /**
 /**
  * List languages in search result table
  * List languages in search result table
  */
  */
-function _locale_translate_language_list($translation, $limit_language) {
+function _locale_translate_language_list($string, $limit_language) {
   // Add CSS.
   // Add CSS.
   drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
   drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
 
 
+  // Include both translated and not yet translated target languages in the
+  // list. The source language is English for built-in strings and the default
+  // language for other strings.
   $languages = language_list();
   $languages = language_list();
-  unset($languages['en']);
+  $default = language_default();
+  $omit = $string['group'] == 'default' ? 'en' : $default->language;
+  unset($languages[$omit]);
   $output = '';
   $output = '';
   foreach ($languages as $langcode => $language) {
   foreach ($languages as $langcode => $language) {
     if (!$limit_language || $limit_language == $langcode) {
     if (!$limit_language || $limit_language == $langcode) {
-      $output .= (!empty($translation[$langcode])) ? $langcode . ' ' : "<em class=\"locale-untranslated\">$langcode</em> ";
+      $output .= (!empty($string['languages'][$langcode])) ? $langcode . ' ' : "<em class=\"locale-untranslated\">$langcode</em> ";
     }
     }
   }
   }
 
 
@@ -2301,6 +2345,8 @@ function _locale_batch_build($files, $finished = NULL, $components = array()) {
 }
 }
 
 
 /**
 /**
+ * Implements callback_batch_operation().
+ *
  * Perform interface translation import as a batch step.
  * Perform interface translation import as a batch step.
  *
  *
  * @param $filepath
  * @param $filepath
@@ -2319,6 +2365,8 @@ function _locale_batch_import($filepath, &$context) {
 }
 }
 
 
 /**
 /**
+ * Implements callback_batch_finished().
+ *
  * Finished callback of system page locale import batch.
  * Finished callback of system page locale import batch.
  * Inform the user of translation files imported.
  * Inform the user of translation files imported.
  */
  */
@@ -2329,6 +2377,8 @@ function _locale_batch_system_finished($success, $results) {
 }
 }
 
 
 /**
 /**
+ * Implements callback_batch_finished().
+ *
  * Finished callback of language addition locale import batch.
  * Finished callback of language addition locale import batch.
  * Inform the user of translation files imported.
  * Inform the user of translation files imported.
  */
  */

+ 1 - 1
includes/lock.inc

@@ -92,7 +92,7 @@ function _lock_id() {
  * Acquire (or renew) a lock, but do not block if it fails.
  * Acquire (or renew) a lock, but do not block if it fails.
  *
  *
  * @param $name
  * @param $name
- *   The name of the lock.
+ *   The name of the lock. Limit of name's length is 255 characters.
  * @param $timeout
  * @param $timeout
  *   A number of seconds (float) before the lock expires (minimum of 0.001).
  *   A number of seconds (float) before the lock expires (minimum of 0.001).
  *
  *

+ 9 - 8
includes/mail.inc

@@ -10,7 +10,7 @@
  *
  *
  * $conf['mail_line_endings'] will override this setting.
  * $conf['mail_line_endings'] will override this setting.
  */
  */
-define('MAIL_LINE_ENDINGS', isset($_SERVER['WINDIR']) || strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE ? "\r\n" : "\n");
+define('MAIL_LINE_ENDINGS', isset($_SERVER['WINDIR']) || (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE) ? "\r\n" : "\n");
 
 
 /**
 /**
  * Composes and optionally sends an e-mail message.
  * Composes and optionally sends an e-mail message.
@@ -339,13 +339,13 @@ interface MailSystemInterface {
  *
  *
  * We deliberately use LF rather than CRLF, see drupal_mail().
  * We deliberately use LF rather than CRLF, see drupal_mail().
  *
  *
- * @param $text
+ * @param string $text
  *   The plain text to process.
  *   The plain text to process.
- * @param $indent (optional)
+ * @param string $indent (optional)
  *   A string to indent the text with. Only '>' characters are repeated on
  *   A string to indent the text with. Only '>' characters are repeated on
  *   subsequent wrapped lines. Others are replaced by spaces.
  *   subsequent wrapped lines. Others are replaced by spaces.
  *
  *
- * @return
+ * @return string
  *   The content of the email as a string with formatting applied.
  *   The content of the email as a string with formatting applied.
  */
  */
 function drupal_wrap_mail($text, $indent = '') {
 function drupal_wrap_mail($text, $indent = '') {
@@ -356,8 +356,9 @@ function drupal_wrap_mail($text, $indent = '') {
   $soft = strpos($clean_indent, ' ') === FALSE;
   $soft = strpos($clean_indent, ' ') === FALSE;
   // Check if the string has line breaks.
   // Check if the string has line breaks.
   if (strpos($text, "\n") !== FALSE) {
   if (strpos($text, "\n") !== FALSE) {
-    // Remove trailing spaces to make existing breaks hard.
-    $text = preg_replace('/ +\n/m', "\n", $text);
+    // Remove trailing spaces to make existing breaks hard, but leave signature
+    // marker untouched (RFC 3676, Section 4.3).
+    $text = preg_replace('/(?(?<!^--) +\n|  +\n)/m', "\n", $text);
     // Wrap each line at the needed width.
     // Wrap each line at the needed width.
     $lines = explode("\n", $text);
     $lines = explode("\n", $text);
     array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent)));
     array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent)));
@@ -563,9 +564,9 @@ function drupal_html_to_text($string, $allowed_tags = NULL) {
  */
  */
 function _drupal_wrap_mail_line(&$line, $key, $values) {
 function _drupal_wrap_mail_line(&$line, $key, $values) {
   // Use soft-breaks only for purely quoted or unindented text.
   // Use soft-breaks only for purely quoted or unindented text.
-  $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? "  \n" : "\n");
+  $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n");
   // Break really long words at the maximum width allowed.
   // Break really long words at the maximum width allowed.
-  $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n");
+  $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n", TRUE);
 }
 }
 
 
 /**
 /**

+ 104 - 29
includes/menu.inc

@@ -229,12 +229,20 @@ define('MENU_CONTEXT_INLINE', 0x0002);
 define('MENU_FOUND', 1);
 define('MENU_FOUND', 1);
 
 
 /**
 /**
- * Internal menu status code -- Menu item was not found.
+ * Menu status code -- Not found.
+ *
+ * This can be used as the return value from a page callback, although it is
+ * preferable to use a load function to accomplish this; see the hook_menu()
+ * documentation for details.
  */
  */
 define('MENU_NOT_FOUND', 2);
 define('MENU_NOT_FOUND', 2);
 
 
 /**
 /**
- * Internal menu status code -- Menu item access is denied.
+ * Menu status code -- Access denied.
+ *
+ * This can be used as the return value from a page callback, although it is
+ * preferable to use an access callback to accomplish this; see the hook_menu()
+ * documentation for details.
  */
  */
 define('MENU_ACCESS_DENIED', 3);
 define('MENU_ACCESS_DENIED', 3);
 
 
@@ -309,7 +317,7 @@ define('MENU_PREFERRED_LINK', '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91');
  * actually exists. This list of 'masks' is built in menu_rebuild().
  * actually exists. This list of 'masks' is built in menu_rebuild().
  *
  *
  * @param $parts
  * @param $parts
- *   An array of path parts, for the above example
+ *   An array of path parts; for the above example, 
  *   array('node', '12345', 'edit').
  *   array('node', '12345', 'edit').
  *
  *
  * @return
  * @return
@@ -430,8 +438,8 @@ function menu_set_item($path, $router_item) {
  * Gets a router item.
  * Gets a router item.
  *
  *
  * @param $path
  * @param $path
- *   The path, for example node/5. The function will find the corresponding
- *   node/% item and return that.
+ *   The path; for example, 'node/5'. The function will find the corresponding
+ *   node/% item and return that. Defaults to the current path.
  * @param $router_item
  * @param $router_item
  *   Internal use only.
  *   Internal use only.
  *
  *
@@ -456,7 +464,9 @@ function menu_get_item($path = NULL, $router_item = NULL) {
     // Rebuild if we know it's needed, or if the menu masks are missing which
     // 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.
     // occurs rarely, likely due to a race condition of multiple rebuilds.
     if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
     if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
-      menu_rebuild();
+      if (_menu_check_rebuild()) {
+        menu_rebuild();
+      }
     }
     }
     $original_map = arg(NULL, $path);
     $original_map = arg(NULL, $path);
 
 
@@ -542,7 +552,7 @@ function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
  * @param $item
  * @param $item
  *   A menu router or menu link item
  *   A menu router or menu link item
  * @param $map
  * @param $map
- *   An array of path arguments (ex: array('node', '5'))
+ *   An array of path arguments; for example, array('node', '5').
  *
  *
  * @return
  * @return
  *   Returns TRUE for success, FALSE if an object cannot be loaded.
  *   Returns TRUE for success, FALSE if an object cannot be loaded.
@@ -566,7 +576,8 @@ function _menu_load_objects(&$item, &$map) {
           // 'load arguments' in the hook_menu() entry, but they need
           // 'load arguments' in the hook_menu() entry, but they need
           // some processing. In this case the $function is the key to the
           // some processing. In this case the $function is the key to the
           // load_function array, and the value is the list of arguments.
           // load_function array, and the value is the list of arguments.
-          list($function, $args) = each($function);
+          $args = current($function);
+          $function = key($function);
           $load_functions[$index] = $function;
           $load_functions[$index] = $function;
 
 
           // Some arguments are placeholders for dynamic items to process.
           // Some arguments are placeholders for dynamic items to process.
@@ -612,7 +623,7 @@ function _menu_load_objects(&$item, &$map) {
  * @param $item
  * @param $item
  *   A menu router or menu link item
  *   A menu router or menu link item
  * @param $map
  * @param $map
- *   An array of path arguments (ex: array('node', '5'))
+ *   An array of path arguments; for example, array('node', '5').
  *
  *
  * @return
  * @return
  *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
  *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
@@ -738,7 +749,7 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
  * @param $router_item
  * @param $router_item
  *   A menu router item
  *   A menu router item
  * @param $map
  * @param $map
- *   An array of path arguments (ex: array('node', '5'))
+ *   An array of path arguments; for example, array('node', '5').
  * @param $to_arg
  * @param $to_arg
  *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
  *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
  *   path from the menu table, for example tabs.
  *   path from the menu table, for example tabs.
@@ -801,9 +812,9 @@ function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
  * Translates the path elements in the map using any to_arg helper function.
  * Translates the path elements in the map using any to_arg helper function.
  *
  *
  * @param $map
  * @param $map
- *   An array of path arguments (ex: array('node', '5'))
+ *   An array of path arguments; for example, array('node', '5').
  * @param $to_arg_functions
  * @param $to_arg_functions
- *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
+ *   An array of helper functions; for example, array(2 => 'menu_tail_to_arg').
  *
  *
  * @see hook_menu()
  * @see hook_menu()
  */
  */
@@ -1000,7 +1011,7 @@ function menu_tree($menu_name) {
 }
 }
 
 
 /**
 /**
- * Returns a rendered menu tree.
+ * Returns an output structure for rendering a menu tree.
  *
  *
  * The menu item's LI element is given one of the following classes:
  * The menu item's LI element is given one of the following classes:
  * - expanded: The menu item is showing its submenu.
  * - expanded: The menu item is showing its submenu.
@@ -1485,7 +1496,7 @@ function menu_tree_collect_node_links(&$tree, &$node_links) {
  *   menu_tree_collect_node_links().
  *   menu_tree_collect_node_links().
  */
  */
 function menu_tree_check_access(&$tree, $node_links = array()) {
 function menu_tree_check_access(&$tree, $node_links = array()) {
-  if ($node_links) {
+  if ($node_links && (user_access('access content') || user_access('bypass node access'))) {
     $nids = array_keys($node_links);
     $nids = array_keys($node_links);
     $select = db_select('node', 'n');
     $select = db_select('node', 'n');
     $select->addField('n', 'nid');
     $select->addField('n', 'nid');
@@ -1596,6 +1607,7 @@ function _menu_tree_data(&$links, $parents, $depth) {
  * Implements template_preprocess_HOOK() for theme_menu_tree().
  * Implements template_preprocess_HOOK() for theme_menu_tree().
  */
  */
 function template_preprocess_menu_tree(&$variables) {
 function template_preprocess_menu_tree(&$variables) {
+  $variables['#tree'] = $variables['tree'];
   $variables['tree'] = $variables['tree']['#children'];
   $variables['tree'] = $variables['tree']['#children'];
 }
 }
 
 
@@ -1926,13 +1938,21 @@ function menu_local_tasks($level = 0) {
     }
     }
 
 
     // Get all tabs (also known as local tasks) and the root page.
     // 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();
+    $cid = 'local_tasks:' . $router_item['tab_root'];
+    if ($cache = cache_get($cid, 'cache_menu')) {
+      $result = $cache->data;
+    }
+    else {
+      $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()
+        ->fetchAll();
+      cache_set($cid, $result, 'cache_menu');
+    }
     $map = $router_item['original_map'];
     $map = $router_item['original_map'];
     $children = array();
     $children = array();
     $tasks = array();
     $tasks = array();
@@ -2139,7 +2159,7 @@ function menu_local_tasks($level = 0) {
  *   example 'node' or 'admin/structure/block/manage'.
  *   example 'node' or 'admin/structure/block/manage'.
  * @param $args
  * @param $args
  *   A list of dynamic path arguments to append to $parent_path to form the
  *   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
+ *   fully-qualified menu router path; for example, array(123) for a certain
  *   node or array('system', 'navigation') for a certain block.
  *   node or array('system', 'navigation') for a certain block.
  *
  *
  * @return
  * @return
@@ -2383,7 +2403,8 @@ function menu_set_active_trail($new_trail = NULL) {
       // a stripped down menu tree containing the active trail only, in case
       // a stripped down menu tree containing the active trail only, in case
       // the given menu has not been built in this request yet.
       // the given menu has not been built in this request yet.
       $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE);
       $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE);
-      list($key, $curr) = each($tree);
+      $curr = current($tree);
+      next($tree);
     }
     }
     // There is no link for the current path.
     // There is no link for the current path.
     else {
     else {
@@ -2401,7 +2422,7 @@ function menu_set_active_trail($new_trail = NULL) {
           // argument placeholders (%). Such links are not contained in regular
           // argument placeholders (%). Such links are not contained in regular
           // menu trees, and have only been loaded for the additional
           // menu trees, and have only been loaded for the additional
           // translation that happens here, so as to be able to display them in
           // translation that happens here, so as to be able to display them in
-          // the breadcumb for the current page.
+          // the breadcrumb for the current page.
           // @see _menu_tree_check_access()
           // @see _menu_tree_check_access()
           // @see _menu_link_translate()
           // @see _menu_link_translate()
           if (strpos($link['href'], '%') !== FALSE) {
           if (strpos($link['href'], '%') !== FALSE) {
@@ -2413,7 +2434,8 @@ function menu_set_active_trail($new_trail = NULL) {
         }
         }
         $tree = $curr['below'] ? $curr['below'] : array();
         $tree = $curr['below'] ? $curr['below'] : array();
       }
       }
-      list($key, $curr) = each($tree);
+      $curr = current($tree);
+      next($tree);
     }
     }
     // Make sure the current page is in the trail to build the page title, by
     // 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
     // appending either the preferred link or the menu router item for the
@@ -2430,7 +2452,7 @@ function menu_set_active_trail($new_trail = NULL) {
  * Looks up the preferred menu link for a given system path.
  * Looks up the preferred menu link for a given system path.
  *
  *
  * @param $path
  * @param $path
- *   The path, for example 'node/5'. The function will find the corresponding
+ *   The path; for example, 'node/5'. The function will find the corresponding
  *   menu link ('node/5' if it exists, or fallback to 'node/%').
  *   menu link ('node/5' if it exists, or fallback to 'node/%').
  * @param $selected_menu
  * @param $selected_menu
  *   The name of a menu used to restrict the search for a preferred menu link.
  *   The name of a menu used to restrict the search for a preferred menu link.
@@ -2461,6 +2483,9 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
     // untranslated paths). Afterwards, the most relevant path is picked from
     // untranslated paths). Afterwards, the most relevant path is picked from
     // the menus, ordered by menu preference.
     // the menus, ordered by menu preference.
     $item = menu_get_item($path);
     $item = menu_get_item($path);
+    if ($item === FALSE) {
+      return FALSE;
+    }
     $path_candidates = array();
     $path_candidates = array();
     // 1. The current item href.
     // 1. The current item href.
     $path_candidates[$item['href']] = $item['href'];
     $path_candidates[$item['href']] = $item['href'];
@@ -2487,6 +2512,7 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
     $query->addField('ml', 'weight', 'link_weight');
     $query->addField('ml', 'weight', 'link_weight');
     $query->fields('m');
     $query->fields('m');
     $query->condition('ml.link_path', $path_candidates, 'IN');
     $query->condition('ml.link_path', $path_candidates, 'IN');
+    $query->addTag('preferred_menu_links');
 
 
     // Sort candidates by link path and menu name.
     // Sort candidates by link path and menu name.
     $candidates = array();
     $candidates = array();
@@ -2602,10 +2628,30 @@ function menu_get_active_breadcrumb() {
  */
  */
 function menu_get_active_title() {
 function menu_get_active_title() {
   $active_trail = menu_get_active_trail();
   $active_trail = menu_get_active_trail();
+  $local_task_title = NULL;
 
 
   foreach (array_reverse($active_trail) as $item) {
   foreach (array_reverse($active_trail) as $item) {
-    if (!(bool) ($item['type'] & MENU_IS_LOCAL_TASK)) {
-      return $item['title'];
+    // Local task titles are displayed as tabs and therefore should not be
+    // repeated as the page title. However, if the local task appears in a
+    // top-level menu, it is no longer a "local task" anymore (the front page
+    // of the site does not have tabs) so it is better to use the local task
+    // title in that case than to fall back on the front page link in the
+    // active trail (which is usually "Home" and would not make sense in this
+    // context).
+    if ((bool) ($item['type'] & MENU_IS_LOCAL_TASK)) {
+      // A local task title is being skipped; track it in case it needs to be
+      // used later.
+      $local_task_title = $item['title'];
+    }
+    else {
+      // This is not a local task, so use it for the page title (unless the
+      // conditions described above are met).
+      if (isset($local_task_title) && isset($item['href']) && $item['href'] == '<front>') {
+        return $local_task_title;
+      }
+      else {
+        return $item['title'];
+      }
     }
     }
   }
   }
 }
 }
@@ -2643,7 +2689,7 @@ function menu_link_load($mlid) {
 }
 }
 
 
 /**
 /**
- * Clears the cached cached data for a single named menu.
+ * Clears the cached data for a single named menu.
  */
  */
 function menu_cache_clear($menu_name = 'navigation') {
 function menu_cache_clear($menu_name = 'navigation') {
   $cache_cleared = &drupal_static(__FUNCTION__, array());
   $cache_cleared = &drupal_static(__FUNCTION__, array());
@@ -2684,6 +2730,21 @@ function menu_reset_static_cache() {
   drupal_static_reset('menu_link_get_preferred');
   drupal_static_reset('menu_link_get_preferred');
 }
 }
 
 
+/**
+ * Checks whether a menu_rebuild() is necessary.
+ */
+function _menu_check_rebuild() {
+  // To absolutely ensure that the menu rebuild is required, re-load the
+  // variables in case they were set by another process.
+  $variables = variable_initialize();
+  if (empty($variables['menu_rebuild_needed']) && !empty($variables['menu_masks'])) {
+    unset($GLOBALS['conf']['menu_rebuild_needed']);
+    $GLOBALS['conf']['menu_masks'] = $variables['menu_masks'];
+    return FALSE;
+  }
+  return TRUE;
+}
+
 /**
 /**
  * Populates the database tables used by various menu functions.
  * Populates the database tables used by various menu functions.
  *
  *
@@ -2704,6 +2765,14 @@ function menu_rebuild() {
     // We choose to block here since otherwise the router item may not
     // We choose to block here since otherwise the router item may not
     // be available in menu_execute_active_handler() resulting in a 404.
     // be available in menu_execute_active_handler() resulting in a 404.
     lock_wait('menu_rebuild');
     lock_wait('menu_rebuild');
+
+    if (_menu_check_rebuild()) {
+      // If we get here and menu_masks was not set, then it is possible a menu
+      // is being reloaded, or that the process rebuilding the menu was unable
+      // to complete successfully. A missing menu_masks variable could result
+      // in a 404, so re-run the function.
+      return menu_rebuild();
+    }
     return FALSE;
     return FALSE;
   }
   }
 
 
@@ -2728,6 +2797,12 @@ function menu_rebuild() {
     $transaction->rollback();
     $transaction->rollback();
     watchdog_exception('menu', $e);
     watchdog_exception('menu', $e);
   }
   }
+  // Explicitly commit the transaction now; this ensures that the database
+  // operations during the menu rebuild are committed before the lock is made
+  // available again, since locks may not always reside in the same database
+  // connection. The lock is acquired outside of the transaction so should also
+  // be released outside of it.
+  unset($transaction);
 
 
   lock_release('menu_rebuild');
   lock_release('menu_rebuild');
   return TRUE;
   return TRUE;

+ 129 - 21
includes/module.inc

@@ -227,6 +227,10 @@ function system_list_reset() {
   drupal_static_reset('list_themes');
   drupal_static_reset('list_themes');
   cache_clear_all('bootstrap_modules', 'cache_bootstrap');
   cache_clear_all('bootstrap_modules', 'cache_bootstrap');
   cache_clear_all('system_list', 'cache_bootstrap');
   cache_clear_all('system_list', 'cache_bootstrap');
+
+  // Clean up the bootstrap file scan cache.
+  drupal_static_reset('_drupal_file_scan_cache');
+  cache_clear_all('_drupal_file_scan_cache', 'cache_bootstrap');
 }
 }
 
 
 /**
 /**
@@ -265,11 +269,11 @@ function _module_build_dependencies($files) {
 /**
 /**
  * Determines whether a given module exists.
  * Determines whether a given module exists.
  *
  *
- * @param $module
+ * @param string $module
  *   The name of the module (without the .module extension).
  *   The name of the module (without the .module extension).
  *
  *
- * @return
- *   TRUE if the module is both installed and enabled.
+ * @return bool
+ *   TRUE if the module is both installed and enabled, FALSE otherwise.
  */
  */
 function module_exists($module) {
 function module_exists($module) {
   $list = module_list();
   $list = module_list();
@@ -320,16 +324,27 @@ function module_load_install($module) {
  *   The name of the included file, if successful; FALSE otherwise.
  *   The name of the included file, if successful; FALSE otherwise.
  */
  */
 function module_load_include($type, $module, $name = NULL) {
 function module_load_include($type, $module, $name = NULL) {
+  static $files = array();
+
   if (!isset($name)) {
   if (!isset($name)) {
     $name = $module;
     $name = $module;
   }
   }
 
 
+  $key = $type . ':' . $module . ':' . $name;
+  if (isset($files[$key])) {
+    return $files[$key];
+  }
+
   if (function_exists('drupal_get_path')) {
   if (function_exists('drupal_get_path')) {
     $file = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$name.$type";
     $file = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$name.$type";
     if (is_file($file)) {
     if (is_file($file)) {
       require_once $file;
       require_once $file;
+      $files[$key] = $file;
       return $file;
       return $file;
     }
     }
+    else {
+      $files[$key] = FALSE;
+    }
   }
   }
   return FALSE;
   return FALSE;
 }
 }
@@ -365,20 +380,22 @@ function module_load_all_includes($type, $name = NULL) {
  * - Invoke hook_modules_installed().
  * - Invoke hook_modules_installed().
  * - Invoke hook_modules_enabled().
  * - Invoke hook_modules_enabled().
  *
  *
- * @param $module_list
+ * @param string[] $module_list
  *   An array of module names.
  *   An array of module names.
- * @param $enable_dependencies
+ * @param bool $enable_dependencies
  *   If TRUE, dependencies will automatically be added and enabled in the
  *   If TRUE, dependencies will automatically be added and enabled in the
  *   correct order. This incurs a significant performance cost, so use FALSE
  *   correct order. This incurs a significant performance cost, so use FALSE
  *   if you know $module_list is already complete and in the correct order.
  *   if you know $module_list is already complete and in the correct order.
  *
  *
- * @return
+ * @return bool
  *   FALSE if one or more dependencies are missing, TRUE otherwise.
  *   FALSE if one or more dependencies are missing, TRUE otherwise.
  *
  *
  * @see hook_install()
  * @see hook_install()
  * @see hook_enable()
  * @see hook_enable()
  * @see hook_modules_installed()
  * @see hook_modules_installed()
  * @see hook_modules_enabled()
  * @see hook_modules_enabled()
+ * @see module_disable()
+ * @see drupal_uninstall_modules()
  */
  */
 function module_enable($module_list, $enable_dependencies = TRUE) {
 function module_enable($module_list, $enable_dependencies = TRUE) {
   if ($enable_dependencies) {
   if ($enable_dependencies) {
@@ -387,7 +404,11 @@ function module_enable($module_list, $enable_dependencies = TRUE) {
     // Create an associative array with weights as values.
     // Create an associative array with weights as values.
     $module_list = array_flip(array_values($module_list));
     $module_list = array_flip(array_values($module_list));
 
 
-    while (list($module) = each($module_list)) {
+    // The array is iterated over manually (instead of using a foreach) because
+    // modules may be added to the list within the loop and we need to process
+    // them.
+    while ($module = key($module_list)) {
+      next($module_list);
       if (!isset($module_data[$module])) {
       if (!isset($module_data[$module])) {
         // This module is not found in the filesystem, abort.
         // This module is not found in the filesystem, abort.
         return FALSE;
         return FALSE;
@@ -505,12 +526,15 @@ function module_enable($module_list, $enable_dependencies = TRUE) {
 /**
 /**
  * Disables a given set of modules.
  * Disables a given set of modules.
  *
  *
- * @param $module_list
+ * @param string[] $module_list
  *   An array of module names.
  *   An array of module names.
- * @param $disable_dependents
+ * @param bool $disable_dependents
  *   If TRUE, dependent modules will automatically be added and disabled in the
  *   If TRUE, dependent modules will automatically be added and disabled in the
  *   correct order. This incurs a significant performance cost, so use FALSE
  *   correct order. This incurs a significant performance cost, so use FALSE
  *   if you know $module_list is already complete and in the correct order.
  *   if you know $module_list is already complete and in the correct order.
+ *
+ * @see drupal_uninstall_modules()
+ * @see module_enable()
  */
  */
 function module_disable($module_list, $disable_dependents = TRUE) {
 function module_disable($module_list, $disable_dependents = TRUE) {
   if ($disable_dependents) {
   if ($disable_dependents) {
@@ -520,7 +544,11 @@ function module_disable($module_list, $disable_dependents = TRUE) {
     $module_list = array_flip(array_values($module_list));
     $module_list = array_flip(array_values($module_list));
 
 
     $profile = drupal_get_profile();
     $profile = drupal_get_profile();
-    while (list($module) = each($module_list)) {
+    // The array is iterated over manually (instead of using a foreach) because
+    // modules may be added to the list within the loop and we need to process
+    // them.
+    while ($module = key($module_list)) {
+      next($module_list);
       if (!isset($module_data[$module]) || !$module_data[$module]->status) {
       if (!isset($module_data[$module]) || !$module_data[$module]->status) {
         // This module doesn't exist or is already disabled, skip it.
         // This module doesn't exist or is already disabled, skip it.
         unset($module_list[$module]);
         unset($module_list[$module]);
@@ -610,9 +638,40 @@ function module_disable($module_list, $disable_dependents = TRUE) {
  * just models that you can modify. Only the hooks implemented within modules
  * just models that you can modify. Only the hooks implemented within modules
  * are executed when running Drupal.
  * are executed when running Drupal.
  *
  *
- * See also @link themeable the themeable group page. @endlink
+ * @see themeable
+ * @see callbacks
  */
  */
 
 
+ /**
+  * @defgroup callbacks Callbacks
+  * @{
+  * Callback function signatures.
+  *
+  * Drupal's API sometimes uses callback functions to allow you to define how
+  * some type of processing happens. A callback is a function with a defined
+  * signature, which you define in a module. Then you pass the function name as
+  * a parameter to a Drupal API function or return it as part of a hook
+  * implementation return value, and your function is called at an appropriate
+  * time. For instance, when setting up batch processing you might need to
+  * provide a callback function for each processing step and/or a callback for
+  * when processing is finished; you would do that by defining these functions
+  * and passing their names into the batch setup function.
+  *
+  * Callback function signatures, like hook definitions, are described by
+  * creating and documenting dummy functions in a *.api.php file; normally, the
+  * dummy callback function's name should start with "callback_", and you should
+  * document the parameters and return value and provide a sample function body.
+  * Then your API documentation can refer to this callback function in its
+  * documentation. A user of your API can usually name their callback function
+  * anything they want, although a standard name would be to replace "callback_"
+  * with the module name.
+  *
+  * @see hooks
+  * @see themeable
+  *
+  * @}
+  */
+
 /**
 /**
  * Determines whether a module implements a hook.
  * Determines whether a module implements a hook.
  *
  *
@@ -645,12 +704,16 @@ function module_hook($module, $hook) {
 /**
 /**
  * Determines which modules are implementing a hook.
  * Determines which modules are implementing a hook.
  *
  *
- * @param $hook
+ * Lazy-loaded include files specified with "group" via hook_hook_info() or
+ * hook_module_implements_alter() will be automatically included by this
+ * function when necessary.
+ *
+ * @param string $hook
  *   The name of the hook (e.g. "help" or "menu").
  *   The name of the hook (e.g. "help" or "menu").
- * @param $sort
+ * @param bool $sort
  *   By default, modules are ordered by weight and filename, settings this option
  *   By default, modules are ordered by weight and filename, settings this option
  *   to TRUE, module list will be ordered by module name.
  *   to TRUE, module list will be ordered by module name.
- * @param $reset
+ * @param bool $reset
  *   For internal use only: Whether to force the stored list of hook
  *   For internal use only: Whether to force the stored list of hook
  *   implementations to be regenerated (such as after enabling a new module,
  *   implementations to be regenerated (such as after enabling a new module,
  *   before processing hook_enable).
  *   before processing hook_enable).
@@ -665,8 +728,10 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
   static $drupal_static_fast;
   static $drupal_static_fast;
   if (!isset($drupal_static_fast)) {
   if (!isset($drupal_static_fast)) {
     $drupal_static_fast['implementations'] = &drupal_static(__FUNCTION__);
     $drupal_static_fast['implementations'] = &drupal_static(__FUNCTION__);
+    $drupal_static_fast['verified'] = &drupal_static(__FUNCTION__ . ':verified');
   }
   }
   $implementations = &$drupal_static_fast['implementations'];
   $implementations = &$drupal_static_fast['implementations'];
+  $verified = &$drupal_static_fast['verified'];
 
 
   // We maintain a persistent cache of hook implementations in addition to the
   // 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
   // static cache to avoid looping through every module and every hook on each
@@ -680,14 +745,19 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
   // per request.
   // per request.
   if ($reset) {
   if ($reset) {
     $implementations = array();
     $implementations = array();
+    $verified = array();
     cache_set('module_implements', array(), 'cache_bootstrap');
     cache_set('module_implements', array(), 'cache_bootstrap');
     drupal_static_reset('module_hook_info');
     drupal_static_reset('module_hook_info');
     drupal_static_reset('drupal_alter');
     drupal_static_reset('drupal_alter');
     cache_clear_all('hook_info', 'cache_bootstrap');
     cache_clear_all('hook_info', 'cache_bootstrap');
+    cache_clear_all('system_cache_tables', 'cache');
     return;
     return;
   }
   }
 
 
   // Fetch implementations from cache.
   // Fetch implementations from cache.
+  // This happens on the first call to module_implements(*, *, FALSE) during a
+  // request, but also when $implementations have been reset, e.g. after
+  // module_enable().
   if (empty($implementations)) {
   if (empty($implementations)) {
     $implementations = cache_get('module_implements', 'cache_bootstrap');
     $implementations = cache_get('module_implements', 'cache_bootstrap');
     if ($implementations === FALSE) {
     if ($implementations === FALSE) {
@@ -696,12 +766,17 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
     else {
     else {
       $implementations = $implementations->data;
       $implementations = $implementations->data;
     }
     }
+    // Forget all previously "verified" hooks, in case that $implementations
+    // were cleared via drupal_static_reset('module_implements') instead of
+    // module_implements(*, *, TRUE).
+    $verified = array();
   }
   }
 
 
   if (!isset($implementations[$hook])) {
   if (!isset($implementations[$hook])) {
     // The hook is not cached, so ensure that whether or not it has
     // 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, that the cache is updated at the end of the request.
     $implementations['#write_cache'] = TRUE;
     $implementations['#write_cache'] = TRUE;
+    // Discover implementations for this hook.
     $hook_info = module_hook_info();
     $hook_info = module_hook_info();
     $implementations[$hook] = array();
     $implementations[$hook] = array();
     $list = module_list(FALSE, FALSE, $sort);
     $list = module_list(FALSE, FALSE, $sort);
@@ -713,13 +788,31 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
         $implementations[$hook][$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
         $implementations[$hook][$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
       }
       }
     }
     }
-    // Allow modules to change the weight of specific implementations but avoid
+    // Allow modules to change the weight of specific implementations, but avoid
     // an infinite loop.
     // an infinite loop.
     if ($hook != 'module_implements_alter') {
     if ($hook != 'module_implements_alter') {
+      // Remember the implementations before hook_module_implements_alter().
+      $implementations_before = $implementations[$hook];
       drupal_alter('module_implements', $implementations[$hook], $hook);
       drupal_alter('module_implements', $implementations[$hook], $hook);
+      // Verify implementations that were added or modified.
+      foreach (array_diff_assoc($implementations[$hook], $implementations_before) as $module => $group) {
+        // If drupal_alter('module_implements') changed or added a $group, the
+        // respective file needs to be included.
+        if ($group) {
+          module_load_include('inc', $module, "$module.$group");
+        }
+        // If a new implementation was added, verify that the function exists.
+        if (!function_exists($module . '_' . $hook)) {
+          unset($implementations[$hook][$module]);
+        }
+      }
     }
     }
+    // Implementations for this hook are now "verified".
+    $verified[$hook] = TRUE;
   }
   }
-  else {
+  elseif (!isset($verified[$hook])) {
+    // Implementations for this hook were in the cache, but they are not
+    // "verified" yet.
     foreach ($implementations[$hook] as $module => $group) {
     foreach ($implementations[$hook] as $module => $group) {
       // If this hook implementation is stored in a lazy-loaded file, so include
       // If this hook implementation is stored in a lazy-loaded file, so include
       // that file first.
       // that file first.
@@ -738,6 +831,7 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
         $implementations['#write_cache'] = TRUE;
         $implementations['#write_cache'] = TRUE;
       }
       }
     }
     }
+    $verified[$hook] = TRUE;
   }
   }
 
 
   return array_keys($implementations[$hook]);
   return array_keys($implementations[$hook]);
@@ -802,11 +896,13 @@ function module_hook_info() {
  * @see module_implements()
  * @see module_implements()
  */
  */
 function module_implements_write_cache() {
 function module_implements_write_cache() {
+  // The list of implementations includes vital modules only before full
+  // bootstrap, so do not write cache if we are not fully bootstrapped yet.
+  if (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL) {
+    return;
+  }
   $implementations = &drupal_static('module_implements');
   $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')) {
+  if (isset($implementations['#write_cache'])) {
     unset($implementations['#write_cache']);
     unset($implementations['#write_cache']);
     cache_set('module_implements', $implementations, 'cache_bootstrap');
     cache_set('module_implements', $implementations, 'cache_bootstrap');
   }
   }
@@ -815,6 +911,9 @@ function module_implements_write_cache() {
 /**
 /**
  * Invokes a hook in a particular module.
  * Invokes a hook in a particular module.
  *
  *
+ * All arguments are passed by value. Use drupal_alter() if you need to pass
+ * arguments by reference.
+ *
  * @param $module
  * @param $module
  *   The name of the module (without the .module extension).
  *   The name of the module (without the .module extension).
  * @param $hook
  * @param $hook
@@ -824,6 +923,8 @@ function module_implements_write_cache() {
  *
  *
  * @return
  * @return
  *   The return value of the hook implementation.
  *   The return value of the hook implementation.
+ *
+ * @see drupal_alter()
  */
  */
 function module_invoke($module, $hook) {
 function module_invoke($module, $hook) {
   $args = func_get_args();
   $args = func_get_args();
@@ -837,6 +938,9 @@ function module_invoke($module, $hook) {
 /**
 /**
  * Invokes a hook in all enabled modules that implement it.
  * Invokes a hook in all enabled modules that implement it.
  *
  *
+ * All arguments are passed by value. Use drupal_alter() if you need to pass
+ * arguments by reference.
+ *
  * @param $hook
  * @param $hook
  *   The name of the hook to invoke.
  *   The name of the hook to invoke.
  * @param ...
  * @param ...
@@ -844,7 +948,11 @@ function module_invoke($module, $hook) {
  *
  *
  * @return
  * @return
  *   An array of return values of the hook implementations. If modules return
  *   An array of return values of the hook implementations. If modules return
- *   arrays from their implementations, those are merged into one array.
+ *   arrays from their implementations, those are merged into one array
+ *   recursively. Note: integer keys in arrays will be lost, as the merge is
+ *   done using array_merge_recursive().
+ *
+ * @see drupal_alter()
  */
  */
 function module_invoke_all($hook) {
 function module_invoke_all($hook) {
   $args = func_get_args();
   $args = func_get_args();

+ 31 - 1
includes/pager.inc

@@ -321,9 +321,19 @@ function theme_pager($variables) {
   $tags = $variables['tags'];
   $tags = $variables['tags'];
   $element = $variables['element'];
   $element = $variables['element'];
   $parameters = $variables['parameters'];
   $parameters = $variables['parameters'];
-  $quantity = $variables['quantity'];
+  $quantity = empty($variables['quantity']) ? 0 : $variables['quantity'];
   global $pager_page_array, $pager_total;
   global $pager_page_array, $pager_total;
 
 
+  // Nothing to do if there is no pager.
+  if (!isset($pager_page_array[$element]) || !isset($pager_total[$element])) {
+    return;
+  }
+
+  // Nothing to do if there is only one page.
+  if ($pager_total[$element] <= 1) {
+    return;
+  }
+
   // Calculate various markers within this pager piece:
   // Calculate various markers within this pager piece:
   // Middle is used to "center" pages around the current page.
   // Middle is used to "center" pages around the current page.
   $pager_middle = ceil($quantity / 2);
   $pager_middle = ceil($quantity / 2);
@@ -455,6 +465,11 @@ function theme_pager_first($variables) {
   global $pager_page_array;
   global $pager_page_array;
   $output = '';
   $output = '';
 
 
+  // Nothing to do if there is no pager.
+  if (!isset($pager_page_array[$element])) {
+    return;
+  }
+
   // If we are anywhere but the first page
   // If we are anywhere but the first page
   if ($pager_page_array[$element] > 0) {
   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));
     $output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array(0, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));
@@ -485,6 +500,11 @@ function theme_pager_previous($variables) {
   global $pager_page_array;
   global $pager_page_array;
   $output = '';
   $output = '';
 
 
+  // Nothing to do if there is no pager.
+  if (!isset($pager_page_array[$element])) {
+    return;
+  }
+
   // If we are anywhere but the first page
   // If we are anywhere but the first page
   if ($pager_page_array[$element] > 0) {
   if ($pager_page_array[$element] > 0) {
     $page_new = pager_load_array($pager_page_array[$element] - $interval, $element, $pager_page_array);
     $page_new = pager_load_array($pager_page_array[$element] - $interval, $element, $pager_page_array);
@@ -524,6 +544,11 @@ function theme_pager_next($variables) {
   global $pager_page_array, $pager_total;
   global $pager_page_array, $pager_total;
   $output = '';
   $output = '';
 
 
+  // Nothing to do if there is no pager.
+  if (!isset($pager_page_array[$element]) || !isset($pager_total[$element])) {
+    return;
+  }
+
   // If we are anywhere but the last page
   // If we are anywhere but the last page
   if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
   if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
     $page_new = pager_load_array($pager_page_array[$element] + $interval, $element, $pager_page_array);
     $page_new = pager_load_array($pager_page_array[$element] + $interval, $element, $pager_page_array);
@@ -560,6 +585,11 @@ function theme_pager_last($variables) {
   global $pager_page_array, $pager_total;
   global $pager_page_array, $pager_total;
   $output = '';
   $output = '';
 
 
+  // Nothing to do if there is no pager.
+  if (!isset($pager_page_array[$element]) || !isset($pager_total[$element])) {
+    return;
+  }
+
   // If we are anywhere but the last page
   // If we are anywhere but the last page
   if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
   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));
     $output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array($pager_total[$element] - 1, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));

+ 5 - 1
includes/password.inc

@@ -140,7 +140,7 @@ function _password_enforce_log2_boundaries($count_log2) {
  * @param $algo
  * @param $algo
  *   The string name of a hashing algorithm usable by hash(), like 'sha256'.
  *   The string name of a hashing algorithm usable by hash(), like 'sha256'.
  * @param $password
  * @param $password
- *   The plain-text password to hash.
+ *   Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to hash.
  * @param $setting
  * @param $setting
  *   An existing hash or the output of _password_generate_salt().  Must be
  *   An existing hash or the output of _password_generate_salt().  Must be
  *   at least 12 characters (the settings and salt).
  *   at least 12 characters (the settings and salt).
@@ -150,6 +150,10 @@ function _password_enforce_log2_boundaries($count_log2) {
  *   The return string will be truncated at DRUPAL_HASH_LENGTH characters max.
  *   The return string will be truncated at DRUPAL_HASH_LENGTH characters max.
  */
  */
 function _password_crypt($algo, $password, $setting) {
 function _password_crypt($algo, $password, $setting) {
+  // Prevent DoS attacks by refusing to hash large passwords.
+  if (strlen($password) > 512) {
+    return FALSE;
+  }
   // The first 12 characters of an existing hash are its setting string.
   // The first 12 characters of an existing hash are its setting string.
   $setting = substr($setting, 0, 12);
   $setting = substr($setting, 0, 12);
 
 

+ 12 - 9
includes/path.inc

@@ -347,7 +347,8 @@ function drupal_match_path($path, $patterns) {
  * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
  * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
  *
  *
  * @return
  * @return
- *   The current Drupal URL path.
+ *   The current Drupal URL path. The path is untrusted user input and must be
+ *   treated as such.
  *
  *
  * @see request_path()
  * @see request_path()
  */
  */
@@ -465,13 +466,15 @@ function path_delete($criteria) {
     $criteria = array('pid' => $criteria);
     $criteria = array('pid' => $criteria);
   }
   }
   $path = path_load($criteria);
   $path = path_load($criteria);
-  $query = db_delete('url_alias');
-  foreach ($criteria as $field => $value) {
-    $query->condition($field, $value);
+  if (isset($path['source'])) {
+    $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']);
   }
   }
-  $query->execute();
-  module_invoke_all('path_delete', $path);
-  drupal_clear_path_cache($path['source']);
 }
 }
 
 
 /**
 /**
@@ -560,8 +563,8 @@ function drupal_valid_path($path, $dynamic_allowed = FALSE) {
   elseif ($dynamic_allowed && preg_match('/\/\%/', $path)) {
   elseif ($dynamic_allowed && preg_match('/\/\%/', $path)) {
     // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
     // 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()) {
     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['link_path']  = $item['path'];
+      $item['link_title'] = $item['title'];
       $item['external']   = FALSE;
       $item['external']   = FALSE;
       $item['options'] = '';
       $item['options'] = '';
       _menu_link_translate($item);
       _menu_link_translate($item);

+ 37 - 9
includes/registry.inc

@@ -10,7 +10,7 @@
  * @{
  * @{
  * The code registry engine.
  * The code registry engine.
  *
  *
- * Drupal maintains an internal registry of all functions or classes in the
+ * Drupal maintains an internal registry of all interfaces or classes in the
  * system, allowing it to lazy-load code files as needed (reducing the amount
  * system, allowing it to lazy-load code files as needed (reducing the amount
  * of code that must be parsed on each request).
  * of code that must be parsed on each request).
  */
  */
@@ -19,7 +19,6 @@
  * Does the work for registry_update().
  * Does the work for registry_update().
  */
  */
 function _registry_update() {
 function _registry_update() {
-
   // The registry serves as a central autoloader for all classes, including
   // The registry serves as a central autoloader for all classes, including
   // the database query builders. However, the registry rebuild process
   // the database query builders. However, the registry rebuild process
   // requires write ability to the database, which means having access to the
   // requires write ability to the database, which means having access to the
@@ -33,6 +32,11 @@ function _registry_update() {
   require_once DRUPAL_ROOT . '/includes/database/select.inc';
   require_once DRUPAL_ROOT . '/includes/database/select.inc';
   require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/query.inc';
   require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/query.inc';
 
 
+  // During the first registry rebuild in a request, we check all the files.
+  // During subsequent rebuilds, we only add new files. It makes the rebuilding
+  // process faster during installation of modules.
+  static $check_existing_files = TRUE;
+
   // Get current list of modules and their files.
   // Get current list of modules and their files.
   $modules = db_query("SELECT * FROM {system} WHERE type = 'module'")->fetchAll();
   $modules = db_query("SELECT * FROM {system} WHERE type = 'module'")->fetchAll();
   // Get the list of files we are going to parse.
   // Get the list of files we are going to parse.
@@ -55,6 +59,9 @@ function _registry_update() {
     $files["$filename"] = array('module' => '', 'weight' => 0);
     $files["$filename"] = array('module' => '', 'weight' => 0);
   }
   }
 
 
+  // Initialize an empty array for the unchanged files.
+  $unchanged_files = array();
+
   $transaction = db_transaction();
   $transaction = db_transaction();
   try {
   try {
     // Allow modules to manually modify the list of files before the registry
     // Allow modules to manually modify the list of files before the registry
@@ -63,10 +70,19 @@ function _registry_update() {
     // list can then be added to the list of files that the registry will parse,
     // list can then be added to the list of files that the registry will parse,
     // or modify attributes of a file.
     // or modify attributes of a file.
     drupal_alter('registry_files', $files, $modules);
     drupal_alter('registry_files', $files, $modules);
+
     foreach (registry_get_parsed_files() as $filename => $file) {
     foreach (registry_get_parsed_files() as $filename => $file) {
       // Add the hash for those files we have already parsed.
       // Add the hash for those files we have already parsed.
       if (isset($files[$filename])) {
       if (isset($files[$filename])) {
-        $files[$filename]['hash'] = $file['hash'];
+        if ($check_existing_files === TRUE) {
+          $files[$filename]['hash'] = $file['hash'];
+        }
+        else {
+          // Ignore that file for this request, it has been parsed previously
+          // and it is unlikely it has changed.
+          unset($files[$filename]);
+          $unchanged_files[$filename] = $file;
+        }
       }
       }
       else {
       else {
         // Flush the registry of resources in files that are no longer on disc
         // Flush the registry of resources in files that are no longer on disc
@@ -79,8 +95,12 @@ function _registry_update() {
           ->execute();
           ->execute();
       }
       }
     }
     }
+
     $parsed_files = _registry_parse_files($files);
     $parsed_files = _registry_parse_files($files);
 
 
+    // Add unchanged files to the files.
+    $files += $unchanged_files;
+
     $unchanged_resources = array();
     $unchanged_resources = array();
     $lookup_cache = array();
     $lookup_cache = array();
     if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) {
     if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) {
@@ -89,12 +109,10 @@ function _registry_update() {
     foreach ($lookup_cache as $key => $file) {
     foreach ($lookup_cache as $key => $file) {
       // If the file for this cached resource is carried over unchanged from
       // If the file for this cached resource is carried over unchanged from
       // the last registry build, then we can safely re-cache it.
       // the last registry build, then we can safely re-cache it.
-      if ($file && in_array($file, array_keys($files)) && !in_array($file, $parsed_files)) {
+      if ($file && isset($files[$file]) && !in_array($file, $parsed_files, TRUE)) {
         $unchanged_resources[$key] = $file;
         $unchanged_resources[$key] = $file;
       }
       }
     }
     }
-    module_implements('', FALSE, TRUE);
-    _registry_check_code(REGISTRY_RESET_LOOKUP_CACHE);
   }
   }
   catch (Exception $e) {
   catch (Exception $e) {
     $transaction->rollback();
     $transaction->rollback();
@@ -102,6 +120,13 @@ function _registry_update() {
     throw $e;
     throw $e;
   }
   }
 
 
+  module_implements('', FALSE, TRUE);
+  _registry_check_code(REGISTRY_RESET_LOOKUP_CACHE);
+
+  // During the next run in this request, don't bother re-checking existing
+  // files.
+  $check_existing_files = FALSE;
+
   // We have some unchanged resources, warm up the cache - no need to pay
   // We have some unchanged resources, warm up the cache - no need to pay
   // for looking them up again.
   // for looking them up again.
   if (count($unchanged_resources) > 0) {
   if (count($unchanged_resources) > 0) {
@@ -120,7 +145,10 @@ function registry_get_parsed_files() {
 }
 }
 
 
 /**
 /**
- * Parse all files that have changed since the registry was last built, and save their function and class listings.
+ * Parse all changed files and save their interface and class listings.
+ *
+ * Parse all files that have changed since the registry was last built, and save
+ * their interface and class listings.
  *
  *
  * @param $files
  * @param $files
  *  The list of files to check and parse.
  *  The list of files to check and parse.
@@ -149,7 +177,7 @@ function _registry_parse_files($files) {
 }
 }
 
 
 /**
 /**
- * Parse a file and save its function and class listings.
+ * Parse a file and save its interface and class listings.
  *
  *
  * @param $filename
  * @param $filename
  *   Name of the file we are going to parse.
  *   Name of the file we are going to parse.
@@ -161,7 +189,7 @@ function _registry_parse_files($files) {
  *   (optional) Weight of the module.
  *   (optional) Weight of the module.
  */
  */
 function _registry_parse_file($filename, $contents, $module = '', $weight = 0) {
 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)) {
+  if (preg_match_all('/^\s*(?:abstract|final)?\s*(class|interface|trait)\s+([a-zA-Z0-9_]+)/m', $contents, $matches)) {
     foreach ($matches[2] as $key => $name) {
     foreach ($matches[2] as $key => $name) {
       db_merge('registry')
       db_merge('registry')
         ->key(array(
         ->key(array(

+ 114 - 0
includes/request-sanitizer.inc

@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @file
+ * Contains code for sanitizing user input from the request.
+ */
+
+/**
+ * Sanitizes user input from the request.
+ */
+class DrupalRequestSanitizer {
+
+  /**
+   * Tracks whether the request was already sanitized.
+   */
+  protected static $sanitized = FALSE;
+
+  /**
+   * Modifies the request to strip dangerous keys from user input.
+   */
+  public static function sanitize() {
+    if (!self::$sanitized) {
+      $whitelist = variable_get('sanitize_input_whitelist', array());
+      $log_sanitized_keys = variable_get('sanitize_input_logging', FALSE);
+
+      // Process query string parameters.
+      $get_sanitized_keys = array();
+      $_GET = self::stripDangerousValues($_GET, $whitelist, $get_sanitized_keys);
+      if ($log_sanitized_keys && $get_sanitized_keys) {
+        _drupal_trigger_error_with_delayed_logging(format_string('Potentially unsafe keys removed from query string parameters (GET): @keys', array('@keys' => implode(', ', $get_sanitized_keys))), E_USER_NOTICE);
+      }
+
+      // Process request body parameters.
+      $post_sanitized_keys = array();
+      $_POST = self::stripDangerousValues($_POST, $whitelist, $post_sanitized_keys);
+      if ($log_sanitized_keys && $post_sanitized_keys) {
+        _drupal_trigger_error_with_delayed_logging(format_string('Potentially unsafe keys removed from request body parameters (POST): @keys', array('@keys' => implode(', ', $post_sanitized_keys))), E_USER_NOTICE);
+      }
+
+      // Process cookie parameters.
+      $cookie_sanitized_keys = array();
+      $_COOKIE = self::stripDangerousValues($_COOKIE, $whitelist, $cookie_sanitized_keys);
+      if ($log_sanitized_keys && $cookie_sanitized_keys) {
+        _drupal_trigger_error_with_delayed_logging(format_string('Potentially unsafe keys removed from cookie parameters (COOKIE): @keys', array('@keys' => implode(', ', $cookie_sanitized_keys))), E_USER_NOTICE);
+      }
+
+      $request_sanitized_keys = array();
+      $_REQUEST = self::stripDangerousValues($_REQUEST, $whitelist, $request_sanitized_keys);
+
+      self::$sanitized = TRUE;
+    }
+  }
+
+  /**
+   * Removes the destination if it is dangerous.
+   *
+   * Note this can only be called after common.inc has been included.
+   *
+   * @return bool
+   *   TRUE if the destination has been removed from $_GET, FALSE if not.
+   */
+  public static function cleanDestination() {
+    $dangerous_keys = array();
+    $log_sanitized_keys = variable_get('sanitize_input_logging', FALSE);
+
+    $parts = drupal_parse_url($_GET['destination']);
+    // If there is a query string, check its query parameters.
+    if (!empty($parts['query'])) {
+      $whitelist = variable_get('sanitize_input_whitelist', array());
+
+      self::stripDangerousValues($parts['query'], $whitelist, $dangerous_keys);
+      if (!empty($dangerous_keys)) {
+        // The destination is removed rather than sanitized to mirror the
+        // handling of external destinations.
+        unset($_GET['destination']);
+        unset($_REQUEST['destination']);
+        if ($log_sanitized_keys) {
+          trigger_error(format_string('Potentially unsafe destination removed from query string parameters (GET) because it contained the following keys: @keys', array('@keys' => implode(', ', $dangerous_keys))));
+        }
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Strips dangerous keys from the provided input.
+   *
+   * @param mixed $input
+   *   The input to sanitize.
+   * @param string[] $whitelist
+   *   An array of keys to whitelist as safe.
+   * @param string[] $sanitized_keys
+   *   An array of keys that have been removed.
+   *
+   * @return mixed
+   *   The sanitized input.
+   */
+  protected static function stripDangerousValues($input, array $whitelist, array &$sanitized_keys) {
+    if (is_array($input)) {
+      foreach ($input as $key => $value) {
+        if ($key !== '' && is_string($key) && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) {
+          unset($input[$key]);
+          $sanitized_keys[] = $key;
+        }
+        else {
+          $input[$key] = self::stripDangerousValues($input[$key], $whitelist, $sanitized_keys);
+        }
+      }
+    }
+    return $input;
+  }
+
+}

+ 32 - 7
includes/session.inc

@@ -79,7 +79,7 @@ function _drupal_session_read($sid) {
   // Handle the case of first time visitors and clients that don't store
   // Handle the case of first time visitors and clients that don't store
   // cookies (eg. web crawlers).
   // cookies (eg. web crawlers).
   $insecure_session_name = substr(session_name(), 1);
   $insecure_session_name = substr(session_name(), 1);
-  if (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name])) {
+  if (empty($sid) || (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name]))) {
     $user = drupal_anonymous_user();
     $user = drupal_anonymous_user();
     return '';
     return '';
   }
   }
@@ -163,7 +163,7 @@ function _drupal_session_write($sid, $value) {
   try {
   try {
     if (!drupal_save_session()) {
     if (!drupal_save_session()) {
       // We don't have anything to do if we are not allowed to save the session.
       // We don't have anything to do if we are not allowed to save the session.
-      return;
+      return TRUE;
     }
     }
 
 
     // Check whether $_SESSION has been changed in this request.
     // Check whether $_SESSION has been changed in this request.
@@ -263,10 +263,10 @@ function drupal_session_initialize() {
     // Less random sessions (which are much faster to generate) are used for
     // Less random sessions (which are much faster to generate) are used for
     // anonymous users than are generated in drupal_session_regenerate() when
     // anonymous users than are generated in drupal_session_regenerate() when
     // a user becomes authenticated.
     // a user becomes authenticated.
-    session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE)));
+    session_id(drupal_random_key());
     if ($is_https && variable_get('https', FALSE)) {
     if ($is_https && variable_get('https', FALSE)) {
       $insecure_session_name = substr(session_name(), 1);
       $insecure_session_name = substr(session_name(), 1);
-      $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE));
+      $session_id = drupal_random_key();
       $_COOKIE[$insecure_session_name] = $session_id;
       $_COOKIE[$insecure_session_name] = $session_id;
     }
     }
   }
   }
@@ -360,7 +360,7 @@ function drupal_session_regenerate() {
       $old_insecure_session_id = $_COOKIE[$insecure_session_name];
       $old_insecure_session_id = $_COOKIE[$insecure_session_name];
     }
     }
     $params = session_get_cookie_params();
     $params = session_get_cookie_params();
-    $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55));
+    $session_id = drupal_random_key();
     // If a session cookie lifetime is set, the session will expire
     // If a session cookie lifetime is set, the session will expire
     // $params['lifetime'] seconds from the current request. If it is not set,
     // $params['lifetime'] seconds from the current request. If it is not set,
     // it will expire when the browser is closed.
     // it will expire when the browser is closed.
@@ -371,8 +371,11 @@ function drupal_session_regenerate() {
 
 
   if (drupal_session_started()) {
   if (drupal_session_started()) {
     $old_session_id = session_id();
     $old_session_id = session_id();
+    _drupal_session_regenerate_existing();
+  }
+  else {
+    session_id(drupal_random_key());
   }
   }
-  session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)));
 
 
   if (isset($old_session_id)) {
   if (isset($old_session_id)) {
     $params = session_get_cookie_params();
     $params = session_get_cookie_params();
@@ -412,6 +415,26 @@ function drupal_session_regenerate() {
   date_default_timezone_set(drupal_get_user_timezone());
   date_default_timezone_set(drupal_get_user_timezone());
 }
 }
 
 
+/**
+ * Regenerates an existing session.
+ */
+function _drupal_session_regenerate_existing() {
+  global $user;
+  // Preserve existing settings for the saving of sessions.
+  $original_save_session_status = drupal_save_session();
+  // Turn off saving of sessions.
+  drupal_save_session(FALSE);
+  session_write_close();
+  drupal_session_started(FALSE);
+  // Preserve the user object, as starting a new session will reset it.
+  $original_user = $user;
+  session_id(drupal_random_key());
+  drupal_session_start();
+  $user = $original_user;
+  // Restore the original settings for the saving of sessions.
+  drupal_save_session($original_save_session_status);
+}
+
 /**
 /**
  * Session handler assigned by session_set_save_handler().
  * Session handler assigned by session_set_save_handler().
  *
  *
@@ -425,7 +448,7 @@ function _drupal_session_destroy($sid) {
 
 
   // Nothing to do if we are not allowed to change the session.
   // Nothing to do if we are not allowed to change the session.
   if (!drupal_save_session()) {
   if (!drupal_save_session()) {
-    return;
+    return TRUE;
   }
   }
 
 
   // Delete session data.
   // Delete session data.
@@ -446,6 +469,8 @@ function _drupal_session_destroy($sid) {
   elseif (variable_get('https', FALSE)) {
   elseif (variable_get('https', FALSE)) {
     _drupal_session_delete_cookie('S' . session_name(), TRUE);
     _drupal_session_delete_cookie('S' . session_name(), TRUE);
   }
   }
+
+  return TRUE;
 }
 }
 
 
 /**
 /**

+ 170 - 24
includes/stream_wrappers.inc

@@ -93,7 +93,7 @@ define('STREAM_WRAPPERS_LOCAL_NORMAL', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_N
 /**
 /**
  * Generic PHP stream wrapper interface.
  * Generic PHP stream wrapper interface.
  *
  *
- * @see http://www.php.net/manual/en/class.streamwrapper.php
+ * @see http://www.php.net/manual/class.streamwrapper.php
  */
  */
 interface StreamWrapperInterface {
 interface StreamWrapperInterface {
   public function stream_open($uri, $mode, $options, &$opened_url);
   public function stream_open($uri, $mode, $options, &$opened_url);
@@ -133,7 +133,7 @@ interface DrupalStreamWrapperInterface extends StreamWrapperInterface {
    * @param $uri
    * @param $uri
    *   A string containing the URI that should be used for this instance.
    *   A string containing the URI that should be used for this instance.
    */
    */
-  function setUri($uri);
+  public function setUri($uri);
 
 
   /**
   /**
    * Returns the stream resource URI.
    * Returns the stream resource URI.
@@ -219,7 +219,6 @@ interface DrupalStreamWrapperInterface extends StreamWrapperInterface {
   public function dirname($uri = NULL);
   public function dirname($uri = NULL);
 }
 }
 
 
-
 /**
 /**
  * Drupal stream wrapper base class for local files.
  * Drupal stream wrapper base class for local files.
  *
  *
@@ -401,7 +400,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   Returns TRUE if file was opened successfully.
    *   Returns TRUE if file was opened successfully.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-open.php
+   * @see http://php.net/manual/streamwrapper.stream-open.php
    */
    */
   public function stream_open($uri, $mode, $options, &$opened_path) {
   public function stream_open($uri, $mode, $options, &$opened_path) {
     $this->uri = $uri;
     $this->uri = $uri;
@@ -429,7 +428,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   Always returns TRUE at the present time.
    *   Always returns TRUE at the present time.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-lock.php
+   * @see http://php.net/manual/streamwrapper.stream-lock.php
    */
    */
   public function stream_lock($operation) {
   public function stream_lock($operation) {
     if (in_array($operation, array(LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB))) {
     if (in_array($operation, array(LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB))) {
@@ -448,7 +447,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   The string that was read, or FALSE in case of an error.
    *   The string that was read, or FALSE in case of an error.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-read.php
+   * @see http://php.net/manual/streamwrapper.stream-read.php
    */
    */
   public function stream_read($count) {
   public function stream_read($count) {
     return fread($this->handle, $count);
     return fread($this->handle, $count);
@@ -463,7 +462,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   The number of bytes written (integer).
    *   The number of bytes written (integer).
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-write.php
+   * @see http://php.net/manual/streamwrapper.stream-write.php
    */
    */
   public function stream_write($data) {
   public function stream_write($data) {
     return fwrite($this->handle, $data);
     return fwrite($this->handle, $data);
@@ -475,7 +474,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE if end-of-file has been reached.
    *   TRUE if end-of-file has been reached.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-eof.php
+   * @see http://php.net/manual/streamwrapper.stream-eof.php
    */
    */
   public function stream_eof() {
   public function stream_eof() {
     return feof($this->handle);
     return feof($this->handle);
@@ -492,7 +491,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE on success.
    *   TRUE on success.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-seek.php
+   * @see http://php.net/manual/streamwrapper.stream-seek.php
    */
    */
   public function stream_seek($offset, $whence) {
   public function stream_seek($offset, $whence) {
     // fseek returns 0 on success and -1 on a failure.
     // fseek returns 0 on success and -1 on a failure.
@@ -506,7 +505,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE if data was successfully stored (or there was no data to store).
    *   TRUE if data was successfully stored (or there was no data to store).
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-flush.php
+   * @see http://php.net/manual/streamwrapper.stream-flush.php
    */
    */
   public function stream_flush() {
   public function stream_flush() {
     return fflush($this->handle);
     return fflush($this->handle);
@@ -518,7 +517,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   The current offset in bytes from the beginning of file.
    *   The current offset in bytes from the beginning of file.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-tell.php
+   * @see http://php.net/manual/streamwrapper.stream-tell.php
    */
    */
   public function stream_tell() {
   public function stream_tell() {
     return ftell($this->handle);
     return ftell($this->handle);
@@ -531,7 +530,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    *   An array with file status, or FALSE in case of an error - see fstat()
    *   An array with file status, or FALSE in case of an error - see fstat()
    *   for a description of this array.
    *   for a description of this array.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-stat.php
+   * @see http://php.net/manual/streamwrapper.stream-stat.php
    */
    */
   public function stream_stat() {
   public function stream_stat() {
     return fstat($this->handle);
     return fstat($this->handle);
@@ -543,12 +542,161 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE if stream was successfully closed.
    *   TRUE if stream was successfully closed.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.stream-close.php
+   * @see http://php.net/manual/streamwrapper.stream-close.php
    */
    */
   public function stream_close() {
   public function stream_close() {
     return fclose($this->handle);
     return fclose($this->handle);
   }
   }
 
 
+  /**
+   * Sets metadata on the stream.
+   *
+   * WARNING: Do not call this method directly! It will be called internally by
+   * PHP itself when one of the following functions is called on a stream URL:
+   *
+   * @param string $uri
+   *   A string containing the URI to the file to set metadata on.
+   * @param int $option
+   *   One of:
+   *   - STREAM_META_TOUCH: The method was called in response to touch().
+   *   - STREAM_META_OWNER_NAME: The method was called in response to chown()
+   *     with string parameter.
+   *   - STREAM_META_OWNER: The method was called in response to chown().
+   *   - STREAM_META_GROUP_NAME: The method was called in response to chgrp().
+   *   - STREAM_META_GROUP: The method was called in response to chgrp().
+   *   - STREAM_META_ACCESS: The method was called in response to chmod().
+   * @param mixed $value
+   *   If option is:
+   *   - STREAM_META_TOUCH: Array consisting of two arguments of the touch()
+   *     function.
+   *   - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner
+   *     user/group as string.
+   *   - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner
+   *     user/group as integer.
+   *   - STREAM_META_ACCESS: The argument of the chmod() as integer.
+   *
+   * @return bool
+   *   Returns TRUE on success or FALSE on failure. If $option is not
+   *   implemented, FALSE should be returned.
+   *
+   * @see touch()
+   * @see chmod()
+   * @see chown()
+   * @see chgrp()
+   * @link http://php.net/manual/streamwrapper.stream-metadata.php
+   */
+  public function stream_metadata($uri, $option, $value) {
+    $target = $this->getLocalPath($uri);
+    $return = FALSE;
+    switch ($option) {
+      case STREAM_META_TOUCH:
+        if (!empty($value)) {
+          $return = touch($target, $value[0], $value[1]);
+        }
+        else {
+          $return = touch($target);
+        }
+        break;
+
+      case STREAM_META_OWNER_NAME:
+      case STREAM_META_OWNER:
+        $return = chown($target, $value);
+        break;
+
+      case STREAM_META_GROUP_NAME:
+      case STREAM_META_GROUP:
+        $return = chgrp($target, $value);
+        break;
+
+      case STREAM_META_ACCESS:
+        $return = chmod($target, $value);
+        break;
+    }
+    if ($return) {
+      // For convenience clear the file status cache of the underlying file,
+      // since metadata operations are often followed by file status checks.
+      clearstatcache(TRUE, $target);
+    }
+    return $return;
+  }
+
+  /**
+   * Truncate stream.
+   *
+   * Will respond to truncation; e.g., through ftruncate().
+   *
+   * @param int $new_size
+   *   The new size.
+   *
+   * @return bool
+   *   TRUE on success, FALSE otherwise.
+   */
+  public function stream_truncate($new_size) {
+    return ftruncate($this->handle, $new_size);
+  }
+
+  /**
+   * Retrieve the underlying stream resource.
+   *
+   * This method is called in response to stream_select().
+   *
+   * @param int $cast_as
+   *   Can be STREAM_CAST_FOR_SELECT when stream_select() is calling
+   *   stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for
+   *   other uses.
+   *
+   * @return resource|false
+   *   The underlying stream resource or FALSE if stream_select() is not
+   *   supported.
+   *
+   * @see stream_select()
+   * @link http://php.net/manual/streamwrapper.stream-cast.php
+   */
+  public function stream_cast($cast_as) {
+    return $this->handle ? $this->handle : FALSE;
+  }
+
+  /**
+   * Change stream options.
+   *
+   * This method is called to set options on the stream.
+   *
+   * Since Windows systems do not allow it and it is not needed for most use
+   * cases anyway, this method is not supported on local files and will trigger
+   * an error and return false. If needed, custom subclasses can provide
+   * OS-specific implementations for advanced use cases.
+   *
+   * @param int $option
+   *   One of:
+   *   - STREAM_OPTION_BLOCKING: The method was called in response to
+   *     stream_set_blocking().
+   *   - STREAM_OPTION_READ_TIMEOUT: The method was called in response to
+   *     stream_set_timeout().
+   *   - STREAM_OPTION_WRITE_BUFFER: The method was called in response to
+   *     stream_set_write_buffer().
+   * @param int $arg1
+   *   If option is:
+   *   - STREAM_OPTION_BLOCKING: The requested blocking mode:
+   *     - 1 means blocking.
+   *     - 0 means not blocking.
+   *   - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds.
+   *   - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or
+   *     STREAM_BUFFER_FULL.
+   * @param int $arg2
+   *   If option is:
+   *   - STREAM_OPTION_BLOCKING: This option is not set.
+   *   - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds.
+   *   - STREAM_OPTION_WRITE_BUFFER: The requested buffer size.
+   *
+   * @return bool
+   *   TRUE on success, FALSE otherwise. If $option is not implemented, FALSE
+   *   should be returned.
+   */
+  public function stream_set_option($option, $arg1, $arg2) {
+    trigger_error('stream_set_option() not supported for local file based stream wrappers', E_USER_WARNING);
+    return FALSE;
+  }
+
   /**
   /**
    * Support for unlink().
    * Support for unlink().
    *
    *
@@ -558,7 +706,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE if resource was successfully deleted.
    *   TRUE if resource was successfully deleted.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.unlink.php
+   * @see http://php.net/manual/streamwrapper.unlink.php
    */
    */
   public function unlink($uri) {
   public function unlink($uri) {
     $this->uri = $uri;
     $this->uri = $uri;
@@ -576,7 +724,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE if file was successfully renamed.
    *   TRUE if file was successfully renamed.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.rename.php
+   * @see http://php.net/manual/streamwrapper.rename.php
    */
    */
   public function rename($from_uri, $to_uri) {
   public function rename($from_uri, $to_uri) {
     return rename($this->getLocalPath($from_uri), $this->getLocalPath($to_uri));
     return rename($this->getLocalPath($from_uri), $this->getLocalPath($to_uri));
@@ -622,7 +770,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE if directory was successfully created.
    *   TRUE if directory was successfully created.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.mkdir.php
+   * @see http://php.net/manual/streamwrapper.mkdir.php
    */
    */
   public function mkdir($uri, $mode, $options) {
   public function mkdir($uri, $mode, $options) {
     $this->uri = $uri;
     $this->uri = $uri;
@@ -654,7 +802,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE if directory was successfully removed.
    *   TRUE if directory was successfully removed.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.rmdir.php
+   * @see http://php.net/manual/streamwrapper.rmdir.php
    */
    */
   public function rmdir($uri, $options) {
   public function rmdir($uri, $options) {
     $this->uri = $uri;
     $this->uri = $uri;
@@ -678,7 +826,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    *   An array with file status, or FALSE in case of an error - see fstat()
    *   An array with file status, or FALSE in case of an error - see fstat()
    *   for a description of this array.
    *   for a description of this array.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.url-stat.php
+   * @see http://php.net/manual/streamwrapper.url-stat.php
    */
    */
   public function url_stat($uri, $flags) {
   public function url_stat($uri, $flags) {
     $this->uri = $uri;
     $this->uri = $uri;
@@ -704,7 +852,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE on success.
    *   TRUE on success.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.dir-opendir.php
+   * @see http://php.net/manual/streamwrapper.dir-opendir.php
    */
    */
   public function dir_opendir($uri, $options) {
   public function dir_opendir($uri, $options) {
     $this->uri = $uri;
     $this->uri = $uri;
@@ -719,7 +867,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   The next filename, or FALSE if there are no more files in the directory.
    *   The next filename, or FALSE if there are no more files in the directory.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.dir-readdir.php
+   * @see http://php.net/manual/streamwrapper.dir-readdir.php
    */
    */
   public function dir_readdir() {
   public function dir_readdir() {
     return readdir($this->handle);
     return readdir($this->handle);
@@ -731,7 +879,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE on success.
    *   TRUE on success.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php
+   * @see http://php.net/manual/streamwrapper.dir-rewinddir.php
    */
    */
   public function dir_rewinddir() {
   public function dir_rewinddir() {
     rewinddir($this->handle);
     rewinddir($this->handle);
@@ -747,7 +895,7 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface
    * @return
    * @return
    *   TRUE on success.
    *   TRUE on success.
    *
    *
-   * @see http://php.net/manual/en/streamwrapper.dir-closedir.php
+   * @see http://php.net/manual/streamwrapper.dir-closedir.php
    */
    */
   public function dir_closedir() {
   public function dir_closedir() {
     closedir($this->handle);
     closedir($this->handle);
@@ -788,8 +936,6 @@ class DrupalPublicStreamWrapper extends DrupalLocalStreamWrapper {
  *
  *
  * Provides support for storing privately accessible files with the Drupal file
  * Provides support for storing privately accessible files with the Drupal file
  * interface.
  * interface.
- *
- * Extends DrupalPublicStreamWrapper.
  */
  */
 class DrupalPrivateStreamWrapper extends DrupalLocalStreamWrapper {
 class DrupalPrivateStreamWrapper extends DrupalLocalStreamWrapper {
   /**
   /**

+ 3 - 4
includes/tablesort.inc

@@ -46,10 +46,9 @@ class TableSort extends SelectQueryExtender {
       // Based on code from db_escape_table(), but this can also contain a dot.
       // Based on code from db_escape_table(), but this can also contain a dot.
       $field = preg_replace('/[^A-Za-z0-9_.]+/', '', $ts['sql']);
       $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);
+      // orderBy() will ensure that only ASC/DESC values are accepted, so we
+      // don't need to sanitize that here.
+      $this->orderBy($field, $ts['sort']);
     }
     }
     return $this;
     return $this;
   }
   }

+ 202 - 54
includes/theme.inc

@@ -508,7 +508,7 @@ class ThemeRegistry Extends DrupalCacheArray {
  *   themes/bartik.
  *   themes/bartik.
  *
  *
  * @see theme()
  * @see theme()
- * @see _theme_process_registry()
+ * @see _theme_build_registry()
  * @see hook_theme()
  * @see hook_theme()
  * @see list_themes()
  * @see list_themes()
  */
  */
@@ -869,11 +869,18 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
 /**
 /**
  * Generates themed output.
  * Generates themed output.
  *
  *
- * All requests for themed output must go through this function. It examines
- * the request and routes it to the appropriate
+ * All requests for themed output must go through this function (however,
+ * calling the theme() function directly is strongly discouraged - see next
+ * paragraph). It examines the request and routes it to the appropriate
  * @link themeable theme function or template @endlink, by checking the theme
  * @link themeable theme function or template @endlink, by checking the theme
  * registry.
  * registry.
  *
  *
+ * Avoid calling this function directly. It is preferable to replace direct
+ * calls to the theme() function with calls to drupal_render() by passing a
+ * render array with a #theme key to drupal_render(), which in turn calls
+ * theme().
+ *
+ * @section sec_theme_hooks Theme Hooks
  * Most commonly, the first argument to this function is the name of the theme
  * 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
  * hook. For instance, to theme a taxonomy term, the theme hook name is
  * 'taxonomy_term'. Modules register theme hooks within a hook_theme()
  * 'taxonomy_term'. Modules register theme hooks within a hook_theme()
@@ -885,6 +892,7 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * underscores changed to hyphens, so for the 'taxonomy_term' theme hook, the
  * underscores changed to hyphens, so for the 'taxonomy_term' theme hook, the
  * default template is 'taxonomy-term.tpl.php'.
  * default template is 'taxonomy-term.tpl.php'.
  *
  *
+ * @subsection sub_overriding_theme_hooks Overriding Theme Hooks
  * Themes may also register new theme hooks within a hook_theme()
  * Themes may also register new theme hooks within a hook_theme()
  * implementation, but it is more common for themes to override default
  * implementation, but it is more common for themes to override default
  * implementations provided by modules than to register entirely new theme
  * implementations provided by modules than to register entirely new theme
@@ -897,6 +905,7 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * rendering engine, it overrides the default implementation of the 'page' theme
  * rendering engine, it overrides the default implementation of the 'page' theme
  * hook by containing a 'page.tpl.php' file within its folder structure).
  * hook by containing a 'page.tpl.php' file within its folder structure).
  *
  *
+ * @subsection sub_preprocess_templates Preprocessing for Template Files
  * If the implementation is a template file, several functions are called
  * If the implementation is a template file, several functions are called
  * before the template file is invoked, to modify the $variables array. These
  * before the template file is invoked, to modify the $variables array. These
  * fall into the "preprocessing" phase and the "processing" phase, and are
  * fall into the "preprocessing" phase and the "processing" phase, and are
@@ -945,12 +954,14 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * - THEME_process_HOOK(&$variables):  Allows the theme to process the
  * - THEME_process_HOOK(&$variables):  Allows the theme to process the
  *   variables specific to the theme hook.
  *   variables specific to the theme hook.
  *
  *
+ * @subsection sub_preprocess_theme_funcs Preprocessing for Theme Functions
  * If the implementation is a function, only the theme-hook-specific preprocess
  * If the implementation is a function, only the theme-hook-specific preprocess
  * and process functions (the ones ending in _HOOK) are called from the
  * and process functions (the ones ending in _HOOK) are called from the
  * list above. This is because theme hooks with function implementations
  * list above. This is because theme hooks with function implementations
  * need to be fast, and calling the non-theme-hook-specific preprocess and
  * need to be fast, and calling the non-theme-hook-specific preprocess and
  * process functions for them would incur a noticeable performance penalty.
  * process functions for them would incur a noticeable performance penalty.
  *
  *
+ * @subsection sub_alternate_suggestions Suggesting Alternate Hooks
  * There are two special variables that these preprocess and process functions
  * There are two special variables that these preprocess and process functions
  * can set: 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be
  * 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,
  * merged together to form a list of 'suggested' alternate theme hooks to use,
@@ -992,6 +1003,7 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * @return
  * @return
  *   An HTML string representing the themed output.
  *   An HTML string representing the themed output.
  *
  *
+ * @see drupal_render()
  * @see themeable
  * @see themeable
  * @see hook_theme()
  * @see hook_theme()
  * @see template_preprocess()
  * @see template_preprocess()
@@ -1017,6 +1029,7 @@ function theme($hook, $variables = array()) {
     }
     }
     $hook = $candidate;
     $hook = $candidate;
   }
   }
+  $theme_hook_original = $hook;
 
 
   // If there's no implementation, check for more generic fallbacks. If there's
   // If there's no implementation, check for more generic fallbacks. If there's
   // still no implementation, log an error and return an empty string.
   // still no implementation, log an error and return an empty string.
@@ -1078,6 +1091,8 @@ function theme($hook, $variables = array()) {
     $variables += array($info['render element'] => array());
     $variables += array($info['render element'] => array());
   }
   }
 
 
+  $variables['theme_hook_original'] = $theme_hook_original;
+
   // Invoke the variable processors, if any. The processors may specify
   // Invoke the variable processors, if any. The processors may specify
   // alternate suggestions for which hook's template/function to use. If the
   // 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
   // hook is a suggestion of a base hook, invoke the variable processors of
@@ -1186,7 +1201,12 @@ function theme($hook, $variables = array()) {
     if (isset($info['path'])) {
     if (isset($info['path'])) {
       $template_file = $info['path'] . '/' . $template_file;
       $template_file = $info['path'] . '/' . $template_file;
     }
     }
-    $output = $render_function($template_file, $variables);
+    if (variable_get('theme_debug', FALSE)) {
+      $output = _theme_render_template_debug($render_function, $template_file, $variables, $extension);
+    }
+    else {
+      $output = $render_function($template_file, $variables);
+    }
   }
   }
 
 
   // restore path_to_theme()
   // restore path_to_theme()
@@ -1228,6 +1248,7 @@ function path_to_theme() {
 function drupal_find_theme_functions($cache, $prefixes) {
 function drupal_find_theme_functions($cache, $prefixes) {
   $implementations = array();
   $implementations = array();
   $functions = get_defined_functions();
   $functions = get_defined_functions();
+  $theme_functions = preg_grep('/^(' . implode(')|(', $prefixes) . ')_/', $functions['user']);
 
 
   foreach ($cache as $hook => $info) {
   foreach ($cache as $hook => $info) {
     foreach ($prefixes as $prefix) {
     foreach ($prefixes as $prefix) {
@@ -1244,7 +1265,7 @@ function drupal_find_theme_functions($cache, $prefixes) {
       // intermediary suggestion.
       // intermediary suggestion.
       $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
       $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
       if (!isset($info['base hook']) && !empty($pattern)) {
       if (!isset($info['base hook']) && !empty($pattern)) {
-        $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $functions['user']);
+        $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $theme_functions);
         if ($matches) {
         if ($matches) {
           foreach ($matches as $match) {
           foreach ($matches as $match) {
             $new_hook = substr($match, strlen($prefix) + 1);
             $new_hook = substr($match, strlen($prefix) + 1);
@@ -1508,6 +1529,72 @@ function theme_render_template($template_file, $variables) {
   return ob_get_clean();
   return ob_get_clean();
 }
 }
 
 
+/**
+ * Renders a template for any engine.
+ *
+ * Includes the possibility to get debug output by setting the
+ * theme_debug variable to TRUE.
+ *
+ * @param string $template_function
+ *   The function to call for rendering the template.
+ * @param string $template_file
+ *   The filename of the template to render.
+ * @param array $variables
+ *   A keyed array of variables that will appear in the output.
+ * @param string $extension
+ *   The extension used by the theme engine for template files.
+ *
+ * @return string
+ *   The output generated by the template including debug information.
+ */
+function _theme_render_template_debug($template_function, $template_file, $variables, $extension) {
+  $output = array(
+    'debug_prefix' => '',
+    'debug_info' => '',
+    'rendered_markup' => call_user_func($template_function, $template_file, $variables),
+    'debug_suffix' => '',
+  );
+  $output['debug_prefix'] .= "\n\n<!-- THEME DEBUG -->";
+  $output['debug_prefix'] .= "\n<!-- CALL: theme('" . check_plain($variables['theme_hook_original']) . "') -->";
+  // If there are theme suggestions, reverse the array so more specific
+  // suggestions are shown first.
+  if (!empty($variables['theme_hook_suggestions'])) {
+    $variables['theme_hook_suggestions'] = array_reverse($variables['theme_hook_suggestions']);
+  }
+  // Add debug output for directly called suggestions like
+  // '#theme' => 'comment__node__article'.
+  if (strpos($variables['theme_hook_original'], '__') !== FALSE) {
+    $derived_suggestions[] = $hook = $variables['theme_hook_original'];
+    while ($pos = strrpos($hook, '__')) {
+      $hook = substr($hook, 0, $pos);
+      $derived_suggestions[] = $hook;
+    }
+    // Get the value of the base hook (last derived suggestion) and append it
+    // to the end of all theme suggestions.
+    $base_hook = array_pop($derived_suggestions);
+    $variables['theme_hook_suggestions'] = array_merge($derived_suggestions, $variables['theme_hook_suggestions']);
+    $variables['theme_hook_suggestions'][] = $base_hook;
+  }
+  if (!empty($variables['theme_hook_suggestions'])) {
+    $current_template = basename($template_file);
+    $suggestions = $variables['theme_hook_suggestions'];
+    // Only add the original theme hook if it wasn't a directly called
+    // suggestion.
+    if (strpos($variables['theme_hook_original'], '__') === FALSE) {
+      $suggestions[] = $variables['theme_hook_original'];
+    }
+    foreach ($suggestions as &$suggestion) {
+      $template = strtr($suggestion, '_', '-') . $extension;
+      $prefix = ($template == $current_template) ? 'x' : '*';
+      $suggestion = $prefix . ' ' . $template;
+    }
+    $output['debug_info'] .= "\n<!-- FILE NAME SUGGESTIONS:\n   " . check_plain(implode("\n   ", $suggestions)) . "\n-->";
+  }
+  $output['debug_info'] .= "\n<!-- BEGIN OUTPUT from '" . check_plain($template_file) . "' -->\n";
+  $output['debug_suffix'] .= "\n<!-- END OUTPUT from '" . check_plain($template_file) . "' -->\n\n";
+  return implode('', $output);
+}
+
 /**
 /**
  * Enables a given list of themes.
  * Enables a given list of themes.
  *
  *
@@ -1605,7 +1692,7 @@ function theme_status_messages($variables) {
       $output .= " </ul>\n";
       $output .= " </ul>\n";
     }
     }
     else {
     else {
-      $output .= $messages[0];
+      $output .= reset($messages);
     }
     }
     $output .= "</div>\n";
     $output .= "</div>\n";
   }
   }
@@ -1624,11 +1711,29 @@ function theme_status_messages($variables) {
  * copy if none of the enabled modules or the active theme implement any
  * copy if none of the enabled modules or the active theme implement any
  * preprocess or process functions or override this theme implementation.
  * 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.
+ * @param array $variables
+ *   An associative array containing the keys:
+ *   - text: The text of the link.
+ *   - path: The internal path or external URL being linked to. It is used as
+ *     the $path parameter of the url() function.
+ *   - options: (optional) An array that defaults to empty, but can contain:
+ *     - attributes: Can contain optional attributes:
+ *       - class: must be declared in an array. Example: 'class' =>
+ *         array('class_name1','class_name2').
+ *       - title: must be a string. Example: 'title' => 'Example title'
+ *       - Others are more flexible as long as they work with
+ *         drupal_attributes($variables['options']['attributes]).
+ *     - html: Boolean flag that tells whether text contains HTML or plain
+ *       text. If set to TRUE, the text value will not be sanitized so the
+         calling function must ensure that it already contains safe HTML.
+ *   The elements $variables['options']['attributes'] and
+ *   $variables['options']['html'] are used in this function similarly to the
+ *   way that $options['attributes'] and $options['html'] are used in l().
+ *   The link itself is built by the url() function, which takes
+ *   $variables['path'] and $variables['options'] as arguments.
  *
  *
  * @see l()
  * @see l()
+ * @see url()
  */
  */
 function theme_link($variables) {
 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>';
   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>';
@@ -1671,15 +1776,13 @@ function theme_link($variables) {
  *     http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
  *     http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
  */
  */
 function theme_links($variables) {
 function theme_links($variables) {
-  $links = $variables['links'];
-  $attributes = $variables['attributes'];
+  $links = (array) $variables['links'];
+  $attributes = (array) $variables['attributes'];
   $heading = $variables['heading'];
   $heading = $variables['heading'];
   global $language_url;
   global $language_url;
   $output = '';
   $output = '';
 
 
-  if (count($links) > 0) {
-    $output = '';
-
+  if (!empty($links)) {
     // Treat the heading first if it is present to prepend it to the
     // Treat the heading first if it is present to prepend it to the
     // list of links.
     // list of links.
     if (!empty($heading)) {
     if (!empty($heading)) {
@@ -1707,7 +1810,8 @@ function theme_links($variables) {
     foreach ($links as $key => $link) {
     foreach ($links as $key => $link) {
       $class = array($key);
       $class = array($key);
 
 
-      // Add first, last and active classes to the list of links to help out themers.
+      // Add first, last and active classes to the list of links to help out
+      // themers.
       if ($i == 1) {
       if ($i == 1) {
         $class[] = 'first';
         $class[] = 'first';
       }
       }
@@ -1725,7 +1829,8 @@ function theme_links($variables) {
         $output .= l($link['title'], $link['href'], $link);
         $output .= l($link['title'], $link['href'], $link);
       }
       }
       elseif (!empty($link['title'])) {
       elseif (!empty($link['title'])) {
-        // Some links are actually not links, but we wrap these in <span> for adding title and class attributes.
+        // Some links are actually not links, but we wrap these in <span> for
+        // adding title and class attributes.
         if (empty($link['html'])) {
         if (empty($link['html'])) {
           $link['title'] = check_plain($link['title']);
           $link['title'] = check_plain($link['title']);
         }
         }
@@ -1806,7 +1911,7 @@ function theme_breadcrumb($variables) {
 /**
 /**
  * Returns HTML for a table.
  * Returns HTML for a table.
  *
  *
- * @param $variables
+ * @param array $variables
  *   An associative array containing:
  *   An associative array containing:
  *   - header: An array containing the table headers. Each element of the array
  *   - header: An array containing the table headers. Each element of the array
  *     can be either a localized string or an associative array with the
  *     can be either a localized string or an associative array with the
@@ -1814,7 +1919,9 @@ function theme_breadcrumb($variables) {
  *     - "data": The localized title of the table column.
  *     - "data": The localized title of the table column.
  *     - "field": The database field represented in the table column (required
  *     - "field": The database field represented in the table column (required
  *       if user is to be able to sort on this column).
  *       if user is to be able to sort on this column).
- *     - "sort": A default sort order for this column ("asc" or "desc").
+ *     - "sort": A default sort order for this column ("asc" or "desc"). Only
+ *       one column should be given a default sort order because table sorting
+ *       only applies to one column at a time.
  *     - Any HTML attributes, such as "colspan", to apply to the column header
  *     - Any HTML attributes, such as "colspan", to apply to the column header
  *       cell.
  *       cell.
  *   - rows: An array of table rows. Every row is an array of cells, or an
  *   - rows: An array of table rows. Every row is an array of cells, or an
@@ -1841,6 +1948,11 @@ function theme_breadcrumb($variables) {
  *       )
  *       )
  *     );
  *     );
  *     @endcode
  *     @endcode
+ *   - footer: An array of table rows which will be printed within a <tfoot>
+ *     tag, in the same format as the rows element (see above).
+ *     The structure is the same the one defined for the "rows" key except
+ *     that the no_striping boolean has no effect, there is no rows striping
+ *     for the table footer.
  *   - attributes: An array of HTML attributes to apply to the table tag.
  *   - attributes: An array of HTML attributes to apply to the table tag.
  *   - caption: A localized string to use for the <caption> tag.
  *   - caption: A localized string to use for the <caption> tag.
  *   - colgroups: An array of column groups. Each element of the array can be
  *   - colgroups: An array of column groups. Each element of the array can be
@@ -1877,8 +1989,11 @@ function theme_breadcrumb($variables) {
  *   - sticky: Use a "sticky" table header.
  *   - sticky: Use a "sticky" table header.
  *   - empty: The message to display in an extra row if table does not have any
  *   - empty: The message to display in an extra row if table does not have any
  *     rows.
  *     rows.
+ *
+ * @return string
+ *   The HTML output.
  */
  */
-function theme_table($variables) {
+function theme_table(array $variables) {
   $header = $variables['header'];
   $header = $variables['header'];
   $rows = $variables['rows'];
   $rows = $variables['rows'];
   $attributes = $variables['attributes'];
   $attributes = $variables['attributes'];
@@ -1888,7 +2003,7 @@ function theme_table($variables) {
   $empty = $variables['empty'];
   $empty = $variables['empty'];
 
 
   // Add sticky headers, if applicable.
   // Add sticky headers, if applicable.
-  if (count($header) && $sticky) {
+  if (!empty($header) && $sticky) {
     drupal_add_js('misc/tableheader.js');
     drupal_add_js('misc/tableheader.js');
     // Add 'sticky-enabled' class to the table to identify it for JS.
     // Add 'sticky-enabled' class to the table to identify it for JS.
     // This is needed to target tables constructed by this function.
     // This is needed to target tables constructed by this function.
@@ -1902,7 +2017,7 @@ function theme_table($variables) {
   }
   }
 
 
   // Format the table columns:
   // Format the table columns:
-  if (count($colgroups)) {
+  if (!empty($colgroups)) {
     foreach ($colgroups as $number => $colgroup) {
     foreach ($colgroups as $number => $colgroup) {
       $attributes = array();
       $attributes = array();
 
 
@@ -1937,66 +2052,94 @@ function theme_table($variables) {
   }
   }
 
 
   // Add the 'empty' row message if available.
   // Add the 'empty' row message if available.
-  if (!count($rows) && $empty) {
+  if (empty($rows) && $empty) {
     $header_count = 0;
     $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++;
+    if (!empty($header)) {
+      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')));
+    $rows[] = array(
+      array(
+        'data' => $empty,
+        'colspan' => $header_count,
+        'class' => array(
+          'empty',
+          'message'
+        ),
+      ),
+    );
   }
   }
 
 
-  // Format the table header:
-  if (count($header)) {
+  // Format the table header.
+  if (!empty($header)) {
     $ts = tablesort_init($header);
     $ts = tablesort_init($header);
     // HTML requires that the thead tag has tr tags in it followed by tbody
     // 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.
     // tags. Using ternary operator to check and see if we have any rows.
-    $output .= (count($rows) ? ' <thead><tr>' : ' <tr>');
+    $output .= (!empty($rows) ? ' <thead><tr>' : ' <tr>');
     foreach ($header as $cell) {
     foreach ($header as $cell) {
       $cell = tablesort_header($cell, $header, $ts);
       $cell = tablesort_header($cell, $header, $ts);
       $output .= _theme_table_cell($cell, TRUE);
       $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");
+    // Using ternary operator to close the tags based on whether
+    // or not there are rows.
+    $output .= (!empty($rows) ? " </tr></thead>\n" : "</tr>\n");
   }
   }
   else {
   else {
     $ts = array();
     $ts = array();
   }
   }
 
 
-  // Format the table rows:
-  if (count($rows)) {
-    $output .= "<tbody>\n";
+  // Format the table and footer rows.
+  $sections = array();
+
+  if (!empty($rows)) {
+    $sections['tbody'] = $rows;
+  }
+
+  if (!empty($variables['footer'])) {
+    $sections['tfoot'] = $variables['footer'];
+  }
+
+  // tbody and tfoot have the same structure and are built using the same
+  // procedure.
+  foreach ($sections as $tag => $content) {
+    $output .= "<" . $tag . ">\n";
     $flip = array('even' => 'odd', 'odd' => 'even');
     $flip = array('even' => 'odd', 'odd' => 'even');
     $class = 'even';
     $class = 'even';
-    foreach ($rows as $number => $row) {
-      $attributes = array();
+    $default_no_striping = ($tag === 'tfoot');
 
 
-      // Check if we're dealing with a simple or complex row
+    foreach ($content as $number => $row) {
+      // Check if we're dealing with a simple or complex row.
       if (isset($row['data'])) {
       if (isset($row['data'])) {
-        foreach ($row as $key => $value) {
-          if ($key == 'data') {
-            $cells = $value;
-          }
-          else {
-            $attributes[$key] = $value;
-          }
-        }
+        $cells = $row['data'];
+        $no_striping = isset($row['no_striping']) ?
+          $row['no_striping'] : $default_no_striping;
+
+        // Set the attributes array and exclude 'data' and 'no_striping'.
+        $attributes = $row;
+        unset($attributes['data']);
+        unset($attributes['no_striping']);
       }
       }
       else {
       else {
         $cells = $row;
         $cells = $row;
+        $attributes = array();
+        $no_striping = $default_no_striping;
       }
       }
-      if (count($cells)) {
-        // Add odd/even class
-        if (empty($row['no_striping'])) {
+
+      if (!empty($cells)) {
+        // Add odd/even class.
+        if (!$no_striping) {
           $class = $flip[$class];
           $class = $flip[$class];
           $attributes['class'][] = $class;
           $attributes['class'][] = $class;
         }
         }
 
 
-        // Build row
+        // Build row.
         $output .= ' <tr' . drupal_attributes($attributes) . '>';
         $output .= ' <tr' . drupal_attributes($attributes) . '>';
         $i = 0;
         $i = 0;
         foreach ($cells as $cell) {
         foreach ($cells as $cell) {
@@ -2006,10 +2149,12 @@ function theme_table($variables) {
         $output .= " </tr>\n";
         $output .= " </tr>\n";
       }
       }
     }
     }
-    $output .= "</tbody>\n";
+
+    $output .= "</" . $tag . ">\n";
   }
   }
 
 
   $output .= "</table>\n";
   $output .= "</table>\n";
+
   return $output;
   return $output;
 }
 }
 
 
@@ -2533,10 +2678,13 @@ function template_preprocess_page(&$variables) {
   // Move some variables to the top level for themer convenience and template cleanliness.
   // Move some variables to the top level for themer convenience and template cleanliness.
   $variables['show_messages'] = $variables['page']['#show_messages'];
   $variables['show_messages'] = $variables['page']['#show_messages'];
 
 
-  foreach (system_region_list($GLOBALS['theme']) as $region_key => $region_name) {
+  foreach (system_region_list($GLOBALS['theme'], REGIONS_ALL, FALSE) as $region_key) {
     if (!isset($variables['page'][$region_key])) {
     if (!isset($variables['page'][$region_key])) {
       $variables['page'][$region_key] = array();
       $variables['page'][$region_key] = array();
     }
     }
+    if ($region_content = drupal_get_region_content($region_key)) {
+      $variables['page'][$region_key][]['#markup'] = $region_content;
+    }
   }
   }
 
 
   // Set up layout variable.
   // Set up layout variable.

+ 9 - 5
includes/unicode.inc

@@ -116,11 +116,15 @@ function _unicode_check() {
   if (ini_get('mbstring.encoding_translation') != 0) {
   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')));
     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')));
+  // mbstring.http_input and mbstring.http_output are deprecated and empty by
+  // default in PHP 5.6.
+  if (version_compare(PHP_VERSION, '5.6.0') == -1) {
+    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
   // Set appropriate configuration

+ 12 - 0
includes/update.inc

@@ -795,6 +795,14 @@ function update_fix_d7_requirements() {
 function update_fix_d7_install_profile() {
 function update_fix_d7_install_profile() {
   $profile = drupal_get_profile();
   $profile = drupal_get_profile();
 
 
+  // 'Default' profile has been renamed to 'Standard' in D7.
+  // We change the profile here to prevent a broken record in the system table.
+  // See system_update_7049().
+  if ($profile == 'default') {
+    $profile = 'standard';
+    variable_set('install_profile', $profile);
+  }
+
   $results = db_select('system', 's')
   $results = db_select('system', 's')
     ->fields('s', array('name', 'schema_version'))
     ->fields('s', array('name', 'schema_version'))
     ->condition('name', $profile)
     ->condition('name', $profile)
@@ -908,6 +916,8 @@ function update_get_d6_session_name() {
 }
 }
 
 
 /**
 /**
+ * Implements callback_batch_operation().
+ *
  * Performs one update and stores the results for display on the results page.
  * Performs one update and stores the results for display on the results page.
  *
  *
  * If an update function completes successfully, it should return a message
  * If an update function completes successfully, it should return a message
@@ -1078,6 +1088,8 @@ function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $
 }
 }
 
 
 /**
 /**
+ * Implements callback_batch_finished().
+ *
  * Finishes the update process and stores the results for eventual display.
  * Finishes the update process and stores the results for eventual display.
  *
  *
  * After the updates run, all caches are flushed. The update results are
  * After the updates run, all caches are flushed. The update results are

+ 35 - 1
includes/xmlrpc.inc

@@ -178,7 +178,41 @@ function xmlrpc_message_parse($xmlrpc_message) {
   xml_set_element_handler($xmlrpc_message->_parser, 'xmlrpc_message_tag_open', 'xmlrpc_message_tag_close');
   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');
   xml_set_character_data_handler($xmlrpc_message->_parser, 'xmlrpc_message_cdata');
   xmlrpc_message_set($xmlrpc_message);
   xmlrpc_message_set($xmlrpc_message);
-  if (!xml_parse($xmlrpc_message->_parser, $xmlrpc_message->message)) {
+
+  // Strip XML declaration.
+  $header = preg_replace('/<\?xml.*?\?'.'>/s', '', substr($xmlrpc_message->message, 0, 100), 1);
+  $xml = trim(substr_replace($xmlrpc_message->message, $header, 0, 100));
+  if ($xml == '') {
+    return FALSE;
+  }
+  // Strip DTD.
+  $header = preg_replace('/^<!DOCTYPE[^>]*+>/i', '', substr($xml, 0, 200), 1);
+  $xml = trim(substr_replace($xml, $header, 0, 200));
+  if ($xml == '') {
+    return FALSE;
+  }
+  // Confirm the XML now starts with a valid root tag. A root tag can end in [> \t\r\n]
+  $root_tag = substr($xml, 0, strcspn(substr($xml, 0, 20), "> \t\r\n"));
+  // Reject a second DTD.
+  if (strtoupper($root_tag) == '<!DOCTYPE') {
+    return FALSE;
+  }
+  if (!in_array($root_tag, array('<methodCall', '<methodResponse', '<fault'))) {
+    return FALSE;
+  }
+  // Skip parsing if there is an unreasonably large number of tags.
+  try {
+    $dom = new DOMDocument();
+    @$dom->loadXML($xml);
+    if ($dom->getElementsByTagName('*')->length > variable_get('xmlrpc_message_maximum_tag_count', 30000)) {
+      return FALSE;
+    }
+  }
+  catch (Exception $e) {
+    return FALSE;
+  }
+
+  if (!xml_parse($xmlrpc_message->_parser, $xml)) {
     return FALSE;
     return FALSE;
   }
   }
   xml_parser_free($xmlrpc_message->_parser);
   xml_parser_free($xmlrpc_message->_parser);

+ 11 - 1
includes/xmlrpcs.inc

@@ -9,7 +9,9 @@
  * Invokes XML-RPC methods on this server.
  * Invokes XML-RPC methods on this server.
  *
  *
  * @param array $callbacks
  * @param array $callbacks
- *   Array of external XML-RPC method names with the callbacks they map to.
+ *   Either an associative array of external XML-RPC method names as keys with
+ *   the callbacks they map to as values, or a more complex structure
+ *   describing XML-RPC callbacks as returned from hook_xmlrpc().
  */
  */
 function xmlrpc_server($callbacks) {
 function xmlrpc_server($callbacks) {
   $xmlrpc_server = new stdClass();
   $xmlrpc_server = new stdClass();
@@ -262,6 +264,10 @@ function xmlrpc_server_call($xmlrpc_server, $methodname, $args) {
  */
  */
 function xmlrpc_server_multicall($methodcalls) {
 function xmlrpc_server_multicall($methodcalls) {
   // See http://www.xmlrpc.com/discuss/msgReader$1208
   // See http://www.xmlrpc.com/discuss/msgReader$1208
+  // To avoid multicall expansion attacks, limit the number of duplicate method
+  // calls allowed with a default of 1. Set to -1 for unlimited.
+  $duplicate_method_limit = variable_get('xmlrpc_multicall_duplicate_method_limit', 1);
+  $method_count = array();
   $return = array();
   $return = array();
   $xmlrpc_server = xmlrpc_server_get();
   $xmlrpc_server = xmlrpc_server_get();
   foreach ($methodcalls as $call) {
   foreach ($methodcalls as $call) {
@@ -271,10 +277,14 @@ function xmlrpc_server_multicall($methodcalls) {
       $ok = FALSE;
       $ok = FALSE;
     }
     }
     $method = $call['methodName'];
     $method = $call['methodName'];
+    $method_count[$method] = isset($method_count[$method]) ? $method_count[$method] + 1 : 1;
     $params = $call['params'];
     $params = $call['params'];
     if ($method == 'system.multicall') {
     if ($method == 'system.multicall') {
       $result = xmlrpc_error(-32600, t('Recursive calls to system.multicall are forbidden.'));
       $result = xmlrpc_error(-32600, t('Recursive calls to system.multicall are forbidden.'));
     }
     }
+    elseif ($duplicate_method_limit > 0 && $method_count[$method] > $duplicate_method_limit) {
+      $result = xmlrpc_error(-156579, t('Too many duplicate method calls in system.multicall.'));
+    }
     elseif ($ok) {
     elseif ($ok) {
       $result = xmlrpc_server_call($xmlrpc_server, $method, $params);
       $result = xmlrpc_server_call($xmlrpc_server, $method, $params);
     }
     }

+ 83 - 8
misc/ajax.js

@@ -14,6 +14,8 @@
 
 
 Drupal.ajax = Drupal.ajax || {};
 Drupal.ajax = Drupal.ajax || {};
 
 
+Drupal.settings.urlIsAjaxTrusted = Drupal.settings.urlIsAjaxTrusted || {};
+
 /**
 /**
  * Attaches the Ajax behavior to each Ajax form element.
  * Attaches the Ajax behavior to each Ajax form element.
  */
  */
@@ -130,6 +132,11 @@ Drupal.ajax = function (base, element, element_settings) {
   // 5. /nojs# - Followed by a fragment.
   // 5. /nojs# - Followed by a fragment.
   //      E.g.: path/nojs#myfragment
   //      E.g.: path/nojs#myfragment
   this.url = element_settings.url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
   this.url = element_settings.url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
+  // If the 'nojs' version of the URL is trusted, also trust the 'ajax' version.
+  if (Drupal.settings.urlIsAjaxTrusted[element_settings.url]) {
+    Drupal.settings.urlIsAjaxTrusted[this.url] = true;
+  }
+
   this.wrapper = '#' + element_settings.wrapper;
   this.wrapper = '#' + element_settings.wrapper;
 
 
   // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
   // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
@@ -142,7 +149,7 @@ Drupal.ajax = function (base, element, element_settings) {
   // The 'this' variable will not persist inside of the options object.
   // The 'this' variable will not persist inside of the options object.
   var ajax = this;
   var ajax = this;
   ajax.options = {
   ajax.options = {
-    url: ajax.url,
+    url: Drupal.sanitizeAjaxUrl(ajax.url),
     data: ajax.submit,
     data: ajax.submit,
     beforeSerialize: function (element_settings, options) {
     beforeSerialize: function (element_settings, options) {
       return ajax.beforeSerialize(element_settings, options);
       return ajax.beforeSerialize(element_settings, options);
@@ -155,26 +162,67 @@ Drupal.ajax = function (base, element, element_settings) {
       ajax.ajaxing = true;
       ajax.ajaxing = true;
       return ajax.beforeSend(xmlhttprequest, options);
       return ajax.beforeSend(xmlhttprequest, options);
     },
     },
-    success: function (response, status) {
+    success: function (response, status, xmlhttprequest) {
       // Sanity check for browser support (object expected).
       // Sanity check for browser support (object expected).
       // When using iFrame uploads, responses must be returned as a string.
       // When using iFrame uploads, responses must be returned as a string.
       if (typeof response == 'string') {
       if (typeof response == 'string') {
         response = $.parseJSON(response);
         response = $.parseJSON(response);
       }
       }
+
+      // Prior to invoking the response's commands, verify that they can be
+      // trusted by checking for a response header. See
+      // ajax_set_verification_header() for details.
+      // - Empty responses are harmless so can bypass verification. This avoids
+      //   an alert message for server-generated no-op responses that skip Ajax
+      //   rendering.
+      // - Ajax objects with trusted URLs (e.g., ones defined server-side via
+      //   #ajax) can bypass header verification. This is especially useful for
+      //   Ajax with multipart forms. Because IFRAME transport is used, the
+      //   response headers cannot be accessed for verification.
+      if (response !== null && !Drupal.settings.urlIsAjaxTrusted[ajax.url]) {
+        if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') {
+          var customMessage = Drupal.t("The response failed verification so will not be processed.");
+          return ajax.error(xmlhttprequest, ajax.url, customMessage);
+        }
+      }
+
       return ajax.success(response, status);
       return ajax.success(response, status);
     },
     },
-    complete: function (response, status) {
+    complete: function (xmlhttprequest, status) {
       ajax.ajaxing = false;
       ajax.ajaxing = false;
       if (status == 'error' || status == 'parsererror') {
       if (status == 'error' || status == 'parsererror') {
-        return ajax.error(response, ajax.url);
+        return ajax.error(xmlhttprequest, ajax.url);
       }
       }
     },
     },
     dataType: 'json',
     dataType: 'json',
+    jsonp: false,
     type: 'POST'
     type: 'POST'
   };
   };
 
 
+  // For multipart forms (e.g., file uploads), jQuery Form targets the form
+  // submission to an iframe instead of using an XHR object. The initial "src"
+  // of the iframe, prior to the form submission, is set to options.iframeSrc.
+  // "about:blank" is the semantically correct, standards-compliant, way to
+  // initialize a blank iframe; however, some old IE versions (possibly only 6)
+  // incorrectly report a mixed content warning when iframes with an
+  // "about:blank" src are added to a parent document with an https:// origin.
+  // jQuery Form works around this by defaulting to "javascript:false" instead,
+  // but that breaks on Chrome 83, so here we force the semantically correct
+  // behavior for all browsers except old IE.
+  // @see https://www.drupal.org/project/drupal/issues/3143016
+  // @see https://github.com/jquery-form/form/blob/df9cb101b9c9c085c8d75ad980c7ff1cf62063a1/jquery.form.js#L68
+  // @see https://bugs.chromium.org/p/chromium/issues/detail?id=1084874
+  // @see https://html.spec.whatwg.org/multipage/browsers.html#creating-browsing-contexts
+  // @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
+  if (navigator.userAgent.indexOf("MSIE") === -1) {
+    ajax.options.iframeSrc = 'about:blank';
+  }
+
   // Bind the ajaxSubmit function to the element event.
   // Bind the ajaxSubmit function to the element event.
   $(ajax.element).bind(element_settings.event, function (event) {
   $(ajax.element).bind(element_settings.event, function (event) {
+    if (!Drupal.settings.urlIsAjaxTrusted[ajax.url] && !Drupal.urlIsLocal(ajax.url)) {
+      throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', {'!url': ajax.url}));
+    }
     return ajax.eventResponse(this, event);
     return ajax.eventResponse(this, event);
   });
   });
 
 
@@ -348,7 +396,7 @@ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
     // this is only needed for IFRAME submissions.
     // this is only needed for IFRAME submissions.
     var v = $.fieldValue(this.element);
     var v = $.fieldValue(this.element);
     if (v !== null) {
     if (v !== null) {
-      options.extraData[this.element.name] = v;
+      options.extraData[this.element.name] = Drupal.checkPlain(v);
     }
     }
   }
   }
 
 
@@ -447,8 +495,8 @@ Drupal.ajax.prototype.getEffect = function (response) {
 /**
 /**
  * Handler for the form redirection error.
  * Handler for the form redirection error.
  */
  */
-Drupal.ajax.prototype.error = function (response, uri) {
-  alert(Drupal.ajaxError(response, uri));
+Drupal.ajax.prototype.error = function (xmlhttprequest, uri, customMessage) {
+  Drupal.displayAjaxError(Drupal.ajaxError(xmlhttprequest, uri, customMessage));
   // Remove the progress element.
   // Remove the progress element.
   if (this.progress.element) {
   if (this.progress.element) {
     $(this.progress.element).remove();
     $(this.progress.element).remove();
@@ -462,7 +510,7 @@ Drupal.ajax.prototype.error = function (response, uri) {
   $(this.element).removeClass('progress-disabled').removeAttr('disabled');
   $(this.element).removeClass('progress-disabled').removeAttr('disabled');
   // Reattach behaviors, if they were detached in beforeSerialize().
   // Reattach behaviors, if they were detached in beforeSerialize().
   if (this.form) {
   if (this.form) {
-    var settings = response.settings || this.settings || Drupal.settings;
+    var settings = this.settings || Drupal.settings;
     Drupal.attachBehaviors(this.form, settings);
     Drupal.attachBehaviors(this.form, settings);
   }
   }
 };
 };
@@ -616,6 +664,33 @@ Drupal.ajax.prototype.commands = {
       .removeClass('odd even')
       .removeClass('odd even')
       .filter(':even').addClass('odd').end()
       .filter(':even').addClass('odd').end()
       .filter(':odd').addClass('even');
       .filter(':odd').addClass('even');
+  },
+
+  /**
+   * Command to add css.
+   *
+   * Uses the proprietary addImport method if available as browsers which
+   * support that method ignore @import statements in dynamically added
+   * stylesheets.
+   */
+  add_css: function (ajax, response, status) {
+    // Add the styles in the normal way.
+    $('head').prepend(response.data);
+    // Add imports in the styles using the addImport method if available.
+    var match, importMatch = /^@import url\("(.*)"\);$/igm;
+    if (document.styleSheets[0].addImport && importMatch.test(response.data)) {
+      importMatch.lastIndex = 0;
+      while (match = importMatch.exec(response.data)) {
+        document.styleSheets[0].addImport(match[1]);
+      }
+    }
+  },
+
+  /**
+   * Command to update a form's build ID.
+   */
+  updateBuildId: function(ajax, response, status) {
+    $('input[name="form_build_id"][value="' + response['old'] + '"]').val(response['new']);
   }
   }
 };
 };
 
 

+ 11 - 6
misc/autocomplete.js

@@ -114,6 +114,7 @@ Drupal.jsAC.prototype.onkeyup = function (input, e) {
  */
  */
 Drupal.jsAC.prototype.select = function (node) {
 Drupal.jsAC.prototype.select = function (node) {
   this.input.value = $(node).data('autocompleteValue');
   this.input.value = $(node).data('autocompleteValue');
+  $(this.input).trigger('autocompleteSelect', [node]);
 };
 };
 
 
 /**
 /**
@@ -167,7 +168,7 @@ Drupal.jsAC.prototype.unhighlight = function (node) {
 Drupal.jsAC.prototype.hidePopup = function (keycode) {
 Drupal.jsAC.prototype.hidePopup = function (keycode) {
   // Select item if the right key or mousebutton was pressed.
   // Select item if the right key or mousebutton was pressed.
   if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
   if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
-    this.input.value = $(this.selected).data('autocompleteValue');
+    this.select(this.selected);
   }
   }
   // Hide popup.
   // Hide popup.
   var popup = this.popup;
   var popup = this.popup;
@@ -220,7 +221,7 @@ Drupal.jsAC.prototype.found = function (matches) {
   for (key in matches) {
   for (key in matches) {
     $('<li></li>')
     $('<li></li>')
       .html($('<div></div>').html(matches[key]))
       .html($('<div></div>').html(matches[key]))
-      .mousedown(function () { ac.select(this); })
+      .mousedown(function () { ac.hidePopup(this); })
       .mouseover(function () { ac.highlight(this); })
       .mouseover(function () { ac.highlight(this); })
       .mouseout(function () { ac.unhighlight(this); })
       .mouseout(function () { ac.unhighlight(this); })
       .data('autocompleteValue', key)
       .data('autocompleteValue', key)
@@ -270,8 +271,11 @@ Drupal.ACDB.prototype.search = function (searchString) {
   var db = this;
   var db = this;
   this.searchString = searchString;
   this.searchString = searchString;
 
 
-  // See if this string needs to be searched for anyway.
-  searchString = searchString.replace(/^\s+|\s+$/, '');
+  // See if this string needs to be searched for anyway. The pattern ../ is
+  // stripped since it may be misinterpreted by the browser.
+  searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, '');
+  // Skip empty search strings, or search strings ending with a comma, since
+  // that is the separator between search terms.
   if (searchString.length <= 0 ||
   if (searchString.length <= 0 ||
     searchString.charAt(searchString.length - 1) == ',') {
     searchString.charAt(searchString.length - 1) == ',') {
     return;
     return;
@@ -293,8 +297,9 @@ Drupal.ACDB.prototype.search = function (searchString) {
     // encodeURIComponent to allow autocomplete search terms to contain slashes.
     // encodeURIComponent to allow autocomplete search terms to contain slashes.
     $.ajax({
     $.ajax({
       type: 'GET',
       type: 'GET',
-      url: db.uri + '/' + Drupal.encodePath(searchString),
+      url: Drupal.sanitizeAjaxUrl(db.uri + '/' + Drupal.encodePath(searchString)),
       dataType: 'json',
       dataType: 'json',
+      jsonp: false,
       success: function (matches) {
       success: function (matches) {
         if (typeof matches.status == 'undefined' || matches.status != 0) {
         if (typeof matches.status == 'undefined' || matches.status != 0) {
           db.cache[searchString] = matches;
           db.cache[searchString] = matches;
@@ -306,7 +311,7 @@ Drupal.ACDB.prototype.search = function (searchString) {
         }
         }
       },
       },
       error: function (xmlhttp) {
       error: function (xmlhttp) {
-        alert(Drupal.ajaxError(xmlhttp, db.uri));
+        Drupal.displayAjaxError(Drupal.ajaxError(xmlhttp, db.uri));
       }
       }
     });
     });
   }, this.delay);
   }, this.delay);

+ 4 - 0
misc/brumann/polyfill-unserialize/.gitignore

@@ -0,0 +1,4 @@
+/vendor/
+/phpunit.xml
+/.composer.lock
+

+ 20 - 0
misc/brumann/polyfill-unserialize/.travis.yml

@@ -0,0 +1,20 @@
+language: php
+
+sudo: false
+
+php:
+  - '5.3'
+  - '5.4'
+  - '5.5'
+  - '5.6'
+  - '7.0'
+  - '7.1'
+
+before_install:
+  - phpenv config-rm xdebug.ini
+  - composer self-update
+
+install:
+  - composer install
+
+script: phpunit

+ 21 - 0
misc/brumann/polyfill-unserialize/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Denis Brumann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 61 - 0
misc/brumann/polyfill-unserialize/README.md

@@ -0,0 +1,61 @@
+Polyfill unserialize [![Build Status](https://travis-ci.org/dbrumann/polyfill-unserialize.svg?branch=master)](https://travis-ci.org/dbrumann/polyfill-unserialize)
+===
+
+Backports unserialize options introduced in PHP 7.0 to older PHP versions.
+This was originally designed as a Proof of Concept for Symfony Issue [#21090](https://github.com/symfony/symfony/pull/21090).
+
+You can use this package in projects that rely on PHP versions older than PHP 7.0.
+In case you are using PHP 7.0+ the original `unserialize()` will be used instead.
+
+From the [documentation](https://secure.php.net/manual/en/function.unserialize.php):
+
+> Warning: Do not pass untrusted user input to unserialize(). Unserialization can
+> result in code being loaded and executed due to object instantiation
+> and autoloading, and a malicious user may be able to exploit this.
+
+This warning holds true even when `allowed_classes` is used.
+
+Requirements
+------------
+
+ - PHP 5.3+
+
+Installation
+------------
+
+You can install this package via composer:
+
+```
+composer require brumann/polyfill-unserialize "^1.0"
+```
+
+Known Issues
+------------
+
+There is a mismatch in behavior when `allowed_classes` in `$options` is not
+of the correct type (array or boolean). PHP 7.1 will issue a warning, whereas
+PHP 7.0 will not. I opted to copy the behavior of the former.
+
+Tests
+-----
+
+You can run the test suite using PHPUnit. It is intentionally not bundled as
+dev dependency to make sure this package has the lowest restrictions on the
+implementing system as possible.
+
+Please read the [PHPUnit Manual](https://phpunit.de/manual/current/en/installation.html)
+for information how to install it on your system.
+
+You can run the test suite as follows:
+
+```
+phpunit -c phpunit.xml.dist tests/
+```
+
+Contributing
+------------
+
+This package is considered feature complete. As such I will likely not update it
+unless there are security issues.
+
+Should you find any bugs or have questions, feel free to submit an Issue or a Pull Request.

+ 26 - 0
misc/brumann/polyfill-unserialize/composer.json

@@ -0,0 +1,26 @@
+{
+    "name": "brumann/polyfill-unserialize",
+    "description": "Backports unserialize options introduced in PHP 7.0 to older PHP versions.",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Denis Brumann",
+            "email": "denis.brumann@sensiolabs.de"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "Brumann\\Polyfill\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Tests\\Brumann\\Polyfill\\": "tests/"
+        }
+    },
+    "minimum-stability": "stable",
+    "require": {
+        "php": "^5.3|^7.0"
+    }
+}

+ 25 - 0
misc/brumann/polyfill-unserialize/phpunit.xml.dist

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
+    backupGlobals="false"
+    colors="true"
+    bootstrap="vendor/autoload.php"
+>
+    <php>
+        <ini name="error_reporting" value="-1" />
+    </php>
+
+    <testsuites>
+        <testsuite name="Brumann\Polyfill Test Suite">
+            <directory>./tests/</directory>
+        </testsuite>
+    </testsuites>
+
+    <filter>
+        <whitelist>
+            <directory>./src/</directory>
+        </whitelist>
+    </filter>
+</phpunit>

+ 58 - 0
misc/brumann/polyfill-unserialize/src/Unserialize.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace Brumann\Polyfill;
+
+final class Unserialize
+{
+    /**
+     * @see https://secure.php.net/manual/en/function.unserialize.php
+     *
+     * @param string $serialized Serialized data
+     * @param array $options Associative array containing options
+     *
+     * @return mixed
+     */
+    public static function unserialize($serialized, array $options = array())
+    {
+        if (PHP_VERSION_ID >= 70000) {
+            return \unserialize($serialized, $options);
+        }
+        if (!array_key_exists('allowed_classes', $options)) {
+            $options['allowed_classes'] = true;
+        }
+        $allowedClasses = $options['allowed_classes'];
+        if (true === $allowedClasses) {
+            return \unserialize($serialized);
+        }
+        if (false === $allowedClasses) {
+            $allowedClasses = array();
+        }
+        if (!is_array($allowedClasses)) {
+            trigger_error(
+                'unserialize(): allowed_classes option should be array or boolean',
+                E_USER_WARNING
+            );
+            $allowedClasses = array();
+        }
+
+        $sanitizedSerialized = preg_replace_callback(
+            '/(^|;)O:\d+:"([^"]*)":(\d+):{/',
+            function ($match) use ($allowedClasses) {
+                list($completeMatch, $leftBorder, $className, $objectSize) = $match;
+                if (in_array($className, $allowedClasses)) {
+                    return $completeMatch;
+                } else {
+                    return sprintf(
+                        '%sO:22:"__PHP_Incomplete_Class":%d:{s:27:"__PHP_Incomplete_Class_Name";%s',
+                        $leftBorder,
+                        $objectSize + 1, // size of object + 1 for added string
+                        \serialize($className)
+                    );
+                }
+            },
+            $serialized
+        );
+
+        return \unserialize($sanitizedSerialized);
+    }
+}

+ 217 - 19
misc/drupal.js

@@ -27,6 +27,42 @@ $.fn.init = function (selector, context, rootjQuery) {
 };
 };
 $.fn.init.prototype = jquery_init.prototype;
 $.fn.init.prototype = jquery_init.prototype;
 
 
+/**
+ * Pre-filter Ajax requests to guard against XSS attacks.
+ *
+ * See https://github.com/jquery/jquery/issues/2432
+ */
+if ($.ajaxPrefilter) {
+  // For newer versions of jQuery, use an Ajax prefilter to prevent
+  // auto-executing script tags from untrusted domains. This is similar to the
+  // fix that is built in to jQuery 3.0 and higher.
+  $.ajaxPrefilter(function (s) {
+    if (s.crossDomain) {
+      s.contents.script = false;
+    }
+  });
+}
+else if ($.httpData) {
+  // For the version of jQuery that ships with Drupal core, override
+  // jQuery.httpData to prevent auto-detecting "script" data types from
+  // untrusted domains.
+  var jquery_httpData = $.httpData;
+  $.httpData = function (xhr, type, s) {
+    // @todo Consider backporting code from newer jQuery versions to check for
+    //   a cross-domain request here, rather than using Drupal.urlIsLocal() to
+    //   block scripts from all URLs that are not on the same site.
+    if (!type && !Drupal.urlIsLocal(s.url)) {
+      var content_type = xhr.getResponseHeader('content-type') || '';
+      if (content_type.indexOf('javascript') >= 0) {
+        // Default to a safe data type.
+        type = 'text';
+      }
+    }
+    return jquery_httpData.call(this, xhr, type, s);
+  };
+  $.httpData.prototype = jquery_httpData.prototype;
+}
+
 /**
 /**
  * Attach all registered behaviors to a page element.
  * Attach all registered behaviors to a page element.
  *
  *
@@ -137,7 +173,7 @@ Drupal.detachBehaviors = function (context, settings, trigger) {
  */
  */
 Drupal.checkPlain = function (str) {
 Drupal.checkPlain = function (str) {
   var character, regex,
   var character, regex,
-      replace = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
+      replace = { '&': '&amp;', "'": '&#39;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
   str = String(str);
   str = String(str);
   for (character in replace) {
   for (character in replace) {
     if (replace.hasOwnProperty(character)) {
     if (replace.hasOwnProperty(character)) {
@@ -168,23 +204,76 @@ Drupal.checkPlain = function (str) {
 Drupal.formatString = function(str, args) {
 Drupal.formatString = function(str, args) {
   // Transform arguments before inserting them.
   // Transform arguments before inserting them.
   for (var key in args) {
   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;
+    if (args.hasOwnProperty(key)) {
+      switch (key.charAt(0)) {
+        // Escaped only.
+        case '@':
+          args[key] = Drupal.checkPlain(args[key]);
+          break;
+        // Pass-through.
+        case '!':
+          break;
+        // Escaped and placeholder.
+        default:
+          args[key] = Drupal.theme('placeholder', args[key]);
+          break;
+      }
     }
     }
-    str = str.replace(key, args[key]);
   }
   }
-  return str;
+
+  return Drupal.stringReplace(str, args, null);
+};
+
+/**
+ * Replace substring.
+ *
+ * The longest keys will be tried first. Once a substring has been replaced,
+ * its new value will not be searched again.
+ *
+ * @param {String} str
+ *   A string with placeholders.
+ * @param {Object} args
+ *   Key-value pairs.
+ * @param {Array|null} keys
+ *   Array of keys from the "args".  Internal use only.
+ *
+ * @return {String}
+ *   Returns the replaced string.
+ */
+Drupal.stringReplace = function (str, args, keys) {
+  if (str.length === 0) {
+    return str;
+  }
+
+  // If the array of keys is not passed then collect the keys from the args.
+  if (!$.isArray(keys)) {
+    keys = [];
+    for (var k in args) {
+      if (args.hasOwnProperty(k)) {
+        keys.push(k);
+      }
+    }
+
+    // Order the keys by the character length. The shortest one is the first.
+    keys.sort(function (a, b) { return a.length - b.length; });
+  }
+
+  if (keys.length === 0) {
+    return str;
+  }
+
+  // Take next longest one from the end.
+  var key = keys.pop();
+  var fragments = str.split(key);
+
+  if (keys.length) {
+    for (var i = 0; i < fragments.length; i++) {
+      // Process each fragment with a copy of remaining keys.
+      fragments[i] = Drupal.stringReplace(fragments[i], args, keys.slice(0));
+    }
+  }
+
+  return fragments.join(args[key]);
 };
 };
 
 
 /**
 /**
@@ -251,7 +340,7 @@ Drupal.t = function (str, args, options) {
  *   A translated string.
  *   A translated string.
  */
  */
 Drupal.formatPlural = function (count, singular, plural, args, options) {
 Drupal.formatPlural = function (count, singular, plural, args, options) {
-  var args = args || {};
+  args = args || {};
   args['@count'] = count;
   args['@count'] = count;
   // Determine the index of the plural form.
   // Determine the index of the plural form.
   var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] == 1) ? 0 : 1);
   var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] == 1) ? 0 : 1);
@@ -269,6 +358,89 @@ Drupal.formatPlural = function (count, singular, plural, args, options) {
   }
   }
 };
 };
 
 
+/**
+ * Returns the passed in URL as an absolute URL.
+ *
+ * @param url
+ *   The URL string to be normalized to an absolute URL.
+ *
+ * @return
+ *   The normalized, absolute URL.
+ *
+ * @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js
+ * @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript
+ * @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53
+ */
+Drupal.absoluteUrl = function (url) {
+  var urlParsingNode = document.createElement('a');
+
+  // Decode the URL first; this is required by IE <= 6. Decoding non-UTF-8
+  // strings may throw an exception.
+  try {
+    url = decodeURIComponent(url);
+  } catch (e) {}
+
+  urlParsingNode.setAttribute('href', url);
+
+  // IE <= 7 normalizes the URL when assigned to the anchor node similar to
+  // the other browsers.
+  return urlParsingNode.cloneNode(false).href;
+};
+
+/**
+ * Returns true if the URL is within Drupal's base path.
+ *
+ * @param url
+ *   The URL string to be tested.
+ *
+ * @return
+ *   Boolean true if local.
+ *
+ * @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58
+ */
+Drupal.urlIsLocal = function (url) {
+  // Always use browser-derived absolute URLs in the comparison, to avoid
+  // attempts to break out of the base path using directory traversal.
+  var absoluteUrl = Drupal.absoluteUrl(url);
+  var protocol = location.protocol;
+
+  // Consider URLs that match this site's base URL but use HTTPS instead of HTTP
+  // as local as well.
+  if (protocol === 'http:' && absoluteUrl.indexOf('https:') === 0) {
+    protocol = 'https:';
+  }
+  var baseUrl = protocol + '//' + location.host + Drupal.settings.basePath.slice(0, -1);
+
+  // Decoding non-UTF-8 strings may throw an exception.
+  try {
+    absoluteUrl = decodeURIComponent(absoluteUrl);
+  } catch (e) {}
+  try {
+    baseUrl = decodeURIComponent(baseUrl);
+  } catch (e) {}
+
+  // The given URL matches the site's base URL, or has a path under the site's
+  // base URL.
+  return absoluteUrl === baseUrl || absoluteUrl.indexOf(baseUrl + '/') === 0;
+};
+
+/**
+ * Sanitizes a URL for use with jQuery.ajax().
+ *
+ * @param url
+ *   The URL string to be sanitized.
+ *
+ * @return
+ *   The sanitized URL.
+ */
+Drupal.sanitizeAjaxUrl = function (url) {
+  var regex = /\=\?(&|$)/;
+  while (url.match(regex)) {
+    url = url.replace(regex, '');
+  }
+  return url;
+}
+
 /**
 /**
  * Generate the themed representation of a Drupal object.
  * Generate the themed representation of a Drupal object.
  *
  *
@@ -347,10 +519,33 @@ Drupal.getSelection = function (element) {
   return { 'start': element.selectionStart, 'end': element.selectionEnd };
   return { 'start': element.selectionStart, 'end': element.selectionEnd };
 };
 };
 
 
+/**
+ * Add a global variable which determines if the window is being unloaded.
+ *
+ * This is primarily used by Drupal.displayAjaxError().
+ */
+Drupal.beforeUnloadCalled = false;
+$(window).bind('beforeunload pagehide', function () {
+    Drupal.beforeUnloadCalled = true;
+});
+
+/**
+ * Displays a JavaScript error from an Ajax response when appropriate to do so.
+ */
+Drupal.displayAjaxError = function (message) {
+  // Skip displaying the message if the user deliberately aborted (for example,
+  // by reloading the page or navigating to a different page) while the Ajax
+  // request was still ongoing. See, for example, the discussion at
+  // http://stackoverflow.com/questions/699941/handle-ajax-error-when-a-user-clicks-refresh.
+  if (!Drupal.beforeUnloadCalled) {
+    alert(message);
+  }
+};
+
 /**
 /**
  * Build an error message from an Ajax response.
  * Build an error message from an Ajax response.
  */
  */
-Drupal.ajaxError = function (xmlhttp, uri) {
+Drupal.ajaxError = function (xmlhttp, uri, customMessage) {
   var statusCode, statusText, pathText, responseText, readyStateText, message;
   var statusCode, statusText, pathText, responseText, readyStateText, message;
   if (xmlhttp.status) {
   if (xmlhttp.status) {
     statusCode = "\n" + Drupal.t("An AJAX HTTP error occurred.") +  "\n" + Drupal.t("HTTP Result Code: !status", {'!status': xmlhttp.status});
     statusCode = "\n" + Drupal.t("An AJAX HTTP error occurred.") +  "\n" + Drupal.t("HTTP Result Code: !status", {'!status': xmlhttp.status});
@@ -383,7 +578,10 @@ Drupal.ajaxError = function (xmlhttp, uri) {
   // We don't need readyState except for status == 0.
   // We don't need readyState except for status == 0.
   readyStateText = xmlhttp.status == 0 ? ("\n" + Drupal.t("ReadyState: !readyState", {'!readyState': xmlhttp.readyState})) : "";
   readyStateText = xmlhttp.status == 0 ? ("\n" + Drupal.t("ReadyState: !readyState", {'!readyState': xmlhttp.readyState})) : "";
 
 
-  message = statusCode + pathText + statusText + responseText + readyStateText;
+  // Additional message beyond what the xmlhttp object provides.
+  customMessage = customMessage ? ("\n" + Drupal.t("CustomMessage: !customMessage", {'!customMessage': customMessage})) : "";
+
+  message = statusCode + pathText + statusText + customMessage + responseText + readyStateText;
   return message;
   return message;
 };
 };
 
 

BIN
misc/favicon.ico


+ 112 - 0
misc/jquery-extend-3.4.0.js

@@ -0,0 +1,112 @@
+/**
+ * For jQuery versions less than 3.4.0, this replaces the jQuery.extend
+ * function with the one from jQuery 3.4.0, slightly modified (documented
+ * below) to be compatible with older jQuery versions and browsers.
+ *
+ * This provides the Object.prototype pollution vulnerability fix to Drupal
+ * installations running older jQuery versions, including the versions shipped
+ * with Drupal core and https://www.drupal.org/project/jquery_update.
+ *
+ * @see https://github.com/jquery/jquery/pull/4333
+ */
+
+(function (jQuery) {
+
+// Do not override jQuery.extend() if the jQuery version is already >=3.4.0.
+var versionParts = jQuery.fn.jquery.split('.');
+var majorVersion = parseInt(versionParts[0]);
+var minorVersion = parseInt(versionParts[1]);
+var patchVersion = parseInt(versionParts[2]);
+var isPreReleaseVersion = (patchVersion.toString() !== versionParts[2]);
+if (
+  (majorVersion > 3) ||
+  (majorVersion === 3 && minorVersion > 4) ||
+  (majorVersion === 3 && minorVersion === 4 && patchVersion > 0) ||
+  (majorVersion === 3 && minorVersion === 4 && patchVersion === 0 && !isPreReleaseVersion)
+) {
+  return;
+}
+
+/**
+ * This is almost verbatim copied from jQuery 3.4.0.
+ *
+ * Only two minor changes have been made:
+ * - The call to isFunction() is changed to jQuery.isFunction().
+ * - The two calls to Array.isArray() is changed to jQuery.isArray().
+ *
+ * The above two changes ensure compatibility with all older jQuery versions
+ * (1.4.4 - 3.3.1) and older browser versions (e.g., IE8).
+ */
+jQuery.extend = jQuery.fn.extend = function() {
+  var options, name, src, copy, copyIsArray, clone,
+    target = arguments[ 0 ] || {},
+    i = 1,
+    length = arguments.length,
+    deep = false;
+
+  // Handle a deep copy situation
+  if ( typeof target === "boolean" ) {
+    deep = target;
+
+    // Skip the boolean and the target
+    target = arguments[ i ] || {};
+    i++;
+  }
+
+  // Handle case when target is a string or something (possible in deep copy)
+  if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
+    target = {};
+  }
+
+  // Extend jQuery itself if only one argument is passed
+  if ( i === length ) {
+    target = this;
+    i--;
+  }
+
+  for ( ; i < length; i++ ) {
+
+    // Only deal with non-null/undefined values
+    if ( ( options = arguments[ i ] ) != null ) {
+
+      // Extend the base object
+      for ( name in options ) {
+        copy = options[ name ];
+
+        // Prevent Object.prototype pollution
+        // Prevent never-ending loop
+        if ( name === "__proto__" || target === copy ) {
+          continue;
+        }
+
+        // Recurse if we're merging plain objects or arrays
+        if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
+          ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
+          src = target[ name ];
+
+          // Ensure proper type for the source value
+          if ( copyIsArray && !jQuery.isArray( src ) ) {
+            clone = [];
+          } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
+            clone = {};
+          } else {
+            clone = src;
+          }
+          copyIsArray = false;
+
+          // Never move original objects, clone them
+          target[ name ] = jQuery.extend( deep, clone, copy );
+
+          // Don't bring in undefined values
+        } else if ( copy !== undefined ) {
+          target[ name ] = copy;
+        }
+      }
+    }
+  }
+
+  // Return the modified object
+  return target;
+};
+
+})(jQuery);

+ 251 - 0
misc/jquery-html-prefilter-3.5.0-backport.js

@@ -0,0 +1,251 @@
+/**
+ * For jQuery versions less than 3.5.0, this replaces the jQuery.htmlPrefilter()
+ * function with one that fixes these security vulnerabilities while also
+ * retaining the pre-3.5.0 behavior where it's safe to do so.
+ * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
+ * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
+ *
+ * Additionally, for jQuery versions that do not have a jQuery.htmlPrefilter()
+ * function (1.x prior to 1.12 and 2.x prior to 2.2), this adds it, and
+ * extends the functions that need to call it to do so.
+ *
+ * Drupal core's jQuery version is 1.4.4, but jQuery Update can provide a
+ * different version, so this covers all versions between 1.4.4 and 3.4.1.
+ * The GitHub links in the code comments below link to jQuery 1.5 code, because
+ * 1.4.4 isn't on GitHub, but the referenced code didn't change from 1.4.4 to
+ * 1.5.
+ */
+
+(function (jQuery) {
+
+  // Parts of this backport differ by jQuery version.
+  var versionParts = jQuery.fn.jquery.split('.');
+  var majorVersion = parseInt(versionParts[0]);
+  var minorVersion = parseInt(versionParts[1]);
+
+  // No backport is needed if we're already on jQuery 3.5 or higher.
+  if ( (majorVersion > 3) || (majorVersion === 3 && minorVersion >= 5) ) {
+    return;
+  }
+
+  // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
+  // their XML equivalent: e.g., "<div />" to "<div></div>". This is
+  // problematic for several reasons, including that it's vulnerable to XSS
+  // attacks. However, since this was jQuery's behavior for many years, many
+  // Drupal modules and jQuery plugins may be relying on it. Therefore, we
+  // preserve that behavior, but for a limited set of tags only, that we believe
+  // to not be vulnerable. This is the set of HTML tags that satisfy all of the
+  // following conditions:
+  // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
+  //   appear in that list, then we don't want to mess with it here either.
+  //   @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
+  // - A normal element (not a void, template, text, or foreign element).
+  //   @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
+  // - An element that is still defined by the current HTML specification
+  //   (not a deprecated element), because we do not want to rely on how
+  //   browsers parse deprecated elements.
+  //   @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
+  // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
+  //   designed for fragments, not entire documents.
+  // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
+  //   regular expression, it didn't match on colgroup, and we don't want to
+  //   introduce a behavior change for that.
+  var selfClosingTagsToReplace = [
+    'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
+    'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
+    'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
+    'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
+    'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
+    'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
+    'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
+    'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
+    'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
+    'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
+  ];
+
+  // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
+  // two expressions makes it easier to target <a/> without also targeting
+  // every tag that starts with "a".
+  var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
+  var whitespace = '[\\x20\\t\\r\\n\\f]';
+  var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
+  var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
+
+  // jQuery 3.5 also fixed a vulnerability for when </select> appears within
+  // an <option> or <optgroup>, but it did that in local code that we can't
+  // backport directly. Instead, we filter such cases out. To do so, we need to
+  // determine when jQuery would otherwise invoke the vulnerable code, which it
+  // uses this regular expression to determine. The regular expression changed
+  // for version 3.0.0 and changed again for 3.4.0.
+  // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
+  // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
+  // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
+  var rtagName;
+  if (majorVersion < 3) {
+    rtagName = /<([\w:]+)/;
+  }
+  else if (minorVersion < 4) {
+    rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]+)/i;
+  }
+  else {
+    rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i;
+  }
+
+  // The regular expression that jQuery uses to determine which self-closing
+  // tags to expand to open and close tags. This is vulnerable, because it
+  // matches all tag names except the few excluded ones. We only use this
+  // expression for determining vulnerability. The expression changed for
+  // version 3, but we only need to check for vulnerability in versions 1 and 2,
+  // so we use the expression from those versions.
+  // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
+  var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
+
+  jQuery.extend({
+    htmlPrefilter: function (html) {
+      // This is how jQuery determines the first tag in the HTML.
+      // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
+      var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
+
+      // It is not valid HTML for <option> or <optgroup> to have <select> as
+      // either a descendant or sibling, and attempts to inject one can cause
+      // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
+      // possible XSS attack, reject the entire string.
+      // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
+      if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
+        html = '';
+      }
+
+      // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
+      // the tags in the `selfClosingTagsToReplace` list defined above.
+      // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
+      // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
+      html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
+      html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
+
+      // Prior to jQuery 1.12 and 2.2, this function gets called (via code later
+      // in this file) in addition to, rather than instead of, the unsafe
+      // expansion of self-closing tags (including ones not in the list above).
+      // We can't prevent that unsafe expansion from running, so instead we
+      // check to make sure that it doesn't affect the DOM returned by the
+      // browser's parsing logic. If it does affect it, then it's vulnerable to
+      // XSS, so we reject the entire string.
+      if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
+        var htmlRisky = html.replace(rxhtmlTag, "<$1></$2>");
+        if (htmlRisky !== html) {
+          // Even though htmlRisky and html are different strings, they might
+          // represent the same HTML structure once parsed, in which case,
+          // htmlRisky is actually safe. We can ask the browser to parse both
+          // to find out, but the browser can't parse table fragments (e.g., a
+          // root-level "<td>"), so we need to wrap them. We just need this
+          // technique to work on all supported browsers; we don't need to
+          // copy from the specific jQuery version we're using.
+          // @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L4939
+          var wrapMap = {
+            thead: [ 1, "<table>", "</table>" ],
+            col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
+            tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+            td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+          };
+          wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+          wrapMap.th = wrapMap.td;
+
+          // Function to wrap HTML into something that a browser can parse.
+          // @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L5032
+          var getWrappedHtml = function (html) {
+            var wrap = wrapMap[tag];
+            if (wrap) {
+              html = wrap[1] + html + wrap[2];
+            }
+            return html;
+          };
+
+          // Function to return canonical HTML after parsing it. This parses
+          // only; it doesn't execute scripts.
+          // @see https://github.com/jquery/jquery-migrate/blob/3.3.0/src/jquery/manipulation.js#L5
+          var getParsedHtml = function (html) {
+            var doc = window.document.implementation.createHTMLDocument( "" );
+            doc.body.innerHTML = html;
+            return doc.body ? doc.body.innerHTML : '';
+          };
+
+          // If the browser couldn't parse either one successfully, or if
+          // htmlRisky parses differently than html, then html is vulnerable,
+          // so reject it.
+          var htmlParsed = getParsedHtml(getWrappedHtml(html));
+          var htmlRiskyParsed = getParsedHtml(getWrappedHtml(htmlRisky));
+          if (htmlRiskyParsed === '' || htmlParsed === '' || (htmlRiskyParsed !== htmlParsed)) {
+            html = '';
+          }
+        }
+      }
+
+      return html;
+    }
+  });
+
+  // Prior to jQuery 1.12 and 2.2, jQuery.clean(), jQuery.buildFragment(), and
+  // jQuery.fn.html() did not call jQuery.htmlPrefilter(), so we add that.
+  if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
+    // Filter the HTML coming into jQuery.fn.html().
+    var fnOriginalHtml = jQuery.fn.html;
+    jQuery.fn.extend({
+      // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5147
+      html: function (value) {
+        if (typeof value === "string") {
+          value = jQuery.htmlPrefilter(value);
+        }
+        // .html() can be called as a setter (with an argument) or as a getter
+        // (without an argument), so invoke fnOriginalHtml() the same way that
+        // we were invoked.
+        return fnOriginalHtml.apply(this, arguments.length ? [value] : []);
+      }
+    });
+
+    // The regular expression that jQuery uses to determine if a string is HTML.
+    // Used by both clean() and buildFragment().
+    // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4960
+    var rhtml = /<|&#?\w+;/;
+
+    // Filter HTML coming into:
+    // - jQuery.clean() for versions prior to 1.9.
+    // - jQuery.buildFragment() for 1.9 and above.
+    //
+    // The looping constructs in the two functions might be essentially
+    // identical, but they're each expressed here in the way that most closely
+    // matches their original expression in jQuery, so that we filter all of
+    // the items and only the items that jQuery will treat as HTML strings.
+    if (majorVersion === 1 && minorVersion < 9) {
+      var originalClean = jQuery.clean;
+      jQuery.extend({
+        // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5493
+        'clean': function (elems, context, fragment, scripts) {
+          for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+            if ( typeof elem === "string" && rhtml.test( elem ) ) {
+              elems[i] = elem = jQuery.htmlPrefilter(elem);
+            }
+          }
+          return originalClean.call(this, elems, context, fragment, scripts);
+        }
+      });
+    }
+    else {
+      var originalBuildFragment = jQuery.buildFragment;
+      jQuery.extend({
+        // @see https://github.com/jquery/jquery/blob/1.9.0/jquery.js#L6419
+        'buildFragment': function (elems, context, scripts, selection) {
+          var l = elems.length;
+          for ( var i = 0; i < l; i++ ) {
+            var elem = elems[i];
+            if (elem || elem === 0) {
+              if ( jQuery.type( elem ) !== "object" && rhtml.test( elem ) ) {
+                elems[i] = elem = jQuery.htmlPrefilter(elem);
+              }
+            }
+          }
+          return originalBuildFragment.call(this, elems, context, scripts, selection);
+        }
+      });
+    }
+  }
+
+})(jQuery);

+ 6 - 2
misc/states.js

@@ -373,7 +373,7 @@ states.Trigger.states = {
 
 
   checked: {
   checked: {
     'change': function () {
     'change': function () {
-      return this.attr('checked');
+      return this.is(':checked');
     }
     }
   },
   },
 
 
@@ -493,7 +493,11 @@ $(document).bind('state:disabled', function(e) {
 $(document).bind('state:required', function(e) {
 $(document).bind('state:required', function(e) {
   if (e.trigger) {
   if (e.trigger) {
     if (e.value) {
     if (e.value) {
-      $(e.target).closest('.form-item, .form-wrapper').find('label').append('<span class="form-required">*</span>');
+      var $label = $(e.target).closest('.form-item, .form-wrapper').find('label');
+      // Avoids duplicate required markers on initialization.
+      if (!$label.find('.form-required').length) {
+        $label.append('<span class="form-required">*</span>');
+      }
     }
     }
     else {
     else {
       $(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove();
       $(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove();

+ 45 - 10
misc/tabledrag.js

@@ -106,8 +106,10 @@ Drupal.tableDrag = function (table, tableSettings) {
 
 
   // Add mouse bindings to the document. The self variable is passed along
   // Add mouse bindings to the document. The self variable is passed along
   // as event handlers do not have direct access to the tableDrag object.
   // as event handlers do not have direct access to the tableDrag object.
-  $(document).bind('mousemove', function (event) { return self.dragRow(event, self); });
-  $(document).bind('mouseup', function (event) { return self.dropRow(event, self); });
+  $(document).bind('mousemove pointermove', function (event) { return self.dragRow(event, self); });
+  $(document).bind('mouseup pointerup', function (event) { return self.dropRow(event, self); });
+  $(document).bind('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
+  $(document).bind('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
 };
 };
 
 
 /**
 /**
@@ -274,7 +276,10 @@ Drupal.tableDrag.prototype.makeDraggable = function (item) {
   });
   });
 
 
   // Add the mousedown action for the handle.
   // Add the mousedown action for the handle.
-  handle.mousedown(function (event) {
+  handle.bind('mousedown touchstart pointerdown', function (event) {
+    if (event.originalEvent.type == "touchstart") {
+      event = event.originalEvent.touches[0];
+    }
     // Create a new dragObject recording the event information.
     // Create a new dragObject recording the event information.
     self.dragObject = {};
     self.dragObject = {};
     self.dragObject.initMouseOffset = self.getMouseOffset(item, event);
     self.dragObject.initMouseOffset = self.getMouseOffset(item, event);
@@ -500,7 +505,7 @@ Drupal.tableDrag.prototype.dragRow = function (event, self) {
     if (self.indentEnabled) {
     if (self.indentEnabled) {
       var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x;
       var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x;
       // Set the number of indentations the mouse has been moved left or right.
       // Set the number of indentations the mouse has been moved left or right.
-      var indentDiff = Math.round(xDiff / self.indentAmount * self.rtl);
+      var indentDiff = Math.round(xDiff / self.indentAmount);
       // Indent the row with our estimated diff, which may be further
       // Indent the row with our estimated diff, which may be further
       // restricted according to the rows around this row.
       // restricted according to the rows around this row.
       var indentChange = self.rowObject.indent(indentDiff);
       var indentChange = self.rowObject.indent(indentDiff);
@@ -575,13 +580,43 @@ Drupal.tableDrag.prototype.dropRow = function (event, self) {
  * Get the mouse coordinates from the event (allowing for browser differences).
  * Get the mouse coordinates from the event (allowing for browser differences).
  */
  */
 Drupal.tableDrag.prototype.mouseCoords = function (event) {
 Drupal.tableDrag.prototype.mouseCoords = function (event) {
-  if (event.pageX || event.pageY) {
-    return { x: event.pageX, y: event.pageY };
+
+  // Match both null and undefined, but not zero, by using != null.
+  // See https://stackoverflow.com/questions/2647867/how-to-determine-if-variable-is-undefined-or-null
+  if (event.pageX != null && event.pageY != null) {
+    return {x: event.pageX, y: event.pageY};
+  }
+
+  // Complete support for pointer events was only introduced to jQuery in
+  // version 1.11.1; between versions 1.7 and 1.11.0 pointer events have the
+  // pageX and pageY properties undefined. In those cases, the properties must
+  // be retrieved from the event.originalEvent object instead.
+  if (event.originalEvent && event.originalEvent.pageX != null && event.originalEvent.pageY != null) {
+    return {x: event.originalEvent.pageX, y: event.originalEvent.pageY};
+  }
+
+  // Some old browsers do not support MouseEvent.pageX and *.pageY at all.
+  // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY
+  // For those, we look at event.clientX and event.clientY instead.
+  if (event.clientX == null || event.clientY == null) {
+    // In some jQuery versions, some events created by jQuery do not have
+    // clientX and clientY. But the original event might have.
+    if (!event.originalEvent) {
+      throw new Error("The event has no coordinates, and no event.originalEvent.");
+    }
+    event = event.originalEvent;
+    if (event.clientX == null || event.clientY == null) {
+      throw new Error("The original event has no coordinates.");
+    }
   }
   }
-  return {
-    x: event.clientX + document.body.scrollLeft - document.body.clientLeft,
-    y: event.clientY + document.body.scrollTop  - document.body.clientTop
-  };
+
+  // Copied from jQuery.event.fix() in jQuery 1.4.1.
+  // In newer jQuery versions, this code is in jQuery.event.mouseHooks.filter().
+  var doc = document.documentElement, body = document.body;
+  var pageX = event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+  var pageY = event.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+
+  return {x: pageX, y: pageY};
 };
 };
 
 
 /**
 /**

+ 5 - 1
misc/tableselect.js

@@ -57,10 +57,14 @@ Drupal.tableSelect = function () {
     // Keep track of the last checked checkbox.
     // Keep track of the last checked checkbox.
     lastChecked = e.target;
     lastChecked = e.target;
   });
   });
+
+  // If all checkboxes are checked on page load, make sure the select-all one
+  // is checked too, otherwise keep unchecked.
+  updateSelectAll((checkboxes.length == $(checkboxes).filter(':checked').length));
 };
 };
 
 
 Drupal.tableSelectRange = function (from, to, state) {
 Drupal.tableSelectRange = function (from, to, state) {
-  // We determine the looping mode based on the the order of from and to.
+  // We determine the looping mode based on the order of from and to.
   var mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
   var mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
 
 
   // Traverse through the sibling nodes.
   // Traverse through the sibling nodes.

BIN
misc/throbber-active.gif


BIN
misc/throbber-inactive.png


+ 79 - 0
misc/typo3/drupal-security/PharExtensionInterceptor.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\Core\Security;
+
+use TYPO3\PharStreamWrapper\Assertable;
+use TYPO3\PharStreamWrapper\Helper;
+use TYPO3\PharStreamWrapper\Exception;
+
+/**
+ * An alternate PharExtensionInterceptor to support phar-based CLI tools.
+ *
+ * @see \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor
+ */
+class PharExtensionInterceptor implements Assertable {
+
+  /**
+   * Determines whether phar file is allowed to execute.
+   *
+   * The phar file is allowed to execute if:
+   * - the base file name has a ".phar" suffix.
+   * - it is the CLI tool that has invoked the interceptor.
+   *
+   * @param string $path
+   *   The path of the phar file to check.
+   * @param string $command
+   *   The command being carried out.
+   *
+   * @return bool
+   *   TRUE if the phar file is allowed to execute.
+   *
+   * @throws Exception
+   *   Thrown when the file is not allowed to execute.
+   */
+  public function assert($path, $command) {
+    if ($this->baseFileContainsPharExtension($path)) {
+      return TRUE;
+    }
+    throw new Exception(
+      sprintf(
+        'Unexpected file extension in "%s"',
+        $path
+      ),
+      1535198703
+    );
+  }
+
+  /**
+   * Determines if a path has a .phar extension or invoked execution.
+   *
+   * @param string $path
+   *   The path of the phar file to check.
+   *
+   * @return bool
+   *   TRUE if the file has a .phar extension or if the execution has been
+   *   invoked by the phar file.
+   */
+  private function baseFileContainsPharExtension($path) {
+    $baseFile = Helper::determineBaseFile($path);
+    if ($baseFile === NULL) {
+      return FALSE;
+    }
+    // If the stream wrapper is registered by invoking a phar file that does
+    // not not have .phar extension then this should be allowed. For
+    // example, some CLI tools recommend removing the extension.
+    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+    // Find the last entry in the backtrace containing a 'file' key as
+    // sometimes the last caller is executed outside the scope of a file. For
+    // example, this occurs with shutdown functions.
+    do {
+      $caller = array_pop($backtrace);
+    } while (empty($caller['file']) && !empty($backtrace));
+    if (isset($caller['file']) && $baseFile === Helper::determineBaseFile($caller['file'])) {
+      return TRUE;
+    }
+    $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
+    return strtolower($fileExtension) === 'phar';
+  }
+
+}

+ 21 - 0
misc/typo3/phar-stream-wrapper/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 TYPO3 project - https://typo3.org/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 221 - 0
misc/typo3/phar-stream-wrapper/README.md

@@ -0,0 +1,221 @@
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/badges/quality-score.png?b=v2)](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/?branch=v2)
+[![Travis CI Build Status](https://travis-ci.org/TYPO3/phar-stream-wrapper.svg?branch=v2)](https://travis-ci.org/TYPO3/phar-stream-wrapper)
+[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/q4ls5tg4w1d6sf4i/branch/v2?svg=true)](https://ci.appveyor.com/project/ohader/phar-stream-wrapper)
+
+# PHP Phar Stream Wrapper
+
+## Abstract & History
+
+Based on Sam Thomas' findings concerning
+[insecure deserialization in combination with obfuscation strategies](https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are)
+allowing to hide Phar files inside valid image resources, the TYPO3 project
+decided back then to introduce a `PharStreamWrapper` to intercept invocations
+of the `phar://` stream in PHP and only allow usage for defined locations in
+the file system.
+
+Since the TYPO3 mission statement is **inspiring people to share**, we thought
+it would be helpful for others to release our `PharStreamWrapper` as standalone
+package to the PHP community.
+
+The mentioned security issue was reported to TYPO3 on 10th June 2018 by Sam Thomas
+and has been addressed concerning the specific attack vector and for this generic
+`PharStreamWrapper` in TYPO3 versions 7.6.30 LTS, 8.7.17 LTS and 9.3.1 on 12th
+July 2018.
+
+* https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are
+* https://youtu.be/GePBmsNJw6Y
+* https://typo3.org/security/advisory/typo3-psa-2018-001/
+* https://typo3.org/security/advisory/typo3-psa-2019-007/
+* https://typo3.org/security/advisory/typo3-psa-2019-008/
+
+## License
+
+In general the TYPO3 core is released under the GNU General Public License version
+2 or any later version (`GPL-2.0-or-later`). In order to avoid licensing issues and
+incompatibilities this `PharStreamWrapper` is licenced under the MIT License. In case
+you duplicate or modify source code, credits are not required but really appreciated.
+
+## Credits
+
+Thanks to [Alex Pott](https://github.com/alexpott), Drupal for creating
+back-ports of all sources in order to provide compatibility with PHP v5.3.
+
+## Installation
+
+The `PharStreamWrapper` is provided as composer package `typo3/phar-stream-wrapper`
+and has minimum requirements of PHP v5.3 ([`v2`](https://github.com/TYPO3/phar-stream-wrapper/tree/v2) branch) and PHP v7.0 ([`master`](https://github.com/TYPO3/phar-stream-wrapper) branch).
+
+### Installation for PHP v7.0
+
+```
+composer require typo3/phar-stream-wrapper ^3.0
+```
+
+### Installation for PHP v5.3
+
+```
+composer require typo3/phar-stream-wrapper ^2.0
+```
+
+## Example
+
+The following example is bundled within this package, the shown
+`PharExtensionInterceptor` denies all stream wrapper invocations files
+not having the `.phar` suffix. Interceptor logic has to be individual and
+adjusted to according requirements.
+
+```
+$behavior = new \TYPO3\PharStreamWrapper\Behavior();
+\TYPO3\PharStreamWrapper\Manager::initialize(
+    $behavior->withAssertion(new PharExtensionInterceptor())
+);
+
+if (in_array('phar', stream_get_wrappers())) {
+    stream_wrapper_unregister('phar');
+    stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper');
+}
+```
+
+* `PharStreamWrapper` defined as class reference will be instantiated each time
+  `phar://` streams shall be processed.
+* `Manager` as singleton pattern being called by `PharStreamWrapper` instances
+  in order to retrieve individual behavior and settings.
+* `Behavior` holds reference to interceptor(s) that shall assert correct/allowed
+  invocation of a given `$path` for a given `$command`. Interceptors implement
+  the interface `Assertable`. Interceptors can act individually on following
+  commands or handle all of them in case not defined specifically:  
+  + `COMMAND_DIR_OPENDIR`
+  + `COMMAND_MKDIR`
+  + `COMMAND_RENAME`
+  + `COMMAND_RMDIR`
+  + `COMMAND_STEAM_METADATA`
+  + `COMMAND_STREAM_OPEN`
+  + `COMMAND_UNLINK`
+  + `COMMAND_URL_STAT`
+
+## Interceptors
+
+The following interceptor is shipped with the package and ready to use in order
+to block any Phar invocation of files not having a `.phar` suffix. Besides that
+individual interceptors are possible of course.
+
+```
+class PharExtensionInterceptor implements Assertable
+{
+    /**
+     * Determines whether the base file name has a ".phar" suffix.
+     *
+     * @param string $path
+     * @param string $command
+     * @return bool
+     * @throws Exception
+     */
+    public function assert($path, $command)
+    {
+        if ($this->baseFileContainsPharExtension($path)) {
+            return true;
+        }
+        throw new Exception(
+            sprintf(
+                'Unexpected file extension in "%s"',
+                $path
+            ),
+            1535198703
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    private function baseFileContainsPharExtension($path)
+    {
+        $baseFile = Helper::determineBaseFile($path);
+        if ($baseFile === null) {
+            return false;
+        }
+        $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
+        return strtolower($fileExtension) === 'phar';
+    }
+}
+```
+
+### ConjunctionInterceptor
+
+This interceptor combines multiple interceptors implementing `Assertable`.
+It succeeds when all nested interceptors succeed as well (logical `AND`).
+
+```
+$behavior = new \TYPO3\PharStreamWrapper\Behavior();
+\TYPO3\PharStreamWrapper\Manager::initialize(
+    $behavior->withAssertion(new ConjunctionInterceptor(array(
+        new PharExtensionInterceptor(),
+        new PharMetaDataInterceptor()
+    )))
+);
+```
+
+### PharExtensionInterceptor
+
+This (basic) interceptor just checks whether the invoked Phar archive has
+an according `.phar` file extension. Resolving symbolic links as well as
+Phar internal alias resolving are considered as well.
+
+```
+$behavior = new \TYPO3\PharStreamWrapper\Behavior();
+\TYPO3\PharStreamWrapper\Manager::initialize(
+    $behavior->withAssertion(new PharExtensionInterceptor())
+);
+```
+
+### PharMetaDataInterceptor
+
+This interceptor is actually checking serialized Phar meta-data against
+PHP objects and would consider a Phar archive malicious in case not only
+scalar values are found. A custom low-level `Phar\Reader` is used in order to
+avoid using PHP's `Phar` object which would trigger the initial vulnerability.
+
+```
+$behavior = new \TYPO3\PharStreamWrapper\Behavior();
+\TYPO3\PharStreamWrapper\Manager::initialize(
+    $behavior->withAssertion(new PharMetaDataInterceptor())
+);
+```
+
+## Reader
+
+* `Phar\Reader::__construct(string $fileName)`: Creates low-level reader for Phar archive
+* `Phar\Reader::resolveContainer(): Phar\Container`: Resolves model representing Phar archive
+* `Phar\Container::getStub(): Phar\Stub`: Resolves (plain PHP) stub section of Phar archive
+* `Phar\Container::getManifest(): Phar\Manifest`: Resolves parsed Phar archive manifest as
+  documented at http://php.net/manual/en/phar.fileformat.manifestfile.php
+* `Phar\Stub::getMappedAlias(): string`: Resolves internal Phar archive alias defined in stub
+  using `Phar::mapPhar('alias.phar')` - actually the plain PHP source is analyzed here
+* `Phar\Manifest::getAlias(): string` - Resolves internal Phar archive alias defined in manifest
+  using `Phar::setAlias('alias.phar')`
+* `Phar\Manifest::getMetaData(): string`: Resolves serialized Phar archive meta-data
+* `Phar\Manifest::deserializeMetaData(): mixed`: Resolves deserialized Phar archive meta-data
+  containing only scalar values - in case an object is determined, an according
+  `Phar\DeserializationException` will be thrown
+
+```
+$reader = new Phar\Reader('example.phar');
+var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData());
+```
+
+## Helper
+
+* `Helper::determineBaseFile(string $path): string`: Determines base file that can be
+  accessed using the regular file system. For instance the following path
+  `phar:///home/user/bundle.phar/content.txt` would be resolved to
+  `/home/user/bundle.phar`.
+* `Helper::resetOpCache()`: Resets PHP's OPcache if enabled as work-around for
+  issues in `include()` or `require()` calls and OPcache delivering wrong
+  results. More details can be found in PHP's bug tracker, for instance like
+  https://bugs.php.net/bug.php?id=66569
+
+## Security Contact
+
+In case of finding additional security issues in the TYPO3 project or in this
+`PharStreamWrapper` package in particular, please get in touch with the
+[TYPO3 Security Team](mailto:security@typo3.org).

+ 30 - 0
misc/typo3/phar-stream-wrapper/composer.json

@@ -0,0 +1,30 @@
+{
+    "name": "typo3/phar-stream-wrapper",
+    "description": "Interceptors for PHP's native phar:// stream handling",
+    "type": "library",
+    "license": "MIT",
+    "homepage": "https://typo3.org/",
+    "keywords": ["php", "phar", "stream-wrapper", "security"],
+    "require": {
+        "php": "^5.3.3|^7.0",
+        "ext-json": "*",
+        "brumann/polyfill-unserialize": "^1.0"
+    },
+    "require-dev": {
+        "ext-xdebug": "*",
+        "phpunit/phpunit": "^4.8.36"
+    },
+    "suggest": {
+        "ext-fileinfo": "For PHP builtin file type guessing, otherwise uses internal processing"
+    },
+    "autoload": {
+        "psr-4": {
+            "TYPO3\\PharStreamWrapper\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "TYPO3\\PharStreamWrapper\\Tests\\": "tests/"
+        }
+    }
+}

+ 22 - 0
misc/typo3/phar-stream-wrapper/src/Assertable.php

@@ -0,0 +1,22 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+interface Assertable
+{
+    /**
+     * @param string $path
+     * @param string $command
+     * @return bool
+     */
+    public function assert($path, $command);
+}

+ 124 - 0
misc/typo3/phar-stream-wrapper/src/Behavior.php

@@ -0,0 +1,124 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class Behavior implements Assertable
+{
+    const COMMAND_DIR_OPENDIR = 'dir_opendir';
+    const COMMAND_MKDIR = 'mkdir';
+    const COMMAND_RENAME = 'rename';
+    const COMMAND_RMDIR = 'rmdir';
+    const COMMAND_STEAM_METADATA = 'stream_metadata';
+    const COMMAND_STREAM_OPEN = 'stream_open';
+    const COMMAND_UNLINK = 'unlink';
+    const COMMAND_URL_STAT = 'url_stat';
+
+    /**
+     * @var string[]
+     */
+    private $availableCommands = array(
+        self::COMMAND_DIR_OPENDIR,
+        self::COMMAND_MKDIR,
+        self::COMMAND_RENAME,
+        self::COMMAND_RMDIR,
+        self::COMMAND_STEAM_METADATA,
+        self::COMMAND_STREAM_OPEN,
+        self::COMMAND_UNLINK,
+        self::COMMAND_URL_STAT,
+    );
+
+    /**
+     * @var Assertable[]
+     */
+    private $assertions;
+
+    /**
+     * @param Assertable $assertable
+     * @return static
+     */
+    public function withAssertion(Assertable $assertable)
+    {
+        $commands = func_get_args();
+        array_shift($commands);
+        $this->assertCommands($commands);
+        $commands = $commands ?: $this->availableCommands;
+
+        $target = clone $this;
+        foreach ($commands as $command) {
+            $target->assertions[$command] = $assertable;
+        }
+        return $target;
+    }
+
+    /**
+     * @param string $path
+     * @param string $command
+     * @return bool
+     */
+    public function assert($path, $command)
+    {
+        $this->assertCommand($command);
+        $this->assertAssertionCompleteness();
+
+        return $this->assertions[$command]->assert($path, $command);
+    }
+
+    /**
+     * @param array $commands
+     */
+    private function assertCommands(array $commands)
+    {
+        $unknownCommands = array_diff($commands, $this->availableCommands);
+        if (empty($unknownCommands)) {
+            return;
+        }
+        throw new \LogicException(
+            sprintf(
+                'Unknown commands: %s',
+                implode(', ', $unknownCommands)
+            ),
+            1535189881
+        );
+    }
+
+    private function assertCommand($command)
+    {
+        if (in_array($command, $this->availableCommands, true)) {
+            return;
+        }
+        throw new \LogicException(
+            sprintf(
+                'Unknown command "%s"',
+                $command
+            ),
+            1535189882
+        );
+    }
+
+    private function assertAssertionCompleteness()
+    {
+        $undefinedAssertions = array_diff(
+            $this->availableCommands,
+            array_keys($this->assertions)
+        );
+        if (empty($undefinedAssertions)) {
+            return;
+        }
+        throw new \LogicException(
+            sprintf(
+                'Missing assertions for commands: %s',
+                implode(', ', $undefinedAssertions)
+            ),
+            1535189883
+        );
+    }
+}

+ 37 - 0
misc/typo3/phar-stream-wrapper/src/Collectable.php

@@ -0,0 +1,37 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
+
+interface Collectable
+{
+    /**
+     * @param PharInvocation $invocation
+     * @return bool
+     */
+    public function has(PharInvocation $invocation);
+
+    /**
+     * @param PharInvocation $invocation
+     * @param null $flags
+     * @return bool
+     */
+    public function collect(PharInvocation $invocation, $flags = null);
+
+    /**
+     * @param callable $callback
+     * @param bool $reverse
+     * @return null|PharInvocation
+     */
+    public function findByCallback($callback, $reverse = false);
+}

+ 16 - 0
misc/typo3/phar-stream-wrapper/src/Exception.php

@@ -0,0 +1,16 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class Exception extends \RuntimeException
+{
+}

+ 199 - 0
misc/typo3/phar-stream-wrapper/src/Helper.php

@@ -0,0 +1,199 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Helper provides low-level tools on file name resolving. However it does not
+ * (and should not) maintain any runtime state information. In order to resolve
+ * Phar archive paths according resolvers have to be used.
+ *
+ * @see \TYPO3\PharStreamWrapper\Resolvable::resolve()
+ */
+class Helper
+{
+    /*
+     * Resets PHP's OPcache if enabled as work-around for issues in `include()`
+     * or `require()` calls and OPcache delivering wrong results.
+     *
+     * @see https://bugs.php.net/bug.php?id=66569
+     */
+    public static function resetOpCache()
+    {
+        if (function_exists('opcache_reset')
+            && function_exists('opcache_get_status')
+        ) {
+            $status = opcache_get_status();
+            if (!empty($status['opcache_enabled'])) {
+                opcache_reset();
+            }
+        }
+    }
+
+    /**
+     * Determines base file that can be accessed using the regular file system.
+     * For e.g. "phar:///home/user/bundle.phar/content.txt" that would result
+     * into "/home/user/bundle.phar".
+     *
+     * @param string $path
+     * @return string|null
+     */
+    public static function determineBaseFile($path)
+    {
+        $parts = explode('/', static::normalizePath($path));
+
+        while (count($parts)) {
+            $currentPath = implode('/', $parts);
+            if (@is_file($currentPath) && realpath($currentPath) !== false) {
+                return $currentPath;
+            }
+            array_pop($parts);
+        }
+
+        return null;
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    public static function hasPharPrefix($path)
+    {
+        return stripos($path, 'phar://') === 0;
+    }
+
+    /**
+     * @param string $path
+     * @return string
+     */
+    public static function removePharPrefix($path)
+    {
+        $path = trim($path);
+        if (!static::hasPharPrefix($path)) {
+            return $path;
+        }
+        return substr($path, 7);
+    }
+
+    /**
+     * Normalizes a path, removes phar:// prefix, fixes Windows directory
+     * separators. Result is without trailing slash.
+     *
+     * @param string $path
+     * @return string
+     */
+    public static function normalizePath($path)
+    {
+        return rtrim(
+            static::normalizeWindowsPath(
+                static::removePharPrefix($path)
+            ),
+            '/'
+        );
+    }
+
+    /**
+     * Fixes a path for windows-backslashes and reduces double-slashes to single slashes
+     *
+     * @param string $path File path to process
+     * @return string
+     */
+    public static function normalizeWindowsPath($path)
+    {
+        return str_replace('\\', '/', $path);
+    }
+
+    /**
+     * Resolves all dots, slashes and removes spaces after or before a path...
+     *
+     * @param string $path Input string
+     * @return string Canonical path, always without trailing slash
+     */
+    private static function getCanonicalPath($path)
+    {
+        $path = static::normalizeWindowsPath($path);
+
+        $absolutePathPrefix = '';
+        if (static::isAbsolutePath($path)) {
+            if (static::isWindows() && strpos($path, ':/') === 1) {
+                $absolutePathPrefix = substr($path, 0, 3);
+                $path = substr($path, 3);
+            } else {
+                $path = ltrim($path, '/');
+                $absolutePathPrefix = '/';
+            }
+        }
+
+        $pathParts = explode('/', $path);
+        $pathPartsLength = count($pathParts);
+        for ($partCount = 0; $partCount < $pathPartsLength; $partCount++) {
+            // double-slashes in path: remove element
+            if ($pathParts[$partCount] === '') {
+                array_splice($pathParts, $partCount, 1);
+                $partCount--;
+                $pathPartsLength--;
+            }
+            // "." in path: remove element
+            if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '.') {
+                array_splice($pathParts, $partCount, 1);
+                $partCount--;
+                $pathPartsLength--;
+            }
+            // ".." in path:
+            if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '..') {
+                if ($partCount === 0) {
+                    array_splice($pathParts, $partCount, 1);
+                    $partCount--;
+                    $pathPartsLength--;
+                } elseif ($partCount >= 1) {
+                    // Rremove this and previous element
+                    array_splice($pathParts, $partCount - 1, 2);
+                    $partCount -= 2;
+                    $pathPartsLength -= 2;
+                } elseif ($absolutePathPrefix) {
+                    // can't go higher than root dir
+                    // simply remove this part and continue
+                    array_splice($pathParts, $partCount, 1);
+                    $partCount--;
+                    $pathPartsLength--;
+                }
+            }
+        }
+
+        return $absolutePathPrefix . implode('/', $pathParts);
+    }
+
+    /**
+     * Checks if the $path is absolute or relative (detecting either '/' or
+     * 'x:/' as first part of string) and returns TRUE if so.
+     *
+     * @param string $path File path to evaluate
+     * @return bool
+     */
+    private static function isAbsolutePath($path)
+    {
+        // Path starting with a / is always absolute, on every system
+        // On Windows also a path starting with a drive letter is absolute: X:/
+        return (isset($path[0]) ? $path[0] : null) === '/'
+            || static::isWindows() && (
+                strpos($path, ':/') === 1
+                || strpos($path, ':\\') === 1
+            );
+    }
+
+    /**
+     * @return bool
+     */
+    private static function isWindows()
+    {
+        return stripos(PHP_OS, 'WIN') === 0;
+    }
+}

+ 88 - 0
misc/typo3/phar-stream-wrapper/src/Interceptor/ConjunctionInterceptor.php

@@ -0,0 +1,88 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Interceptor;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Assertable;
+use TYPO3\PharStreamWrapper\Exception;
+
+class ConjunctionInterceptor implements Assertable
+{
+    /**
+     * @var Assertable[]
+     */
+    private $assertions;
+
+    public function __construct(array $assertions)
+    {
+        $this->assertAssertions($assertions);
+        $this->assertions = $assertions;
+    }
+
+    /**
+     * Executes assertions based on all contained assertions.
+     *
+     * @param string $path
+     * @param string $command
+     * @return bool
+     * @throws Exception
+     */
+    public function assert($path, $command)
+    {
+        if ($this->invokeAssertions($path, $command)) {
+            return true;
+        }
+        throw new Exception(
+            sprintf(
+                'Assertion failed in "%s"',
+                $path
+            ),
+            1539625084
+        );
+    }
+
+    /**
+     * @param Assertable[] $assertions
+     */
+    private function assertAssertions(array $assertions)
+    {
+        foreach ($assertions as $assertion) {
+            if (!$assertion instanceof Assertable) {
+                throw new \InvalidArgumentException(
+                    sprintf(
+                        'Instance %s must implement Assertable',
+                        get_class($assertion)
+                    ),
+                    1539624719
+                );
+            }
+        }
+    }
+
+    /**
+     * @param string $path
+     * @param string $command
+     * @return bool
+     */
+    private function invokeAssertions($path, $command)
+    {
+        try {
+            foreach ($this->assertions as $assertion) {
+                if (!$assertion->assert($path, $command)) {
+                    return false;
+                }
+            }
+        } catch (Exception $exception) {
+            return false;
+        }
+        return true;
+    }
+}

+ 55 - 0
misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php

@@ -0,0 +1,55 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Interceptor;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Assertable;
+use TYPO3\PharStreamWrapper\Exception;
+use TYPO3\PharStreamWrapper\Manager;
+
+class PharExtensionInterceptor implements Assertable
+{
+    /**
+     * Determines whether the base file name has a ".phar" suffix.
+     *
+     * @param string $path
+     * @param string $command
+     * @return bool
+     * @throws Exception
+     */
+    public function assert($path, $command)
+    {
+        if ($this->baseFileContainsPharExtension($path)) {
+            return true;
+        }
+        throw new Exception(
+            sprintf(
+                'Unexpected file extension in "%s"',
+                $path
+            ),
+            1535198703
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    private function baseFileContainsPharExtension($path)
+    {
+        $invocation = Manager::instance()->resolve($path);
+        if ($invocation === null) {
+            return false;
+        }
+        $fileExtension = pathinfo($invocation->getBaseName(), PATHINFO_EXTENSION);
+        return strtolower($fileExtension) === 'phar';
+    }
+}

+ 73 - 0
misc/typo3/phar-stream-wrapper/src/Interceptor/PharMetaDataInterceptor.php

@@ -0,0 +1,73 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Interceptor;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Assertable;
+use TYPO3\PharStreamWrapper\Exception;
+use TYPO3\PharStreamWrapper\Manager;
+use TYPO3\PharStreamWrapper\Phar\DeserializationException;
+use TYPO3\PharStreamWrapper\Phar\Reader;
+
+/**
+ * @internal Experimental implementation of checking against serialized objects in Phar meta-data
+ * @internal This functionality has not been 100% pentested...
+ */
+class PharMetaDataInterceptor implements Assertable
+{
+    /**
+     * Determines whether the according Phar archive contains
+     * (potential insecure) serialized objects.
+     *
+     * @param string $path
+     * @param string $command
+     * @return bool
+     * @throws Exception
+     */
+    public function assert($path, $command)
+    {
+        if ($this->baseFileDoesNotHaveMetaDataIssues($path)) {
+            return true;
+        }
+        throw new Exception(
+            sprintf(
+                'Problematic meta-data in "%s"',
+                $path
+            ),
+            1539632368
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    private function baseFileDoesNotHaveMetaDataIssues($path)
+    {
+        $invocation = Manager::instance()->resolve($path);
+        if ($invocation === null) {
+            return false;
+        }
+        // directly return in case invocation was checked before
+        if ($invocation->getVariable(__CLASS__) === true) {
+            return true;
+        }
+        // otherwise analyze meta-data
+        try {
+            $reader = new Reader($invocation->getBaseName());
+            $reader->resolveContainer()->getManifest()->deserializeMetaData();
+            $invocation->setVariable(__CLASS__, true);
+        } catch (DeserializationException $exception) {
+            return false;
+        }
+        return true;
+    }
+}

+ 135 - 0
misc/typo3/phar-stream-wrapper/src/Manager.php

@@ -0,0 +1,135 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
+use TYPO3\PharStreamWrapper\Resolver\PharInvocationCollection;
+use TYPO3\PharStreamWrapper\Resolver\PharInvocationResolver;
+
+class Manager
+{
+    /**
+     * @var self
+     */
+    private static $instance;
+
+    /**
+     * @var Behavior
+     */
+    private $behavior;
+
+    /**
+     * @var Resolvable
+     */
+    private $resolver;
+
+    /**
+     * @var Collectable
+     */
+    private $collection;
+
+    /**
+     * @param Behavior $behaviour
+     * @param Resolvable $resolver
+     * @param Collectable $collection
+     * @return self
+     */
+    public static function initialize(
+        Behavior $behaviour,
+        Resolvable $resolver = null,
+        Collectable $collection = null
+    ) {
+        if (self::$instance === null) {
+            self::$instance = new self($behaviour, $resolver, $collection);
+            return self::$instance;
+        }
+        throw new \LogicException(
+            'Manager can only be initialized once',
+            1535189871
+        );
+    }
+
+    /**
+     * @return self
+     */
+    public static function instance()
+    {
+        if (self::$instance !== null) {
+            return self::$instance;
+        }
+        throw new \LogicException(
+            'Manager needs to be initialized first',
+            1535189872
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public static function destroy()
+    {
+        if (self::$instance === null) {
+            return false;
+        }
+        self::$instance = null;
+        return true;
+    }
+
+    /**
+     * @param Behavior $behaviour
+     * @param Resolvable $resolver
+     * @param Collectable $collection
+     */
+    private function __construct(
+        Behavior $behaviour,
+        Resolvable $resolver = null,
+        Collectable $collection = null
+    ) {
+        if ($collection === null) {
+            $collection = new PharInvocationCollection();
+        }
+        if ($resolver === null) {
+            $resolver = new PharInvocationResolver();
+        }
+        $this->collection = $collection;
+        $this->resolver = $resolver;
+        $this->behavior = $behaviour;
+    }
+
+    /**
+     * @param string $path
+     * @param string $command
+     * @return bool
+     */
+    public function assert($path, $command)
+    {
+        return $this->behavior->assert($path, $command);
+    }
+
+    /**
+     * @param string $path
+     * @param null|int $flags
+     * @return null|PharInvocation
+     */
+    public function resolve($path, $flags = null)
+    {
+        return $this->resolver->resolve($path, $flags);
+    }
+
+    /**
+     * @return Collectable
+     */
+    public function getCollection()
+    {
+        return $this->collection;
+    }
+}

+ 59 - 0
misc/typo3/phar-stream-wrapper/src/Phar/Container.php

@@ -0,0 +1,59 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Phar;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class Container
+{
+    /**
+     * @var Stub
+     */
+    private $stub;
+
+    /**
+     * @var Manifest
+     */
+    private $manifest;
+
+    /**
+     * @param Stub $stub
+     * @param Manifest $manifest
+     */
+    public function __construct(Stub $stub, Manifest $manifest)
+    {
+        $this->stub = $stub;
+        $this->manifest = $manifest;
+    }
+
+    /**
+     * @return Stub
+     */
+    public function getStub()
+    {
+        return $this->stub;
+    }
+
+    /**
+     * @return Manifest
+     */
+    public function getManifest()
+    {
+        return $this->manifest;
+    }
+
+    /**
+     * @return string
+     */
+    public function getAlias()
+    {
+        return $this->manifest->getAlias() ?: $this->stub->getMappedAlias();
+    }
+}

+ 18 - 0
misc/typo3/phar-stream-wrapper/src/Phar/DeserializationException.php

@@ -0,0 +1,18 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Phar;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Exception;
+
+class DeserializationException extends Exception
+{
+}

+ 176 - 0
misc/typo3/phar-stream-wrapper/src/Phar/Manifest.php

@@ -0,0 +1,176 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Phar;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Brumann\Polyfill\Unserialize;
+
+class Manifest
+{
+    /**
+     * @param string $content
+     * @return self
+     * @see http://php.net/manual/en/phar.fileformat.phar.php
+     */
+    public static function fromContent($content)
+    {
+        $target = new static();
+        $target->manifestLength = Reader::resolveFourByteLittleEndian($content, 0);
+        $target->amountOfFiles = Reader::resolveFourByteLittleEndian($content, 4);
+        $target->flags = Reader::resolveFourByteLittleEndian($content, 10);
+        $target->aliasLength = Reader::resolveFourByteLittleEndian($content, 14);
+        $target->alias = substr($content, 18, $target->aliasLength);
+        $target->metaDataLength = Reader::resolveFourByteLittleEndian($content, 18 + $target->aliasLength);
+        $target->metaData = substr($content, 22 + $target->aliasLength, $target->metaDataLength);
+
+        $apiVersionNibbles = Reader::resolveTwoByteBigEndian($content, 8);
+        $target->apiVersion = implode('.', array(
+            ($apiVersionNibbles & 0xf000) >> 12,
+            ($apiVersionNibbles & 0x0f00) >> 8,
+            ($apiVersionNibbles & 0x00f0) >> 4,
+        ));
+
+        return $target;
+    }
+
+    /**
+     * @var int
+     */
+    private $manifestLength;
+
+    /**
+     * @var int
+     */
+    private $amountOfFiles;
+
+    /**
+     * @var string
+     */
+    private $apiVersion;
+
+    /**
+     * @var int
+     */
+    private $flags;
+
+    /**
+     * @var int
+     */
+    private $aliasLength;
+
+    /**
+     * @var string
+     */
+    private $alias;
+
+    /**
+     * @var int
+     */
+    private $metaDataLength;
+
+    /**
+     * @var string
+     */
+    private $metaData;
+
+    /**
+     * Avoid direct instantiation.
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * @return int
+     */
+    public function getManifestLength()
+    {
+        return $this->manifestLength;
+    }
+
+    /**
+     * @return int
+     */
+    public function getAmountOfFiles()
+    {
+        return $this->amountOfFiles;
+    }
+
+    /**
+     * @return string
+     */
+    public function getApiVersion()
+    {
+        return $this->apiVersion;
+    }
+
+    /**
+     * @return int
+     */
+    public function getFlags()
+    {
+        return $this->flags;
+    }
+
+    /**
+     * @return int
+     */
+    public function getAliasLength()
+    {
+        return $this->aliasLength;
+    }
+
+    /**
+     * @return string
+     */
+    public function getAlias()
+    {
+        return $this->alias;
+    }
+
+    /**
+     * @return int
+     */
+    public function getMetaDataLength()
+    {
+        return $this->metaDataLength;
+    }
+
+    /**
+     * @return string
+     */
+    public function getMetaData()
+    {
+        return $this->metaData;
+    }
+
+    /**
+     * @return mixed|null
+     */
+    public function deserializeMetaData()
+    {
+        if (empty($this->metaData)) {
+            return null;
+        }
+
+        $result = Unserialize::unserialize($this->metaData, array('allowed_classes' => false));
+
+        $serialized = json_encode($result);
+        if (strpos($serialized, '__PHP_Incomplete_Class_Name') !== false) {
+            throw new DeserializationException(
+                'Meta-data contains serialized object',
+                1539623382
+            );
+        }
+
+        return $result;
+    }
+}

+ 254 - 0
misc/typo3/phar-stream-wrapper/src/Phar/Reader.php

@@ -0,0 +1,254 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Phar;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class Reader
+{
+    /**
+     * @var string
+     */
+    private $fileName;
+
+    /**
+     * Mime-type in order to use zlib, bzip2 or no compression.
+     * In case ext-fileinfo is not present only the relevant types
+     * 'application/x-gzip' and 'application/x-bzip2' are assigned
+     * to this class property.
+     *
+     * @var string
+     */
+    private $fileType;
+
+    /**
+     * @param string $fileName
+     */
+    public function __construct($fileName)
+    {
+        if (strpos($fileName, '://') !== false) {
+            throw new ReaderException(
+                'File name must not contain stream prefix',
+                1539623708
+            );
+        }
+
+        $this->fileName = $fileName;
+        $this->fileType = $this->determineFileType();
+    }
+
+    /**
+     * @return Container
+     */
+    public function resolveContainer()
+    {
+        $data = $this->extractData($this->resolveStream() . $this->fileName);
+
+        if ($data['stubContent'] === null) {
+            throw new ReaderException(
+                'Cannot resolve stub',
+                1547807881
+            );
+        }
+        if ($data['manifestContent'] === null || $data['manifestLength'] === null) {
+            throw new ReaderException(
+                'Cannot resolve manifest',
+                1547807882
+            );
+        }
+        if (strlen($data['manifestContent']) < $data['manifestLength']) {
+            throw new ReaderException(
+                sprintf(
+                    'Exected manifest length %d, got %d',
+                    strlen($data['manifestContent']),
+                    $data['manifestLength']
+                ),
+                1547807883
+            );
+        }
+
+        return new Container(
+            Stub::fromContent($data['stubContent']),
+            Manifest::fromContent($data['manifestContent'])
+        );
+    }
+
+    /**
+     * @param string $fileName e.g. '/path/file.phar' or 'compress.zlib:///path/file.phar'
+     * @return array
+     */
+    private function extractData($fileName)
+    {
+        $stubContent = null;
+        $manifestContent = null;
+        $manifestLength = null;
+
+        $resource = fopen($fileName, 'r');
+        if (!is_resource($resource)) {
+            throw new ReaderException(
+                sprintf('Resource %s could not be opened', $fileName),
+                1547902055
+            );
+        }
+
+        while (!feof($resource)) {
+            $line = fgets($resource);
+            // stop reading file when manifest can be extracted
+            if ($manifestLength !== null && $manifestContent !== null && strlen($manifestContent) >= $manifestLength) {
+                break;
+            }
+
+            $manifestPosition = strpos($line, '__HALT_COMPILER();');
+
+            // first line contains start of manifest
+            if ($stubContent === null && $manifestContent === null && $manifestPosition !== false) {
+                $stubContent = substr($line, 0, $manifestPosition - 1);
+                $manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
+                $manifestLength = $this->resolveManifestLength($manifestContent);
+            // line contains start of stub
+            } elseif ($stubContent === null) {
+                $stubContent = $line;
+            // line contains start of manifest
+            } elseif ($manifestContent === null && $manifestPosition !== false) {
+                $manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
+                $manifestLength = $this->resolveManifestLength($manifestContent);
+            // manifest has been started (thus is cannot be stub anymore), add content
+            } elseif ($manifestContent !== null) {
+                $manifestContent .= $line;
+                $manifestLength = $this->resolveManifestLength($manifestContent);
+            // stub has been started (thus cannot be manifest here, yet), add content
+            } elseif ($stubContent !== null) {
+                $stubContent .= $line;
+            }
+        }
+        fclose($resource);
+
+        return array(
+            'stubContent' => $stubContent,
+            'manifestContent' => $manifestContent,
+            'manifestLength' => $manifestLength,
+        );
+    }
+
+    /**
+     * Resolves stream in order to handle compressed Phar archives.
+     *
+     * @return string
+     */
+    private function resolveStream()
+    {
+        if ($this->fileType === 'application/x-gzip' || $this->fileType === 'application/gzip') {
+            return 'compress.zlib://';
+        } elseif ($this->fileType === 'application/x-bzip2') {
+            return 'compress.bzip2://';
+        }
+        return '';
+    }
+
+    /**
+     * @return string
+     */
+    private function determineFileType()
+    {
+        if (class_exists('\\finfo')) {
+            $fileInfo = new \finfo();
+            return $fileInfo->file($this->fileName, FILEINFO_MIME_TYPE);
+        }
+        return $this->determineFileTypeByHeader();
+    }
+
+    /**
+     * In case ext-fileinfo is not present only the relevant types
+     * 'application/x-gzip' and 'application/x-bzip2' are resolved.
+     *
+     * @return string
+     */
+    private function determineFileTypeByHeader()
+    {
+        $resource = fopen($this->fileName, 'r');
+        if (!is_resource($resource)) {
+            throw new ReaderException(
+                sprintf('Resource %s could not be opened', $this->fileName),
+                1557753055
+            );
+        }
+        $header = fgets($resource, 4);
+        fclose($resource);
+        $mimeType = '';
+        if (strpos($header, "\x42\x5a\x68") === 0) {
+            $mimeType = 'application/x-bzip2';
+        } elseif (strpos($header, "\x1f\x8b") === 0) {
+            $mimeType = 'application/x-gzip';
+        }
+        return $mimeType;
+    }
+
+    /**
+     * @param string $content
+     * @return int|null
+     */
+    private function resolveManifestLength($content)
+    {
+        if (strlen($content) < 4) {
+            return null;
+        }
+        return static::resolveFourByteLittleEndian($content, 0);
+    }
+
+    /**
+     * @param string $content
+     * @param int $start
+     * @return int
+     */
+    public static function resolveFourByteLittleEndian($content, $start)
+    {
+        $payload = substr($content, $start, 4);
+        if (!is_string($payload)) {
+            throw new ReaderException(
+                sprintf('Cannot resolve value at offset %d', $start),
+                1539614260
+            );
+        }
+
+        $value = unpack('V', $payload);
+        if (!isset($value[1])) {
+            throw new ReaderException(
+                sprintf('Cannot resolve value at offset %d', $start),
+                1539614261
+            );
+        }
+        return $value[1];
+    }
+
+    /**
+     * @param string $content
+     * @param int $start
+     * @return int
+     */
+    public static function resolveTwoByteBigEndian($content, $start)
+    {
+        $payload = substr($content, $start, 2);
+        if (!is_string($payload)) {
+            throw new ReaderException(
+                sprintf('Cannot resolve value at offset %d', $start),
+                1539614263
+            );
+        }
+
+        $value = unpack('n', $payload);
+        if (!isset($value[1])) {
+            throw new ReaderException(
+                sprintf('Cannot resolve value at offset %d', $start),
+                1539614264
+            );
+        }
+        return $value[1];
+    }
+}

+ 18 - 0
misc/typo3/phar-stream-wrapper/src/Phar/ReaderException.php

@@ -0,0 +1,18 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Phar;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Exception;
+
+class ReaderException extends Exception
+{
+}

+ 65 - 0
misc/typo3/phar-stream-wrapper/src/Phar/Stub.php

@@ -0,0 +1,65 @@
+<?php
+namespace TYPO3\PharStreamWrapper\Phar;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal Experimental implementation of Phar archive internals
+ */
+class Stub
+{
+    /**
+     * @param string $content
+     * @return self
+     */
+    public static function fromContent($content)
+    {
+        $target = new static();
+        $target->content = $content;
+
+        if (
+            stripos($content, 'Phar::mapPhar(') !== false
+            && preg_match('#Phar\:\:mapPhar\(([^)]+)\)#', $content, $matches)
+        ) {
+            // remove spaces, single & double quotes
+            // @todo `'my' . 'alias' . '.phar'` is not evaluated here
+            $target->mappedAlias = trim($matches[1], ' \'"');
+        }
+
+        return $target;
+    }
+
+    /**
+     * @var string
+     */
+    private $content;
+
+    /**
+     * @var string
+     */
+    private $mappedAlias = '';
+
+    /**
+     * @return string
+     */
+    public function getContent()
+    {
+        return $this->content;
+    }
+
+    /**
+     * @return string
+     */
+    public function getMappedAlias()
+    {
+        return $this->mappedAlias;
+    }
+}

+ 511 - 0
misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php

@@ -0,0 +1,511 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
+
+class PharStreamWrapper
+{
+    /**
+     * Internal stream constants that are not exposed to PHP, but used...
+     * @see https://github.com/php/php-src/blob/e17fc0d73c611ad0207cac8a4a01ded38251a7dc/main/php_streams.h
+     */
+    const STREAM_OPEN_FOR_INCLUDE = 128;
+
+    /**
+     * @var resource
+     */
+    public $context;
+
+    /**
+     * @var resource
+     */
+    protected $internalResource;
+
+    /**
+     * @var PharInvocation
+     */
+    protected $invocation;
+
+    /**
+     * @return bool
+     */
+    public function dir_closedir()
+    {
+        if (!is_resource($this->internalResource)) {
+            return false;
+        }
+
+        $this->invokeInternalStreamWrapper(
+            'closedir',
+            $this->internalResource
+        );
+        return !is_resource($this->internalResource);
+    }
+
+    /**
+     * @param string $path
+     * @param int $options
+     * @return bool
+     */
+    public function dir_opendir($path, $options)
+    {
+        $this->assert($path, Behavior::COMMAND_DIR_OPENDIR);
+        $this->internalResource = $this->invokeInternalStreamWrapper(
+            'opendir',
+            $path,
+            $this->context
+        );
+        return is_resource($this->internalResource);
+    }
+
+    /**
+     * @return string|false
+     */
+    public function dir_readdir()
+    {
+        return $this->invokeInternalStreamWrapper(
+            'readdir',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public function dir_rewinddir()
+    {
+        if (!is_resource($this->internalResource)) {
+            return false;
+        }
+
+        $this->invokeInternalStreamWrapper(
+            'rewinddir',
+            $this->internalResource
+        );
+        return is_resource($this->internalResource);
+    }
+
+    /**
+     * @param string $path
+     * @param int $mode
+     * @param int $options
+     * @return bool
+     */
+    public function mkdir($path, $mode, $options)
+    {
+        $this->assert($path, Behavior::COMMAND_MKDIR);
+        return $this->invokeInternalStreamWrapper(
+            'mkdir',
+            $path,
+            $mode,
+            (bool) ($options & STREAM_MKDIR_RECURSIVE),
+            $this->context
+        );
+    }
+
+    /**
+     * @param string $path_from
+     * @param string $path_to
+     * @return bool
+     */
+    public function rename($path_from, $path_to)
+    {
+        $this->assert($path_from, Behavior::COMMAND_RENAME);
+        $this->assert($path_to, Behavior::COMMAND_RENAME);
+        return $this->invokeInternalStreamWrapper(
+            'rename',
+            $path_from,
+            $path_to,
+            $this->context
+        );
+    }
+
+    /**
+     * @param string $path
+     * @param int $options
+     * @return bool
+     */
+    public function rmdir($path, $options)
+    {
+        $this->assert($path, Behavior::COMMAND_RMDIR);
+        return $this->invokeInternalStreamWrapper(
+            'rmdir',
+            $path,
+            $this->context
+        );
+    }
+
+    /**
+     * @param int $cast_as
+     */
+    public function stream_cast($cast_as)
+    {
+        throw new Exception(
+            'Method stream_select() cannot be used',
+            1530103999
+        );
+    }
+
+    public function stream_close()
+    {
+        $this->invokeInternalStreamWrapper(
+            'fclose',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public function stream_eof()
+    {
+        return $this->invokeInternalStreamWrapper(
+            'feof',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public function stream_flush()
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fflush',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @param int $operation
+     * @return bool
+     */
+    public function stream_lock($operation)
+    {
+        return $this->invokeInternalStreamWrapper(
+            'flock',
+            $this->internalResource,
+            $operation
+        );
+    }
+
+    /**
+     * @param string $path
+     * @param int $option
+     * @param string|int $value
+     * @return bool
+     */
+    public function stream_metadata($path, $option, $value)
+    {
+        $this->assert($path, Behavior::COMMAND_STEAM_METADATA);
+        if ($option === STREAM_META_TOUCH) {
+            return call_user_func_array(
+                array($this, 'invokeInternalStreamWrapper'),
+                array_merge(array('touch', $path), (array) $value)
+            );
+        }
+        if ($option === STREAM_META_OWNER_NAME || $option === STREAM_META_OWNER) {
+            return $this->invokeInternalStreamWrapper(
+                'chown',
+                $path,
+                $value
+            );
+        }
+        if ($option === STREAM_META_GROUP_NAME || $option === STREAM_META_GROUP) {
+            return $this->invokeInternalStreamWrapper(
+                'chgrp',
+                $path,
+                $value
+            );
+        }
+        if ($option === STREAM_META_ACCESS) {
+            return $this->invokeInternalStreamWrapper(
+                'chmod',
+                $path,
+                $value
+            );
+        }
+        return false;
+    }
+
+    /**
+     * @param string $path
+     * @param string $mode
+     * @param int $options
+     * @param string|null $opened_path
+     * @return bool
+     */
+    public function stream_open(
+        $path,
+        $mode,
+        $options,
+        &$opened_path = null
+    ) {
+        $this->assert($path, Behavior::COMMAND_STREAM_OPEN);
+        $arguments = array($path, $mode, (bool) ($options & STREAM_USE_PATH));
+        // only add stream context for non include/require calls
+        if (!($options & static::STREAM_OPEN_FOR_INCLUDE)) {
+            $arguments[] = $this->context;
+        // work around https://bugs.php.net/bug.php?id=66569
+        // for including files from Phar stream with OPcache enabled
+        } else {
+            Helper::resetOpCache();
+        }
+        $this->internalResource = call_user_func_array(
+            array($this, 'invokeInternalStreamWrapper'),
+            array_merge(array('fopen'), $arguments)
+        );
+        if (!is_resource($this->internalResource)) {
+            return false;
+        }
+        if ($opened_path !== null) {
+            $metaData = stream_get_meta_data($this->internalResource);
+            $opened_path = $metaData['uri'];
+        }
+        return true;
+    }
+
+    /**
+     * @param int $count
+     * @return string
+     */
+    public function stream_read($count)
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fread',
+            $this->internalResource,
+            $count
+        );
+    }
+
+    /**
+     * @param int $offset
+     * @param int $whence
+     * @return bool
+     */
+    public function stream_seek($offset, $whence = SEEK_SET)
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fseek',
+            $this->internalResource,
+            $offset,
+            $whence
+        ) !== -1;
+    }
+
+    /**
+     * @param int $option
+     * @param int $arg1
+     * @param int $arg2
+     * @return bool
+     */
+    public function stream_set_option($option, $arg1, $arg2)
+    {
+        if ($option === STREAM_OPTION_BLOCKING) {
+            return $this->invokeInternalStreamWrapper(
+                'stream_set_blocking',
+                $this->internalResource,
+                $arg1
+            );
+        }
+        if ($option === STREAM_OPTION_READ_TIMEOUT) {
+            return $this->invokeInternalStreamWrapper(
+                'stream_set_timeout',
+                $this->internalResource,
+                $arg1,
+                $arg2
+            );
+        }
+        if ($option === STREAM_OPTION_WRITE_BUFFER) {
+            return $this->invokeInternalStreamWrapper(
+                'stream_set_write_buffer',
+                $this->internalResource,
+                $arg2
+            ) === 0;
+        }
+        return false;
+    }
+
+    /**
+     * @return array
+     */
+    public function stream_stat()
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fstat',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return int
+     */
+    public function stream_tell()
+    {
+        return $this->invokeInternalStreamWrapper(
+            'ftell',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @param int $new_size
+     * @return bool
+     */
+    public function stream_truncate($new_size)
+    {
+        return $this->invokeInternalStreamWrapper(
+            'ftruncate',
+            $this->internalResource,
+            $new_size
+        );
+    }
+
+    /**
+     * @param string $data
+     * @return int
+     */
+    public function stream_write($data)
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fwrite',
+            $this->internalResource,
+            $data
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    public function unlink($path)
+    {
+        $this->assert($path, Behavior::COMMAND_UNLINK);
+        return $this->invokeInternalStreamWrapper(
+            'unlink',
+            $path,
+            $this->context
+        );
+    }
+
+    /**
+     * @param string $path
+     * @param int $flags
+     * @return array|false
+     */
+    public function url_stat($path, $flags)
+    {
+        $this->assert($path, Behavior::COMMAND_URL_STAT);
+        $functionName = $flags & STREAM_URL_STAT_QUIET ? '@stat' : 'stat';
+        return $this->invokeInternalStreamWrapper($functionName, $path);
+    }
+
+    /**
+     * @param string $path
+     * @param string $command
+     */
+    protected function assert($path, $command)
+    {
+        if (Manager::instance()->assert($path, $command) === true) {
+            $this->collectInvocation($path);
+            return;
+        }
+
+        throw new Exception(
+            sprintf(
+                'Denied invocation of "%s" for command "%s"',
+                $path,
+                $command
+            ),
+            1535189880
+        );
+    }
+
+    /**
+     * @param string $path
+     */
+    protected function collectInvocation($path)
+    {
+        if (isset($this->invocation)) {
+            return;
+        }
+
+        $manager = Manager::instance();
+        $this->invocation = $manager->resolve($path);
+        if ($this->invocation === null) {
+            throw new Exception(
+                'Expected invocation could not be resolved',
+                1556389591
+            );
+        }
+        // confirm, previous interceptor(s) validated invocation
+        $this->invocation->confirm();
+        $collection = $manager->getCollection();
+        if (!$collection->has($this->invocation)) {
+            $collection->collect($this->invocation);
+        }
+    }
+
+    /**
+     * @return Manager|Assertable
+     * @deprecated Use Manager::instance() directly
+     */
+    protected function resolveAssertable()
+    {
+        return Manager::instance();
+    }
+
+    /**
+     * Invokes commands on the native PHP Phar stream wrapper.
+     *
+     * @param string $functionName
+     * @param mixed ...$arguments
+     * @return mixed
+     */
+    private function invokeInternalStreamWrapper($functionName)
+    {
+        $arguments = func_get_args();
+        array_shift($arguments);
+        $silentExecution = $functionName[0] === '@';
+        $functionName = ltrim($functionName, '@');
+        $this->restoreInternalSteamWrapper();
+
+        try {
+            if ($silentExecution) {
+                $result = @call_user_func_array($functionName, $arguments);
+            } else {
+                $result = call_user_func_array($functionName, $arguments);
+            }
+        } catch (\Exception $exception) {
+            $this->registerStreamWrapper();
+            throw $exception;
+        } catch (\Throwable $throwable) {
+            $this->registerStreamWrapper();
+            throw $throwable;
+        }
+
+        $this->registerStreamWrapper();
+        return $result;
+    }
+
+    private function restoreInternalSteamWrapper()
+    {
+        stream_wrapper_restore('phar');
+    }
+
+    private function registerStreamWrapper()
+    {
+        stream_wrapper_unregister('phar');
+        stream_wrapper_register('phar', get_class($this));
+    }
+}

+ 24 - 0
misc/typo3/phar-stream-wrapper/src/Resolvable.php

@@ -0,0 +1,24 @@
+<?php
+namespace TYPO3\PharStreamWrapper;
+
+/*
+ * This file is part of the TYPO3 project.
+ *
+ * It is free software; you can redistribute it and/or modify it under the terms
+ * of the MIT License (MIT). For the full copyright and license information,
+ * please read the LICENSE file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
+
+interface Resolvable
+{
+    /**
+     * @param string $path
+     * @param null|int $flags
+     * @return null|PharInvocation
+     */
+    public function resolve($path, $flags = null);
+}

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