From 0804dc7cae6c2e01a4102babfabf0230ce550f68 Mon Sep 17 00:00:00 2001 From: Pantheon Automation Date: Wed, 3 Jun 2020 11:35:03 -0700 Subject: [PATCH] Update to Drupal 7.71. For more information, see https://www.drupal.org/project/drupal/releases/7.71 --- CHANGELOG.txt | 4 +- includes/batch.inc | 15 +- includes/bootstrap.inc | 2 +- includes/common.inc | 6 +- includes/filetransfer/filetransfer.inc | 2 +- includes/menu.inc | 3 + includes/pager.inc | 30 ++ includes/path.inc | 14 +- includes/request-sanitizer.inc | 2 +- misc/ajax.js | 19 + misc/typo3/phar-stream-wrapper/.gitignore | 3 - misc/typo3/phar-stream-wrapper/README.md | 5 +- misc/typo3/phar-stream-wrapper/composer.json | 4 +- misc/typo3/phar-stream-wrapper/src/Helper.php | 4 +- .../phar-stream-wrapper/src/Phar/Reader.php | 40 +- .../src/PharStreamWrapper.php | 2 +- .../src/Resolver/PharInvocationResolver.php | 18 +- modules/block/block.module | 3 +- modules/color/color.module | 5 +- modules/comment/comment.install | 3 - modules/comment/comment.test | 55 +++ modules/field/modules/number/number.test | 2 +- modules/field/tests/field_test.storage.inc | 6 +- modules/field_ui/field_ui.admin.inc | 4 + modules/field_ui/field_ui.module | 7 +- modules/filter/filter.api.php | 4 +- modules/forum/forum.module | 3 +- modules/search/search.extender.inc | 2 +- modules/search/search.module | 2 +- modules/simpletest/simpletest.info | 1 + .../simpletest/tests/request_sanitizer.test | 354 ++++++++++++++++++ modules/system/system.install | 7 + modules/system/system.test | 11 +- modules/taxonomy/taxonomy.install | 2 +- scripts/run-tests.sh | 2 +- 35 files changed, 589 insertions(+), 57 deletions(-) delete mode 100644 misc/typo3/phar-stream-wrapper/.gitignore create mode 100644 modules/simpletest/tests/request_sanitizer.test diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 870dd344635..f56fd2b48ac 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ -Drupal 7.xx, xxxx-xx-xx (development version) +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 ----------------------- diff --git a/includes/batch.inc b/includes/batch.inc index e89ab8dec93..4d4e504d5d0 100644 --- a/includes/batch.inc +++ b/includes/batch.inc @@ -478,18 +478,17 @@ function _batch_finished() { $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 = NULL; - // Clean-up the session. Not needed for CLI updates. - if (isset($_SESSION)) { - unset($_SESSION['batches'][$batch['id']]); - if (empty($_SESSION['batches'])) { - unset($_SESSION['batches']); - } - } - // Redirect if needed. if ($_batch['progressive']) { // Revert the 'destination' that was saved in batch_process(). diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index a66d64dbf20..ce0f8eb596d 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.70'); +define('VERSION', '7.71'); /** * Core API compatibility. diff --git a/includes/common.inc b/includes/common.inc index 0041671549a..cfb29576f37 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -391,7 +391,7 @@ function drupal_add_feed($url = NULL, $title = '') { */ function drupal_get_feeds($delimiter = "\n") { $feeds = drupal_add_feed(); - return implode($feeds, $delimiter); + return implode($delimiter, $feeds); } /** @@ -3743,7 +3743,7 @@ function _drupal_build_css_path($matches, $base = NULL) { } // Prefix with base and remove '../' segments where possible. - $path = $_base . $matches[1]; + $path = $_base . (isset($matches[1]) ? $matches[1] : ''); $last = ''; while ($path != $last) { $last = $path; @@ -6673,7 +6673,7 @@ function element_children(&$elements, $sort = FALSE) { $children = array(); $sortable = FALSE; foreach ($elements as $key => $value) { - if ($key === '' || $key[0] !== '#') { + if (is_int($key) || $key === '' || $key[0] !== '#') { $children[$key] = $value; if (is_array($value) && isset($value['#weight'])) { $sortable = TRUE; diff --git a/includes/filetransfer/filetransfer.inc b/includes/filetransfer/filetransfer.inc index 6c55b2f43f5..cd420bd06a2 100644 --- a/includes/filetransfer/filetransfer.inc +++ b/includes/filetransfer/filetransfer.inc @@ -301,7 +301,7 @@ abstract class FileTransfer { $parts = explode('/', $path); $chroot = ''; while (count($parts)) { - $check = implode($parts, '/'); + $check = implode('/', $parts); if ($this->isFile($check . '/' . drupal_basename(__FILE__))) { // Remove the trailing slash. return substr($chroot, 0, -1); diff --git a/includes/menu.inc b/includes/menu.inc index ca37ba509dc..2b489d88645 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -2483,6 +2483,9 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) { // untranslated paths). Afterwards, the most relevant path is picked from // the menus, ordered by menu preference. $item = menu_get_item($path); + if ($item === FALSE) { + return FALSE; + } $path_candidates = array(); // 1. The current item href. $path_candidates[$item['href']] = $item['href']; diff --git a/includes/pager.inc b/includes/pager.inc index 17c042d64de..316e17d5ad8 100644 --- a/includes/pager.inc +++ b/includes/pager.inc @@ -324,6 +324,16 @@ function theme_pager($variables) { $quantity = empty($variables['quantity']) ? 0 : $variables['quantity']; 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: // Middle is used to "center" pages around the current page. $pager_middle = ceil($quantity / 2); @@ -455,6 +465,11 @@ function theme_pager_first($variables) { global $pager_page_array; $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 ($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)); @@ -485,6 +500,11 @@ function theme_pager_previous($variables) { global $pager_page_array; $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 ($pager_page_array[$element] > 0) { $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; $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 ($pager_page_array[$element] < ($pager_total[$element] - 1)) { $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; $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 ($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)); diff --git a/includes/path.inc b/includes/path.inc index 6bd48d30634..28d1146a5e2 100644 --- a/includes/path.inc +++ b/includes/path.inc @@ -466,13 +466,15 @@ function path_delete($criteria) { $criteria = array('pid' => $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']); } /** diff --git a/includes/request-sanitizer.inc b/includes/request-sanitizer.inc index 7214436b8ad..6cd7fd3f780 100644 --- a/includes/request-sanitizer.inc +++ b/includes/request-sanitizer.inc @@ -99,7 +99,7 @@ class DrupalRequestSanitizer { protected static function stripDangerousValues($input, array $whitelist, array &$sanitized_keys) { if (is_array($input)) { foreach ($input as $key => $value) { - if ($key !== '' && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) { + if ($key !== '' && is_string($key) && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) { unset($input[$key]); $sanitized_keys[] = $key; } diff --git a/misc/ajax.js b/misc/ajax.js index c944ebbf246..0c9579b00d2 100644 --- a/misc/ajax.js +++ b/misc/ajax.js @@ -198,6 +198,25 @@ Drupal.ajax = function (base, element, element_settings) { 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. $(ajax.element).bind(element_settings.event, function (event) { if (!Drupal.settings.urlIsAjaxTrusted[ajax.url] && !Drupal.urlIsLocal(ajax.url)) { diff --git a/misc/typo3/phar-stream-wrapper/.gitignore b/misc/typo3/phar-stream-wrapper/.gitignore deleted file mode 100644 index 157ff0c5989..00000000000 --- a/misc/typo3/phar-stream-wrapper/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.idea -vendor/ -composer.lock diff --git a/misc/typo3/phar-stream-wrapper/README.md b/misc/typo3/phar-stream-wrapper/README.md index 179bb6fd774..14d1ffd15d3 100644 --- a/misc/typo3/phar-stream-wrapper/README.md +++ b/misc/typo3/phar-stream-wrapper/README.md @@ -1,5 +1,6 @@ [![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 @@ -21,9 +22,11 @@ and has been addressed concerning the specific attack vector and for this generi `PharStreamWrapper` in TYPO3 versions 7.6.30 LTS, 8.7.17 LTS and 9.3.1 on 12th July 2018. -* https://typo3.org/security/advisory/typo3-core-sa-2018-002/ * 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 diff --git a/misc/typo3/phar-stream-wrapper/composer.json b/misc/typo3/phar-stream-wrapper/composer.json index 8c224118750..e36b09e7a50 100644 --- a/misc/typo3/phar-stream-wrapper/composer.json +++ b/misc/typo3/phar-stream-wrapper/composer.json @@ -7,7 +7,6 @@ "keywords": ["php", "phar", "stream-wrapper", "security"], "require": { "php": "^5.3.3|^7.0", - "ext-fileinfo": "*", "ext-json": "*", "brumann/polyfill-unserialize": "^1.0" }, @@ -15,6 +14,9 @@ "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/" diff --git a/misc/typo3/phar-stream-wrapper/src/Helper.php b/misc/typo3/phar-stream-wrapper/src/Helper.php index c074ddea048..cdba65ca281 100644 --- a/misc/typo3/phar-stream-wrapper/src/Helper.php +++ b/misc/typo3/phar-stream-wrapper/src/Helper.php @@ -52,7 +52,7 @@ public static function determineBaseFile($path) while (count($parts)) { $currentPath = implode('/', $parts); - if (@is_file($currentPath)) { + if (@is_file($currentPath) && realpath($currentPath) !== false) { return $currentPath; } array_pop($parts); @@ -106,7 +106,7 @@ public static function normalizePath($path) * @param string $path File path to process * @return string */ - private static function normalizeWindowsPath($path) + public static function normalizeWindowsPath($path) { return str_replace('\\', '/', $path); } diff --git a/misc/typo3/phar-stream-wrapper/src/Phar/Reader.php b/misc/typo3/phar-stream-wrapper/src/Phar/Reader.php index 32e516be3a8..6cc124cf753 100644 --- a/misc/typo3/phar-stream-wrapper/src/Phar/Reader.php +++ b/misc/typo3/phar-stream-wrapper/src/Phar/Reader.php @@ -19,6 +19,11 @@ class Reader 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; @@ -139,7 +144,7 @@ private function extractData($fileName) */ private function resolveStream() { - if ($this->fileType === 'application/x-gzip') { + if ($this->fileType === 'application/x-gzip' || $this->fileType === 'application/gzip') { return 'compress.zlib://'; } elseif ($this->fileType === 'application/x-bzip2') { return 'compress.bzip2://'; @@ -152,8 +157,37 @@ private function resolveStream() */ private function determineFileType() { - $fileInfo = new \finfo(); - return $fileInfo->file($this->fileName, FILEINFO_MIME_TYPE); + 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; } /** diff --git a/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php b/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php index acd5656f47b..d704d26ab1d 100644 --- a/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php +++ b/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php @@ -476,7 +476,7 @@ private function invokeInternalStreamWrapper($functionName) { $arguments = func_get_args(); array_shift($arguments); - $silentExecution = $functionName{0} === '@'; + $silentExecution = $functionName[0] === '@'; $functionName = ltrim($functionName, '@'); $this->restoreInternalSteamWrapper(); diff --git a/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php b/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php index 80b86d3db42..1dc42e8597e 100644 --- a/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php +++ b/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php @@ -14,6 +14,7 @@ use TYPO3\PharStreamWrapper\Helper; use TYPO3\PharStreamWrapper\Manager; use TYPO3\PharStreamWrapper\Phar\Reader; +use TYPO3\PharStreamWrapper\Phar\ReaderException; use TYPO3\PharStreamWrapper\Resolvable; class PharInvocationResolver implements Resolvable @@ -59,7 +60,7 @@ public function resolve($path, $flags = null) { $hasPharPrefix = Helper::hasPharPrefix($path); if ($flags === null) { - $flags = static::RESOLVE_REALPATH | static::RESOLVE_ALIAS | static::ASSERT_INTERNAL_INVOCATION; + $flags = static::RESOLVE_REALPATH | static::RESOLVE_ALIAS; } if ($hasPharPrefix && $flags & static::RESOLVE_ALIAS) { @@ -147,9 +148,14 @@ private function resolveBaseName($path, $flags) } // ensure the possible alias name (how we have been called initially) matches // the resolved alias name that was retrieved by the current possible base name - $reader = new Reader($currentBaseName); - $currentAlias = $reader->resolveContainer()->getAlias(); - if ($currentAlias !== $possibleAlias) { + try { + $reader = new Reader($currentBaseName); + $currentAlias = $reader->resolveContainer()->getAlias(); + } catch (ReaderException $exception) { + // most probably that was not a Phar file + continue; + } + if (empty($currentAlias) || $currentAlias !== $possibleAlias) { continue; } $this->addBaseName($currentBaseName); @@ -215,7 +221,9 @@ private function addBaseName($baseName) if (isset($this->baseNames[$baseName])) { return; } - $this->baseNames[$baseName] = realpath($baseName); + $this->baseNames[$baseName] = Helper::normalizeWindowsPath( + realpath($baseName) + ); } /** diff --git a/modules/block/block.module b/modules/block/block.module index d68ea9e7a9a..a482d261269 100644 --- a/modules/block/block.module +++ b/modules/block/block.module @@ -263,7 +263,7 @@ function block_page_build(&$page) { $all_regions = system_region_list($theme); $item = menu_get_item(); - if ($item['path'] != 'admin/structure/block/demo/' . $theme) { + if ($item === FALSE || $item['path'] != 'admin/structure/block/demo/' . $theme) { // Load all region content assigned via blocks. foreach (array_keys($all_regions) as $region) { // Assign blocks to region. @@ -283,7 +283,6 @@ function block_page_build(&$page) { } else { // Append region description if we are rendering the regions demo page. - $item = menu_get_item(); if ($item['path'] == 'admin/structure/block/demo/' . $theme) { foreach (system_region_list($theme, REGIONS_VISIBLE, FALSE) as $region) { $description = '
' . $all_regions[$region] . '
'; diff --git a/modules/color/color.module b/modules/color/color.module index 5b441aabd35..45acd788ff6 100644 --- a/modules/color/color.module +++ b/modules/color/color.module @@ -734,8 +734,9 @@ function _color_blend($img, $hex1, $hex2, $alpha) { * Converts a hex color into an RGB triplet. */ function _color_unpack($hex, $normalize = FALSE) { - if (strlen($hex) == 4) { - $hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3]; + $hex = substr($hex, 1); + if (strlen($hex) == 3) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } $c = hexdec($hex); for ($i = 16; $i >= 0; $i -= 8) { diff --git a/modules/comment/comment.install b/modules/comment/comment.install index e4da58f383b..9f88c0a0043 100644 --- a/modules/comment/comment.install +++ b/modules/comment/comment.install @@ -9,9 +9,6 @@ * Implements hook_uninstall(). */ function comment_uninstall() { - // Delete comment_body field. - field_delete_field('comment_body'); - // Remove variables. variable_del('comment_block_count'); $node_types = array_keys(node_type_get_types()); diff --git a/modules/comment/comment.test b/modules/comment/comment.test index e087a7173ee..b70fa26c387 100644 --- a/modules/comment/comment.test +++ b/modules/comment/comment.test @@ -6,6 +6,7 @@ */ class CommentHelperCase extends DrupalWebTestCase { + protected $super_user; protected $admin_user; protected $web_user; protected $node; @@ -19,6 +20,7 @@ class CommentHelperCase extends DrupalWebTestCase { parent::setUp($modules); // Create users and test node. + $this->super_user = $this->drupalCreateUser(array('access administration pages', 'administer modules')); $this->admin_user = $this->drupalCreateUser(array('administer content types', 'administer comments', 'administer blocks', 'administer actions', 'administer fields')); $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'create article content', 'edit own comments')); $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->uid)); @@ -2264,3 +2266,56 @@ class CommentNodeChangesTestCase extends CommentHelperCase { $this->assertFalse(comment_load($comment->id), 'The comment could not be loaded after the node was deleted.'); } } + +/** + * Tests uninstalling the comment module. + */ +class CommentUninstallTestCase extends CommentHelperCase { + + public static function getInfo() { + return array( + 'name' => 'Comment module uninstallation', + 'description' => 'Tests that the comments module can be properly uninstalled.', + 'group' => 'Comment', + ); + } + + function testCommentUninstall() { + $this->drupalLogin($this->super_user); + + // Disable comment module. + $edit['modules[Core][comment][enable]'] = FALSE; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->assertText(t('The configuration options have been saved.'), 'Comment module was disabled.'); + + // Uninstall comment module. + $edit = array('uninstall[comment]' => 'comment'); + $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall')); + $this->drupalPost(NULL, NULL, t('Uninstall')); + $this->assertText(t('The selected modules have been uninstalled.'), 'Comment module was uninstalled.'); + + // Run cron and clear field cache so that comment fields and instances + // marked for deletion are actually removed. + $this->cronRun(); + field_cache_clear(); + + // Verify that comment fields have been removed. + $all_fields = array_keys(field_info_field_map()); + $this->assertFalse(in_array('comment_body', $all_fields), 'Comment fields were removed by uninstall.'); + + // Verify that comment field instances have been removed (or at least marked + // for deletion). + // N.B. field_read_instances does an INNER JOIN on field_config so if the + // comment_body row has been removed successfully from there no instances + // will be returned, but that does not guarantee that no rows are left over + // in the field_config_instance table. + $count = db_select('field_config_instance', 'fci') + ->condition('entity_type', 'comment') + ->condition('field_name', 'comment_body') + ->condition('deleted', 0) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertTrue($count == 0, 'Comment field instances were removed by uninstall.'); + } +} diff --git a/modules/field/modules/number/number.test b/modules/field/modules/number/number.test index db225855bf4..f1b2b7ec0b6 100644 --- a/modules/field/modules/number/number.test +++ b/modules/field/modules/number/number.test @@ -174,7 +174,7 @@ class NumberFieldTestCase extends DrupalWebTestCase { ), 'display' => array( 'default' => array( - 'type' => 'number_float', + 'type' => 'number_decimal', ), ), ); diff --git a/modules/field/tests/field_test.storage.inc b/modules/field/tests/field_test.storage.inc index a26af176552..03eae4a6f63 100644 --- a/modules/field/tests/field_test.storage.inc +++ b/modules/field/tests/field_test.storage.inc @@ -455,13 +455,13 @@ function field_test_field_attach_rename_bundle($bundle_old, $bundle_new) { function field_test_field_attach_delete_bundle($entity_type, $bundle, $instances) { $data = _field_test_storage_data(); - foreach ($instances as $field_name => $instance) { - $field = field_info_field($field_name); + foreach ($instances as $instance) { + $field = field_info_field_by_id($instance['field_id']); if ($field['storage']['type'] == 'field_test_storage') { $field_data = &$data[$field['id']]; foreach (array('current', 'revisions') as $sub_table) { foreach ($field_data[$sub_table] as &$row) { - if ($row->bundle == $bundle_old) { + if ($row->bundle == $bundle) { $row->deleted = TRUE; } } diff --git a/modules/field_ui/field_ui.admin.inc b/modules/field_ui/field_ui.admin.inc index 7d09d6f8eb3..1bdaa45ddce 100644 --- a/modules/field_ui/field_ui.admin.inc +++ b/modules/field_ui/field_ui.admin.inc @@ -1026,6 +1026,10 @@ function field_ui_display_overview_form($form, &$form_state, $entity_type, $bund $instance['display'][$view_mode]['type'] = $formatter_type; $formatter = field_info_formatter_types($formatter_type); + // For hidden fields, $formatter will be NULL, but we expect an array later. + // To maintain BC, but avoid PHP 7.4 Notices, ensure $formatter is an array + // with a 'module' element. + $formatter['module'] = isset($formatter['module']) ? $formatter['module'] : ''; $instance['display'][$view_mode]['module'] = $formatter['module']; $instance['display'][$view_mode]['settings'] = $settings; diff --git a/modules/field_ui/field_ui.module b/modules/field_ui/field_ui.module index 3b5f28a0378..f1786270692 100644 --- a/modules/field_ui/field_ui.module +++ b/modules/field_ui/field_ui.module @@ -265,6 +265,12 @@ function field_ui_menu_title($instance) { * Menu access callback for the 'view mode display settings' pages. */ function _field_ui_view_mode_menu_access($entity_type, $bundle, $view_mode, $access_callback) { + // It's good practice to call func_get_args() at the beginning of a function + // to avoid problems with function parameters being modified later. The + // behavior of func_get_args() changed in PHP7. + // @see https://www.php.net/manual/en/migration70.incompatible.php#migration70.incompatible.other.func-parameter-modified + $all_args = func_get_args(); + // First, determine visibility according to the 'use custom display' // setting for the view mode. $bundle = field_extract_bundle($entity_type, $bundle); @@ -275,7 +281,6 @@ function _field_ui_view_mode_menu_access($entity_type, $bundle, $view_mode, $acc // part of _menu_check_access(). if ($visibility) { // Grab the variable 'access arguments' part. - $all_args = func_get_args(); $args = array_slice($all_args, 4); $callback = empty($access_callback) ? 0 : trim($access_callback); if (is_numeric($callback)) { diff --git a/modules/filter/filter.api.php b/modules/filter/filter.api.php index 2901eb95bf5..3b0830e777a 100644 --- a/modules/filter/filter.api.php +++ b/modules/filter/filter.api.php @@ -202,7 +202,7 @@ function callback_filter_settings($form, &$form_state, $filter, $format, $defaul */ function callback_filter_prepare($text, $filter, $format, $langcode, $cache, $cache_id) { // Escape and tags. - $text = preg_replace('|(.+?)|se', "[codefilter_code]$1[/codefilter_code]", $text); + $text = preg_replace('|(.+?)|s', "[codefilter_code]$1[/codefilter_code]", $text); return $text; } @@ -234,7 +234,7 @@ function callback_filter_prepare($text, $filter, $format, $langcode, $cache, $ca * @ingroup callbacks */ function callback_filter_process($text, $filter, $format, $langcode, $cache, $cache_id) { - $text = preg_replace('|\[codefilter_code\](.+?)\[/codefilter_code\]|se', "
$1
", $text); + $text = preg_replace('|\[codefilter_code\](.+?)\[/codefilter_code\]|s', "
$1
", $text); return $text; } diff --git a/modules/forum/forum.module b/modules/forum/forum.module index 1224418a9fa..0baddd4ee24 100644 --- a/modules/forum/forum.module +++ b/modules/forum/forum.module @@ -922,7 +922,8 @@ function forum_get_topics($tid, $sortby, $forum_per_page) { ); $order = _forum_get_topic_order($sortby); - for ($i = 0; $i < count($forum_topic_list_header); $i++) { + // Skip element with index 0 which is NULL. + for ($i = 1; $i < count($forum_topic_list_header); $i++) { if ($forum_topic_list_header[$i]['field'] == $order['field']) { $forum_topic_list_header[$i]['sort'] = $order['sort']; } diff --git a/modules/search/search.extender.inc b/modules/search/search.extender.inc index 407425696cb..a9abf0d86b2 100644 --- a/modules/search/search.extender.inc +++ b/modules/search/search.extender.inc @@ -219,7 +219,7 @@ class SearchQuery extends SelectQueryExtender { } $phrase = FALSE; // Strip off phrase quotes. - if ($match[2]{0} == '"') { + if ($match[2][0] == '"') { $match[2] = substr($match[2], 1, -1); $phrase = TRUE; $this->simple = FALSE; diff --git a/modules/search/search.module b/modules/search/search.module index 7542f989180..7d4db0b4bc7 100644 --- a/modules/search/search.module +++ b/modules/search/search.module @@ -1172,7 +1172,7 @@ function search_excerpt($keys, $text) { } else { $info = search_simplify_excerpt_match($key, $text, $included[$key], $boundary); - if ($info['where']) { + if (isset($info['where'])) { $p = $info['where']; if ($info['keyword']) { $foundkeys[] = $info['keyword']; diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info index 1aec619f5b6..5945f7fcf43 100644 --- a/modules/simpletest/simpletest.info +++ b/modules/simpletest/simpletest.info @@ -33,6 +33,7 @@ files[] = tests/pager.test files[] = tests/password.test files[] = tests/path.test files[] = tests/registry.test +files[] = tests/request_sanitizer.test files[] = tests/schema.test files[] = tests/session.test files[] = tests/tablesort.test diff --git a/modules/simpletest/tests/request_sanitizer.test b/modules/simpletest/tests/request_sanitizer.test new file mode 100644 index 00000000000..9fc811b2f0c --- /dev/null +++ b/modules/simpletest/tests/request_sanitizer.test @@ -0,0 +1,354 @@ + 'DrupalRequestSanitizer', + 'description' => 'Test the DrupalRequestSanitizer class', + 'group' => 'System', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + require_once DRUPAL_ROOT . '/includes/request-sanitizer.inc'; + parent::setUp(); + set_error_handler(array($this, "sanitizerTestErrorHandler")); + } + + /** + * Iterate through all the RequestSanitizerTests. + */ + public function testRequestSanitization() { + foreach ($this->requestSanitizerTests() as $label => $data) { + $this->errors = array(); + // Normalize the test parameters. + $test = array( + 'request' => $data[0], + 'expected' => isset($data[1]) ? $data[1] : array(), + 'expected_errors' => isset($data[2]) ? $data[2] : NULL, + 'whitelist' => isset($data[3]) ? $data[3] : array(), + ); + $this->requestSanitizationTest($test['request'], $test['expected'], $test['expected_errors'], $test['whitelist'], $label); + } + } + + /** + * Tests RequestSanitizer class. + * + * @param \SanitizerTestRequest $request + * The request to sanitize. + * @param array $expected + * An array of expected request parameters after sanitization. + * @param array|null $expected_errors + * An array of expected errors. If set to NULL then error logging is + * disabled. + * @param array $whitelist + * An array of keys to whitelist and not sanitize. + * @param string $label + * A descriptive name for each test / group of assertions. + * + * @throws \ReflectionException + */ + public function requestSanitizationTest(SanitizerTestRequest $request, array $expected = array(), array $expected_errors = NULL, array $whitelist = array(), $label = NULL) { + // Set up globals. + $_GET = $request->getQuery(); + $_POST = $request->getRequest(); + $_COOKIE = $request->getCookies(); + $_REQUEST = array_merge($request->getQuery(), $request->getRequest()); + + $GLOBALS['conf']['sanitize_input_whitelist'] = $whitelist; + $GLOBALS['conf']['sanitize_input_logging'] = is_null($expected_errors) ? FALSE : TRUE; + if ($label !== 'already sanitized request') { + $reflection = new \ReflectionProperty('DrupalRequestSanitizer', 'sanitized'); + $reflection->setAccessible(TRUE); + $reflection->setValue(NULL, FALSE); + } + DrupalRequestSanitizer::sanitize(); + if (isset($_GET['destination'])) { + DrupalRequestSanitizer::cleanDestination(); + } + + // Normalise the expected data. + $expected += array( + 'cookies' => array(), + 'query' => array(), + 'request' => array(), + ); + + // Test PHP globals. + $this->assertEqualLabelled($expected['cookies'], $_COOKIE, NULL, 'Other', $label . ' (COOKIE)'); + $this->assertEqualLabelled($expected['query'], $_GET, NULL, 'Other', $label . ' (GET)'); + $this->assertEqualLabelled($expected['request'], $_POST, NULL, 'Other', $label . ' (POST)'); + $expected_request = array_merge($expected['query'], $expected['request']); + $this->assertEqualLabelled($expected_request, $_REQUEST, NULL, 'Other', $label . ' (REQUEST)'); + + // Ensure any expected errors have been triggered. + if (!empty($expected_errors)) { + foreach ($expected_errors as $expected_error) { + $this->assertError($expected_error, E_USER_NOTICE, $label . ' (errors)'); + } + } + else { + $this->assertEqualLabelled(array(), $this->errors, NULL, 'Other', $label . ' (errors)'); + } + } + + /** + * Data provider for testRequestSanitization. + * + * @return array + * A list of tests to carry out. + */ + public function requestSanitizerTests() { + $tests = array(); + + $request = new SanitizerTestRequest(array('q' => 'index.php')); + $tests['no sanitization GET'] = array($request, array('query' => array('q' => 'index.php'))); + + $request = new SanitizerTestRequest(array(), array('field' => 'value')); + $tests['no sanitization POST'] = array($request, array('request' => array('field' => 'value'))); + + $request = new SanitizerTestRequest(array(), array(), array(), array('key' => 'value')); + $tests['no sanitization COOKIE'] = array($request, array('cookies' => array('key' => 'value'))); + + $request = new SanitizerTestRequest(array('q' => 'index.php'), array('field' => 'value'), array(), array('key' => 'value')); + $tests['no sanitization GET, POST, COOKIE'] = array($request, array('query' => array('q' => 'index.php'), 'request' => array('field' => 'value'), 'cookies' => array('key' => 'value'))); + + $request = new SanitizerTestRequest(array('q' => 'index.php')); + $tests['no sanitization GET log'] = array($request, array('query' => array('q' => 'index.php')), array()); + + $request = new SanitizerTestRequest(array(), array('field' => 'value')); + $tests['no sanitization POST log'] = array($request, array('request' => array('field' => 'value')), array()); + + $request = new SanitizerTestRequest(array(), array(), array(), array('key' => 'value')); + $tests['no sanitization COOKIE log'] = array($request, array('cookies' => array('key' => 'value')), array()); + + $request = new SanitizerTestRequest(array('#q' => 'index.php')); + $tests['sanitization GET'] = array($request); + + $request = new SanitizerTestRequest(array(), array('#field' => 'value')); + $tests['sanitization POST'] = array($request); + + $request = new SanitizerTestRequest(array(), array(), array(), array('#key' => 'value')); + $tests['sanitization COOKIE'] = array($request); + + $request = new SanitizerTestRequest(array('#q' => 'index.php'), array('#field' => 'value'), array(), array('#key' => 'value')); + $tests['sanitization GET, POST, COOKIE'] = array($request); + + $request = new SanitizerTestRequest(array('#q' => 'index.php')); + $tests['sanitization GET log'] = array($request, array(), array('Potentially unsafe keys removed from query string parameters (GET): #q')); + + $request = new SanitizerTestRequest(array(), array('#field' => 'value')); + $tests['sanitization POST log'] = array($request, array(), array('Potentially unsafe keys removed from request body parameters (POST): #field')); + + $request = new SanitizerTestRequest(array(), array(), array(), array('#key' => 'value')); + $tests['sanitization COOKIE log'] = array($request, array(), array('Potentially unsafe keys removed from cookie parameters (COOKIE): #key')); + + $request = new SanitizerTestRequest(array('#q' => 'index.php'), array('#field' => 'value'), array(), array('#key' => 'value')); + $tests['sanitization GET, POST, COOKIE log'] = array($request, array(), array('Potentially unsafe keys removed from query string parameters (GET): #q', 'Potentially unsafe keys removed from request body parameters (POST): #field', 'Potentially unsafe keys removed from cookie parameters (COOKIE): #key')); + + $request = new SanitizerTestRequest(array('q' => 'index.php', 'foo' => array('#bar' => 'foo'))); + $tests['recursive sanitization log'] = array($request, array('query' => array('q' => 'index.php', 'foo' => array())), array('Potentially unsafe keys removed from query string parameters (GET): #bar')); + + $request = new SanitizerTestRequest(array('q' => 'index.php', 'foo' => array('#bar' => 'foo'))); + $tests['recursive no sanitization whitelist'] = array($request, array('query' => array('q' => 'index.php', 'foo' => array('#bar' => 'foo'))), array(), array('#bar')); + + $request = new SanitizerTestRequest(array(), array('#field' => 'value')); + $tests['no sanitization POST whitelist'] = array($request, array('request' => array('#field' => 'value')), array(), array('#field')); + + $request = new SanitizerTestRequest(array('q' => 'index.php', 'foo' => array('#bar' => 'foo', '#foo' => 'bar'))); + $tests['recursive multiple sanitization log'] = array($request, array('query' => array('q' => 'index.php', 'foo' => array())), array('Potentially unsafe keys removed from query string parameters (GET): #bar, #foo')); + + $request = new SanitizerTestRequest(array('#q' => 'index.php')); + $tests['already sanitized request'] = array($request, array('query' => array('#q' => 'index.php'))); + + $request = new SanitizerTestRequest(array('destination' => 'whatever?%23test=value')); + $tests['destination removal GET'] = array($request); + + $request = new SanitizerTestRequest(array('destination' => 'whatever?%23test=value')); + $tests['destination removal GET log'] = array($request, array(), array('Potentially unsafe destination removed from query string parameters (GET) because it contained the following keys: #test')); + + $request = new SanitizerTestRequest(array('destination' => 'whatever?q[%23test]=value')); + $tests['destination removal subkey'] = array($request); + + $request = new SanitizerTestRequest(array('destination' => 'whatever?q[%23test]=value')); + $tests['destination whitelist'] = array($request, array('query' => array('destination' => 'whatever?q[%23test]=value')), array(), array('#test')); + + $request = new SanitizerTestRequest(array('destination' => "whatever?\x00bar=base&%23test=value")); + $tests['destination removal zero byte'] = array($request); + + $request = new SanitizerTestRequest(array('destination' => 'whatever?q=value')); + $tests['destination kept'] = array($request, array('query' => array('destination' => 'whatever?q=value'))); + + $request = new SanitizerTestRequest(array('destination' => 'whatever')); + $tests['destination no query'] = array($request, array('query' => array('destination' => 'whatever'))); + + return $tests; + } + + /** + * Catches and logs errors to $this->errors. + * + * @param int $errno + * The severity level of the error. + * @param string $errstr + * The error message. + */ + public function sanitizerTestErrorHandler($errno, $errstr) { + $this->errors[] = compact('errno', 'errstr'); + } + + /** + * Asserts that the expected error has been logged. + * + * @param string $errstr + * The error message. + * @param int $errno + * The severity level of the error. + * @param string $label + * The label to include with the message. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertError($errstr, $errno, $label) { + $label = (empty($label)) ? '' : $label . ': '; + foreach ($this->errors as $error) { + if ($error['errstr'] === $errstr && $error['errno'] === $errno) { + return $this->pass($label . "Error with level $errno and message '$errstr' found"); + } + } + return $this->fail($label . "Error with level $errno and message '$errstr' not found in " . var_export($this->errors, TRUE)); + } + + /** + * Asserts two values are equal, includes a label. + * + * @param mixed $first + * The first value to check. + * @param mixed $second + * The second value to check. + * @param string $message + * The message to display along with the assertion. + * @param string $group + * The type of assertion - examples are "Browser", "PHP". + * @param string $label + * The label to include with the message. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertEqualLabelled($first, $second, $message = '', $group = 'Other', $label = '') { + $label = (empty($label)) ? '' : $label . ': '; + $message = $message ? $message : t('Value @first is equal to value @second.', array( + '@first' => var_export($first, TRUE), + '@second' => var_export($second, TRUE), + )); + return $this->assert($first == $second, $label . $message, $group); + } + +} + +/** + * Basic HTTP Request class. + */ +class SanitizerTestRequest { + + /** + * The query (GET). + * + * @var array + */ + protected $query; + + /** + * The request (POST). + * + * @var array + */ + protected $request; + + /** + * The request attributes. + * + * @var array + */ + protected $attributes; + + /** + * The request cookies. + * + * @var array + */ + protected $cookies; + + /** + * Constructor. + * + * @param array $query + * The GET parameters. + * @param array $request + * The POST parameters. + * @param array $attributes + * The request attributes. + * @param array $cookies + * The COOKIE parameters. + */ + public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array()) { + $this->query = $query; + $this->request = $request; + $this->attributes = $attributes; + $this->cookies = $cookies; + } + + /** + * Getter for $query. + */ + public function getQuery() { + return $this->query; + } + + /** + * Getter for $request. + */ + public function getRequest() { + return $this->request; + } + + /** + * Getter for $attributes. + */ + public function getAttributes() { + return $this->attributes; + } + + /** + * Getter for $cookies. + */ + public function getCookies() { + return $this->cookies; + } + +} diff --git a/modules/system/system.install b/modules/system/system.install index 414e13b3253..43dc90c4e9c 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -3314,6 +3314,13 @@ function system_update_7083() { // Empty update to force a rebuild of hook_library() and JS aggregates. } +/** + * Rebuild JavaScript aggregates to include 'ajax.js' fix for Chrome 83. + */ +function system_update_7084() { + // Empty update to force a rebuild of JS aggregates. +} + /** * @} End of "defgroup updates-7.x-extra". * The next series of updates should start at 8000. diff --git a/modules/system/system.test b/modules/system/system.test index 5ae3341612f..270311ecb06 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -2995,7 +2995,16 @@ class SystemValidTokenTest extends DrupalUnitTestCase { // The following checks will throw PHP notices, so we disable error // assertions. $this->assertErrors = FALSE; - $this->assertFalse(drupal_valid_token(NULL, new stdClass()), 'Token NULL, value object returns FALSE.'); + + try { + $this->assertFalse(drupal_valid_token(NULL, new stdClass()), 'Token NULL, value object returns FALSE.'); + } + // PHP 7.4 compatibility: the stdClass string conversion throws an exception + // which is also an acceptable outcome of this test. + catch (Error $e) { + $this->pass('Token NULL, value object throws error exception which is ok.'); + } + $this->assertFalse(drupal_valid_token(0, array()), 'Token 0, value array returns FALSE.'); $this->assertFalse(drupal_valid_token('', array()), "Token '', value array returns FALSE."); $this->assertFalse('' === drupal_get_token(array()), 'Token generation does not return an empty string on invalid parameters.'); diff --git a/modules/taxonomy/taxonomy.install b/modules/taxonomy/taxonomy.install index 60a9b5d2afb..ecbd0f2e1a7 100644 --- a/modules/taxonomy/taxonomy.install +++ b/modules/taxonomy/taxonomy.install @@ -448,7 +448,7 @@ function taxonomy_update_7004() { } else { $instance['widget'] = array( - 'type' => 'select', + 'type' => 'options_select', 'module' => 'options', 'settings' => array(), ); diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index a42215e8998..8c0be40ef34 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -729,7 +729,7 @@ function simpletest_script_print_error($message) { */ function simpletest_script_print($message, $color_code) { global $args; - if ($args['color']) { + if (!empty($args['color'])) { echo "\033[" . $color_code . "m" . $message . "\033[0m"; } else {