diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f9b010c58cb..dcef8ea97fc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,14 @@ +Drupal 7.80, 2021-04-20 +----------------------- +- Fixed security issues: + - SA-CORE-2021-002 + +Drupal 7.79, 2021-04-07 +----------------------- +- Initial support for PHP 8 +- Support for SameSite cookie attribute +- Avoid write for unchanged fields (opt-in) + Drupal 7.78, 2021-01-19 ----------------------- - Fixed security issues: diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index e79c5d50942..72c92911d2d 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.78'); +define('VERSION', '7.80'); /** * Core API compatibility. @@ -2621,13 +2621,10 @@ function drupal_get_hash_salt() { * The filename that the error was raised in. * @param $line * The line number the error was raised at. - * @param $context - * An array that points to the active symbol table at the point the error - * occurred. */ -function _drupal_error_handler($error_level, $message, $filename, $line, $context) { +function _drupal_error_handler($error_level, $message, $filename, $line) { require_once DRUPAL_ROOT . '/includes/errors.inc'; - _drupal_error_handler_real($error_level, $message, $filename, $line, $context); + _drupal_error_handler_real($error_level, $message, $filename, $line); } /** @@ -3920,3 +3917,85 @@ function drupal_clear_opcode_cache($filepath) { @apc_delete_file($filepath); } } + +/** + * Drupal's wrapper around PHP's setcookie() function. + * + * This allows the cookie's $value and $options to be altered. + * + * @param $name + * The name of the cookie. + * @param $value + * The value of the cookie. + * @param $options + * An associative array which may have any of the keys expires, path, domain, + * secure, httponly, samesite. + * + * @see setcookie() + * @ingroup php_wrappers + */ +function drupal_setcookie($name, $value, $options) { + $options = _drupal_cookie_params($options); + if (\PHP_VERSION_ID >= 70300) { + setcookie($name, $value, $options); + } + else { + setcookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']); + } +} + +/** + * Process the params for cookies. This emulates support for the SameSite + * attribute in earlier versions of PHP, and allows the value of that attribute + * to be overridden. + * + * @param $options + * An associative array which may have any of the keys expires, path, domain, + * secure, httponly, samesite. + * + * @return + * An associative array which may have any of the keys expires, path, domain, + * secure, httponly, and samesite. + */ +function _drupal_cookie_params($options) { + $options['samesite'] = _drupal_samesite_cookie($options); + if (\PHP_VERSION_ID < 70300) { + // Emulate SameSite support in older PHP versions. + if (!empty($options['samesite'])) { + // Ensure the SameSite attribute is only added once. + if (!preg_match('/SameSite=/i', $options['path'])) { + $options['path'] .= '; SameSite=' . $options['samesite']; + } + } + } + return $options; +} + +/** + * Determine the value for the samesite cookie attribute, in the following order + * of precedence: + * + * 1) A value explicitly passed to drupal_setcookie() + * 2) A value set in $conf['samesite_cookie_value'] + * 3) The setting from php ini + * 4) The default of None, or FALSE (no attribute) if the cookie is not Secure + * + * @param $options + * An associative array as passed to drupal_setcookie(). + * @return + * The value for the samesite cookie attribute. + */ +function _drupal_samesite_cookie($options) { + if (isset($options['samesite'])) { + return $options['samesite']; + } + $override = variable_get('samesite_cookie_value', NULL); + if ($override !== NULL) { + return $override; + } + $ini_options = session_get_cookie_params(); + if (isset($ini_options['samesite'])) { + return $ini_options['samesite']; + } + return empty($options['secure']) ? FALSE : 'None'; +} diff --git a/includes/common.inc b/includes/common.inc index 5a39201ddcd..a19a5eaacc6 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -1559,7 +1559,7 @@ function _filter_xss_split($m, $store = FALSE) { return '<'; } - if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)([^>]*)>?|()$%', $string, $matches)) { + if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)\s*([^>]*)>?|()$%', $string, $matches)) { // Seriously malformed. return ''; } @@ -1618,7 +1618,13 @@ function _filter_xss_attributes($attr) { // Attribute name, href for instance. if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { $attrname = strtolower($match[1]); - $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on'); + $skip = ( + $attrname == 'style' || + substr($attrname, 0, 2) == 'on' || + substr($attrname, 0, 1) == '-' || + // Ignore long attributes to avoid unnecessary processing overhead. + strlen($attrname) > 96 + ); $working = $mode = 1; $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr); } @@ -2329,6 +2335,7 @@ function url($path = NULL, array $options = array()) { } elseif (!empty($path) && !$options['alias']) { $language = isset($options['language']) && isset($options['language']->language) ? $options['language']->language : ''; + require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc'); $alias = drupal_get_path_alias($original_path, $language); if ($alias != $original_path) { // Strip leading slashes from internal path aliases to prevent them @@ -5182,7 +5189,9 @@ function drupal_build_js_cache($files) { // Allow modules to act on the js_cache before writing to disk. drupal_alter('js_cache', $contents); - + + // Remove JS source and source mapping urls or these may cause 404 errors. + $contents = preg_replace('/\/\/(#|@)\s(sourceURL|sourceMappingURL)=\s*(\S*?)\s*$/m', '', $contents); // Prefix filename to prevent blocking by firewalls which reject files // starting with "ad*". $filename = 'js_' . drupal_hash_base64($contents) . '.js'; diff --git a/includes/database/database.inc b/includes/database/database.inc index d4d2d8f0220..61ac44f783c 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -184,7 +184,7 @@ * * @see http://php.net/manual/book.pdo.php */ -abstract class DatabaseConnection extends PDO { +abstract class DatabaseConnection { /** * The database target this connection is for. @@ -261,6 +261,13 @@ abstract class DatabaseConnection extends PDO { */ protected $temporaryNameIndex = 0; + /** + * The actual PDO connection. + * + * @var \PDO + */ + protected $connection; + /** * The connection information for this connection object. * @@ -325,14 +332,27 @@ abstract class DatabaseConnection extends PDO { $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; // Call PDO::__construct and PDO::setAttribute. - parent::__construct($dsn, $username, $password, $driver_options); + $this->connection = new PDO($dsn, $username, $password, $driver_options); // Set a Statement class, unless the driver opted out. if (!empty($this->statementClass)) { - $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); + $this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); } } + /** + * Proxy possible direct calls to the \PDO methods. + * + * Since PHP8.0 the signature of the the \PDO::query() method has changed, + * and this class can't extending \PDO any more. + * + * However, for the BC, proxy any calls to the \PDO methods to the actual + * PDO connection object. + */ + public function __call($name, $arguments) { + return call_user_func_array(array($this->connection, $name), $arguments); + } + /** * Destroys this Connection object. * @@ -346,7 +366,7 @@ abstract class DatabaseConnection extends PDO { // The Statement class attribute only accepts a new value that presents a // proper callable, so we reset it to PDOStatement. if (!empty($this->statementClass)) { - $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); + $this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); } $this->schema = NULL; } @@ -521,7 +541,7 @@ abstract class DatabaseConnection extends PDO { $query = $this->prefixTables($query); // Call PDO::prepare. - return parent::prepare($query); + return $this->connection->prepare($query); } /** @@ -733,7 +753,7 @@ abstract class DatabaseConnection extends PDO { case Database::RETURN_AFFECTED: return $stmt->rowCount(); case Database::RETURN_INSERT_ID: - return $this->lastInsertId(); + return $this->connection->lastInsertId(); case Database::RETURN_NULL: return; default: @@ -1116,7 +1136,7 @@ abstract class DatabaseConnection extends PDO { $rolled_back_other_active_savepoints = TRUE; } } - parent::rollBack(); + $this->connection->rollBack(); if ($rolled_back_other_active_savepoints) { throw new DatabaseTransactionOutOfOrderException(); } @@ -1144,7 +1164,7 @@ abstract class DatabaseConnection extends PDO { $this->query('SAVEPOINT ' . $name); } else { - parent::beginTransaction(); + $this->connection->beginTransaction(); } $this->transactionLayers[$name] = $name; } @@ -1195,7 +1215,7 @@ abstract class DatabaseConnection extends PDO { // If there are no more layers left then we should commit. unset($this->transactionLayers[$name]); if (empty($this->transactionLayers)) { - if (!parent::commit()) { + if (!$this->connection->commit()) { throw new DatabaseTransactionCommitFailedException(); } } @@ -1279,7 +1299,7 @@ abstract class DatabaseConnection extends PDO { * Returns the version of the database server. */ public function version() { - return $this->getAttribute(PDO::ATTR_SERVER_VERSION); + return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); } /** @@ -1724,12 +1744,16 @@ abstract class Database { * * @param $key * The connection key. + * @param $close + * Whether to close the connection. * @return * TRUE in case of success, FALSE otherwise. */ - final public static function removeConnection($key) { + final public static function removeConnection($key, $close = TRUE) { if (isset(self::$databaseInfo[$key])) { - self::closeConnection(NULL, $key); + if ($close) { + self::closeConnection(NULL, $key); + } unset(self::$databaseInfo[$key]); return TRUE; } diff --git a/includes/database/mysql/database.inc b/includes/database/mysql/database.inc index 00df3c13e91..b836111987e 100644 --- a/includes/database/mysql/database.inc +++ b/includes/database/mysql/database.inc @@ -345,10 +345,10 @@ class DatabaseConnection_mysql extends DatabaseConnection { // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci' // for UTF-8. if (!empty($connection_options['collation'])) { - $this->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']); + $this->connection->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']); } else { - $this->exec('SET NAMES ' . $charset); + $this->connection->exec('SET NAMES ' . $charset); } // Set MySQL init_commands if not already defined. Default Drupal's MySQL @@ -366,7 +366,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { $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 was removed in MySQL 8.0.11 // https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal - if (version_compare($this->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')) { + if (version_compare($this->connection->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } $connection_options['init_commands'] += array( @@ -375,7 +375,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { // Execute initial commands. foreach ($connection_options['init_commands'] as $sql) { - $this->exec($sql); + $this->connection->exec($sql); } } @@ -536,7 +536,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { // If there are no more layers left then we should commit. unset($this->transactionLayers[$name]); if (empty($this->transactionLayers)) { - if (!PDO::commit()) { + if (!$this->doCommit()) { throw new DatabaseTransactionCommitFailedException(); } } @@ -559,7 +559,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { $this->transactionLayers = array(); // We also have to explain to PDO that the transaction stack has // been cleaned-up. - PDO::commit(); + $this->doCommit(); } else { throw $e; @@ -569,6 +569,53 @@ class DatabaseConnection_mysql extends DatabaseConnection { } } + /** + * Do the actual commit, including a workaround for PHP 8 behaviour changes. + * + * @return bool + * Success or otherwise of the commit. + */ + protected function doCommit() { + if ($this->connection->inTransaction()) { + return $this->connection->commit(); + } + else { + // In PHP 8.0 a PDOException is thrown when a commit is attempted with no + // transaction active. In previous PHP versions this failed silently. + return TRUE; + } + } + + /** + * {@inheritdoc} + */ + public function rollback($savepoint_name = 'drupal_transaction') { + // MySQL will automatically commit transactions when tables are altered or + // created (DDL transactions are not supported). Prevent triggering an + // exception to ensure that the error that has caused the rollback is + // properly reported. + if (!$this->connection->inTransaction()) { + // Before PHP 8 $this->connection->inTransaction() will return TRUE and + // $this->connection->rollback() does not throw an exception; the + // following code is unreachable. + + // If \DatabaseConnection::rollback() would throw an + // exception then continue to throw an exception. + if (!$this->inTransaction()) { + throw new DatabaseTransactionNoActiveException(); + } + // A previous rollback to an earlier savepoint may mean that the savepoint + // in question has already been accidentally committed. + if (!isset($this->transactionLayers[$savepoint_name])) { + throw new DatabaseTransactionNoActiveException(); + } + + trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING); + return; + } + return parent::rollback($savepoint_name); + } + public function utf8mb4IsConfigurable() { return TRUE; } @@ -579,7 +626,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { public function utf8mb4IsSupported() { // Ensure that the MySQL driver supports utf8mb4 encoding. - $version = $this->getAttribute(PDO::ATTR_CLIENT_VERSION); + $version = $this->connection->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); diff --git a/includes/database/pgsql/database.inc b/includes/database/pgsql/database.inc index fb3d0ab5130..96ffc1d3e44 100644 --- a/includes/database/pgsql/database.inc +++ b/includes/database/pgsql/database.inc @@ -66,11 +66,11 @@ class DatabaseConnection_pgsql extends DatabaseConnection { parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); // Force PostgreSQL to use the UTF-8 character set by default. - $this->exec("SET NAMES 'UTF8'"); + $this->connection->exec("SET NAMES 'UTF8'"); // Execute PostgreSQL init_commands. if (isset($connection_options['init_commands'])) { - $this->exec(implode('; ', $connection_options['init_commands'])); + $this->connection->exec(implode('; ', $connection_options['init_commands'])); } } @@ -117,7 +117,7 @@ class DatabaseConnection_pgsql extends DatabaseConnection { case Database::RETURN_AFFECTED: return $stmt->rowCount(); case Database::RETURN_INSERT_ID: - return $this->lastInsertId($options['sequence_name']); + return $this->connection->lastInsertId($options['sequence_name']); case Database::RETURN_NULL: return; default: diff --git a/includes/database/select.inc b/includes/database/select.inc index 84098bdf7bf..674c6b53ad8 100644 --- a/includes/database/select.inc +++ b/includes/database/select.inc @@ -964,7 +964,7 @@ class SelectQuery extends Query implements SelectQueryInterface { */ protected $forUpdate = FALSE; - public function __construct($table, $alias = NULL, DatabaseConnection $connection, $options = array()) { + public function __construct($table, $alias, DatabaseConnection $connection, $options = array()) { $options['return'] = Database::RETURN_STATEMENT; parent::__construct($connection, $options); $this->where = new DatabaseCondition('AND'); diff --git a/includes/database/sqlite/database.inc b/includes/database/sqlite/database.inc index c50f08ec537..2cf83ccb550 100644 --- a/includes/database/sqlite/database.inc +++ b/includes/database/sqlite/database.inc @@ -121,7 +121,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { // Execute sqlite init_commands. if (isset($connection_options['init_commands'])) { - $this->exec(implode('; ', $connection_options['init_commands'])); + $this->connection->exec(implode('; ', $connection_options['init_commands'])); } } @@ -259,7 +259,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { * expose this function to the world. */ public function PDOPrepare($query, array $options = array()) { - return parent::prepare($query, $options); + return $this->connection->prepare($query, $options); } public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { @@ -350,7 +350,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { } } if ($this->supportsTransactions()) { - PDO::rollBack(); + $this->connection->rollBack(); } } @@ -365,7 +365,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { throw new DatabaseTransactionNameNonUniqueException($name . " is already in use."); } if (!$this->inTransaction()) { - PDO::beginTransaction(); + $this->connection->beginTransaction(); } $this->transactionLayers[$name] = $name; } @@ -390,9 +390,9 @@ class DatabaseConnection_sqlite extends DatabaseConnection { // If there was any rollback() we should roll back whole transaction. if ($this->willRollback) { $this->willRollback = FALSE; - PDO::rollBack(); + $this->connection->rollBack(); } - elseif (!PDO::commit()) { + elseif (!$this->connection->commit()) { throw new DatabaseTransactionCommitFailedException(); } } diff --git a/includes/errors.inc b/includes/errors.inc index 3548d1fd827..4401ebe87e0 100644 --- a/includes/errors.inc +++ b/includes/errors.inc @@ -48,11 +48,8 @@ function drupal_error_levels() { * The filename that the error was raised in. * @param $line * The line number the error was raised at. - * @param $context - * An array that points to the active symbol table at the point the error - * occurred. */ -function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) { +function _drupal_error_handler_real($error_level, $message, $filename, $line) { if ($error_level & error_reporting()) { $types = drupal_error_levels(); list($severity_msg, $severity_level) = $types[$error_level]; diff --git a/includes/menu.inc b/includes/menu.inc index 22e6dba97c8..fb99aac8a25 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -317,7 +317,7 @@ define('MENU_PREFERRED_LINK', '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'); * actually exists. This list of 'masks' is built in menu_rebuild(). * * @param $parts - * An array of path parts; for the above example, + * An array of path parts; for the above example. * array('node', '12345', 'edit'). * * @return @@ -2595,7 +2595,7 @@ function menu_get_active_breadcrumb() { // Don't show a link to the current page in the breadcrumb trail. $end = end($active_trail); - if ($item['href'] == $end['href']) { + if (is_array($end) && $item['href'] == $end['href']) { array_pop($active_trail); } diff --git a/includes/session.inc b/includes/session.inc index 11c77c4c10c..a4ce54b7d0d 100644 --- a/includes/session.inc +++ b/includes/session.inc @@ -284,6 +284,20 @@ function drupal_session_start() { // Save current session data before starting it, as PHP will destroy it. $session_data = isset($_SESSION) ? $_SESSION : NULL; + // Apply any overrides to the session cookie params. + $params = $original_params = session_get_cookie_params(); + // PHP settings for samesite will be handled by _drupal_cookie_params(). + unset($params['samesite']); + $params = _drupal_cookie_params($params); + if ($params !== $original_params) { + if (\PHP_VERSION_ID >= 70300) { + session_set_cookie_params($params); + } + else { + session_set_cookie_params($params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']); + } + } + session_start(); drupal_session_started(TRUE); @@ -323,7 +337,14 @@ function drupal_session_commit() { $insecure_session_name = substr(session_name(), 1); $params = session_get_cookie_params(); $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); + $options = array( + 'expires' => $expire, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => FALSE, + 'httponly' => $params['httponly'], + ); + drupal_setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $options); } } // Write the session data. @@ -365,7 +386,14 @@ function drupal_session_regenerate() { // $params['lifetime'] seconds from the current request. If it is not set, // it will expire when the browser is closed. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); + $options = array( + 'expires' => $expire, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => FALSE, + 'httponly' => $params['httponly'], + ); + drupal_setcookie($insecure_session_name, $session_id, $options); $_COOKIE[$insecure_session_name] = $session_id; } @@ -380,7 +408,14 @@ function drupal_session_regenerate() { if (isset($old_session_id)) { $params = session_get_cookie_params(); $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + $options = array( + 'expires' => $expire, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => $params['secure'], + 'httponly' => $params['httponly'], + ); + drupal_setcookie(session_name(), session_id(), $options); $fields = array('sid' => session_id()); if ($is_https) { $fields['ssid'] = session_id(); @@ -488,7 +523,14 @@ function _drupal_session_delete_cookie($name, $secure = NULL) { if ($secure !== NULL) { $params['secure'] = $secure; } - setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + $options = array( + 'expires' => REQUEST_TIME - 3600, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => $params['secure'], + 'httponly' => $params['httponly'], + ); + drupal_setcookie($name, '', $options); unset($_COOKIE[$name]); } } diff --git a/misc/ajax.js b/misc/ajax.js index 79a4e9eb6f5..a809f5738e3 100644 --- a/misc/ajax.js +++ b/misc/ajax.js @@ -408,7 +408,7 @@ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) { // Insert progressbar or throbber. if (this.progress.type == 'bar') { - var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback)); + var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop); if (this.progress.message) { progressBar.setProgress(-1, this.progress.message); } diff --git a/modules/book/book.test b/modules/book/book.test index 81f4524ac92..448dc23a9d8 100644 --- a/modules/book/book.test +++ b/modules/book/book.test @@ -101,7 +101,7 @@ class BookTestCase extends DrupalWebTestCase { // Check that book pages display along with the correct outlines and // previous/next links. - $this->checkBookNode($book, array($nodes[0], $nodes[3], $nodes[4]), FALSE, FALSE, $nodes[0], array()); + $this->checkBookNode($book, array($nodes[0], $nodes[3], $nodes[4]), FALSE, FALSE, $nodes[0]); $this->checkBookNode($nodes[0], array($nodes[1], $nodes[2]), $book, $book, $nodes[1], array($book)); $this->checkBookNode($nodes[1], NULL, $nodes[0], $nodes[0], $nodes[2], array($book, $nodes[0])); $this->checkBookNode($nodes[2], NULL, $nodes[1], $nodes[0], $nodes[3], array($book, $nodes[0])); @@ -124,7 +124,7 @@ class BookTestCase extends DrupalWebTestCase { // First we must set $this->book to the second book, so that the // correct regex will be generated for testing the outline. $this->book = $other_book; - $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node, array()); + $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node); $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, array($other_book)); } @@ -144,9 +144,9 @@ class BookTestCase extends DrupalWebTestCase { * @param $next * (optional) Next link node. Defaults to FALSE. * @param $breadcrumb - * The nodes that should be displayed in the breadcrumb. + * (optional) The nodes that should be displayed in the breadcrumb. */ - function checkBookNode($node, $nodes, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb) { + function checkBookNode($node, $nodes = NULL, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb = array()) { // $number does not use drupal_static as it should not be reset // since it uniquely identifies each call to checkBookNode(). static $number = 0; diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/modules/field/modules/field_sql_storage/field_sql_storage.module index 842893ad7df..deb08d0dac6 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -434,6 +434,81 @@ function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fi } } +/** + * Callback for array_filter(). + */ +function _field_sql_storage_write_compare_filter_callback($value) { + return NULL !== $value && '' !== $value; +} + +/** + * Cleanup field values for later values comparison. + * + * @param array $field + * Field info as returned by field_info_field_by_id(). + * + * @param array $array + * Field values to cleanup. + * + * @return array + * Filtered values. + */ +function _field_sql_storage_write_compare_filter($field, $array) { + foreach ($array as $language => $items) { + if (empty($items)) { + unset($array[$language]); + } + else { + foreach ($items as $delta => $item) { + // This should not happen but some modules provide invalid data to the + // field API. + if (!is_array($item)) { + continue; + } + // Let's start by pruning empty values and non storable values. + $array[$language][$delta] = array_filter(array_intersect_key($item, $field['columns']), '_field_sql_storage_write_compare_filter_callback'); + // Ordering is important because for widget elements and loaded columns + // from database order might differ and give false positives on field + // value change, especially with complex fields such as image fields. + ksort($array[$language][$delta]); + } + } + } + return $array; +} + +/** + * Compare a single field value for both entities and tell us if it changed. + * + * @param array $field + * Loaded field structure. + * @param object $entity1 + * First entity to compare. + * @param object $entity2 + * Second entity to compare. + * + * @return bool + * True if field value changed, false otherwise. + */ +function _field_sql_storage_write_compare($field, $entity1, $entity2) { + $field_name = $field['field_name']; + if (empty($entity1->$field_name) && empty($entity2->$field_name)) { + // Both are empty we can safely assume that it did not change. + return FALSE; + } + if (!isset($entity1->$field_name) || !isset($entity2->$field_name)) { + // One of them is missing but not the other the value changed. + return TRUE; + } + // We need to proceed to deep array comparison, but we cannot do it naively: + // in most cases the field values come from the edit form, and some Form API + // widget values that are not field columns may be present. We need to clean + // up both original and new field values before comparison. + $items1 = _field_sql_storage_write_compare_filter($field, (array) $entity1->$field_name); + $items2 = _field_sql_storage_write_compare_filter($field, (array) $entity2->$field_name); + return $items1 != $items2; +} + /** * Implements hook_field_storage_write(). */ @@ -443,8 +518,29 @@ function field_sql_storage_field_storage_write($entity_type, $entity, $op, $fiel $vid = $id; } + // Check if the given entity is a new revision or not. In case of a new + // revision creation, we cannot skip any field. + if (!empty($vid) && !empty($entity->original)) { + list(, $original_vid) = entity_extract_ids($entity_type, $entity->original); + if (NULL === $original_vid) { + $original_vid = $id; + } + $is_new_revision = $original_vid != $vid; + } + else { + $is_new_revision = FALSE; + } + + // Allow this optimization to be optional. + $skip_unchanged_fields = variable_get('field_sql_storage_skip_writing_unchanged_fields', FALSE); + foreach ($fields as $field_id) { $field = field_info_field_by_id($field_id); + + if ($skip_unchanged_fields && !$is_new_revision && !empty($entity->original) && !_field_sql_storage_write_compare($field, $entity, $entity->original)) { + continue; + } + $field_name = $field['field_name']; $table_name = _field_sql_storage_tablename($field); $revision_name = _field_sql_storage_revision_tablename($field); diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.test b/modules/field/modules/field_sql_storage/field_sql_storage.test index b2eb5065234..e46677be9c2 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.test +++ b/modules/field/modules/field_sql_storage/field_sql_storage.test @@ -281,6 +281,69 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase { $this->assertEqual($count, 1, 'NULL field translation is wiped.'); } + /** + * Tests the expected return values of _field_sql_storage_write_compare(). + */ + public function testFieldCompareDataModification() { + $langcode = LANGUAGE_NONE; + $field_info = field_info_field($this->field_name); + + // Make sure we have 2 sample field values that are unique. + $value1 = 0; + $value2 = 0; + while ($value1 == $value2) { + $value1 = mt_rand(); + $value2 = (string) mt_rand(); + } + + // Create the 2 entities to compare. + $entity = field_test_create_stub_entity(); + $entity->{$this->field_name}[$langcode][]['value'] = $value1; + $entity1 = clone $entity; + $entity2 = clone $entity; + + // Make sure that it correctly compares identical entities. + $this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The entities are identical.'); + + // Compare to an empty object. + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, new stdClass()), 'The entity is not the same as an empty object.'); + + // Change one of the values. + $entity2->{$this->field_name}[$langcode][0]['value'] = $value2; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The values are not the same.'); + + // Reset $entity2. + $entity2 = clone $entity; + + // Duplicate the value on one of the entities. + $entity1->{$this->field_name}[$langcode][]['value'] = $value1; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The fields do not have the same number of values.'); + + // Add a second value to both entities. + $entity2->{$this->field_name}[$langcode][]['value'] = $value2; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The values are not the same.'); + + // Replace the array containing the value with the actual value. + $entity2->{$this->field_name}[$langcode] = $entity2->{$this->field_name}[$langcode][0]; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The array to hold field values is replaced by the value.'); + + // Null one value. + $entity2->{$this->field_name}[$langcode] = NULL; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'One field is NULL and the other is not.'); + + // Null both values. + $entity1->{$this->field_name}[$langcode] = NULL; + $this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'Both fields are NULL.'); + + // Unset one of the fields. + unset($entity2->{$this->field_name}); + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'One field structure is unset.'); + + // Unset both of the fields. + unset($entity1->{$this->field_name}); + $this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'Both field structures are unset.'); + } + /** * Test trying to update a field with data. */ diff --git a/modules/field/tests/field_test.storage.inc b/modules/field/tests/field_test.storage.inc index 03eae4a6f63..7a6201d319f 100644 --- a/modules/field/tests/field_test.storage.inc +++ b/modules/field/tests/field_test.storage.inc @@ -240,111 +240,6 @@ function field_test_field_storage_delete_revision($entity_type, $entity, $fields _field_test_storage_data($data); } -/** - * Implements hook_field_storage_query(). - */ -function field_test_field_storage_query($field_id, $conditions, $count, &$cursor = NULL, $age) { - $data = _field_test_storage_data(); - - $load_current = $age == FIELD_LOAD_CURRENT; - - $field = field_info_field_by_id($field_id); - $field_columns = array_keys($field['columns']); - - $field_data = $data[$field['id']]; - $sub_table = $load_current ? 'current' : 'revisions'; - // We need to sort records by entity type and entity id. - usort($field_data[$sub_table], '_field_test_field_storage_query_sort_helper'); - - // Initialize results array. - $return = array(); - $entity_count = 0; - $rows_count = 0; - $rows_total = count($field_data[$sub_table]); - $skip = $cursor; - $skipped = 0; - - foreach ($field_data[$sub_table] as $row) { - if ($count != FIELD_QUERY_NO_LIMIT && $entity_count >= $count) { - break; - } - - if ($row->field_id == $field['id']) { - $match = TRUE; - $condition_deleted = FALSE; - // Add conditions. - foreach ($conditions as $condition) { - @list($column, $value, $operator) = $condition; - if (empty($operator)) { - $operator = is_array($value) ? 'IN' : '='; - } - switch ($operator) { - case '=': - $match = $match && $row->{$column} == $value; - break; - case '<>': - case '<': - case '<=': - case '>': - case '>=': - eval('$match = $match && ' . $row->{$column} . ' ' . $operator . ' '. $value); - break; - case 'IN': - $match = $match && in_array($row->{$column}, $value); - break; - case 'NOT IN': - $match = $match && !in_array($row->{$column}, $value); - break; - case 'BETWEEN': - $match = $match && $row->{$column} >= $value[0] && $row->{$column} <= $value[1]; - break; - case 'STARTS_WITH': - case 'ENDS_WITH': - case 'CONTAINS': - // Not supported. - $match = FALSE; - break; - } - // Track condition on 'deleted'. - if ($column == 'deleted') { - $condition_deleted = TRUE; - } - } - - // Exclude deleted data unless we have a condition on it. - if (!$condition_deleted && $row->deleted) { - $match = FALSE; - } - - if ($match) { - if (!isset($skip) || $skipped >= $skip) { - $cursor++; - // If querying all revisions and the entity type has revisions, we need - // to key the results by revision_ids. - $entity_type = entity_get_info($row->type); - $id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id; - - if (!isset($return[$row->type][$id])) { - $return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle)); - $entity_count++; - } - } - else { - $skipped++; - } - } - } - $rows_count++; - - // The query is complete if we walked the whole array. - if ($count != FIELD_QUERY_NO_LIMIT && $rows_count >= $rows_total) { - $cursor = FIELD_QUERY_COMPLETE; - } - } - - return $return; -} - /** * Sort helper for field_test_field_storage_query(). * diff --git a/modules/file/file.field.inc b/modules/file/file.field.inc index fc1a1df200d..ddb4f841fde 100644 --- a/modules/file/file.field.inc +++ b/modules/file/file.field.inc @@ -593,7 +593,7 @@ function file_field_widget_uri($field, $instance, $data = array()) { /** * The #value_callback for the file_generic field element. */ -function file_field_widget_value($element, $input = FALSE, $form_state) { +function file_field_widget_value($element, $input = FALSE, $form_state = array()) { if ($input) { // Checkboxes lose their value when empty. // If the display field is present make sure its unchecked value is saved. @@ -955,17 +955,14 @@ function theme_file_upload_help($variables) { if (isset($upload_validators['file_validate_image_resolution'])) { $max = $upload_validators['file_validate_image_resolution'][0]; $min = $upload_validators['file_validate_image_resolution'][1]; - if ($min && $max && $min == $max) { - $descriptions[] = t('Images must be exactly !size pixels.', array('!size' => '' . $max . '')); - } - elseif ($min && $max) { - $descriptions[] = t('Images must be between !min and !max pixels.', array('!min' => '' . $min . '', '!max' => '' . $max . '')); + if ($min && $max) { + $descriptions[] = t('Images must be at least !min pixels. Images larger than !max pixels will be resized.', array('!min' => '' . $min . '', '!max' => '' . $max . '')); } elseif ($min) { - $descriptions[] = t('Images must be larger than !min pixels.', array('!min' => '' . $min . '')); + $descriptions[] = t('Images must be at least !min pixels.', array('!min' => '' . $min . '')); } elseif ($max) { - $descriptions[] = t('Images must be smaller than !max pixels.', array('!max' => '' . $max . '')); + $descriptions[] = t('Images larger than !max pixels will be resized.', array('!max' => '' . $max . '')); } } diff --git a/modules/image/image.test b/modules/image/image.test index 0c26ffa84ed..22edcaa0647 100644 --- a/modules/image/image.test +++ b/modules/image/image.test @@ -1022,7 +1022,7 @@ class ImageFieldDisplayTestCase extends ImageFieldTestCase { $this->drupalGet('node/add/article'); $this->assertText(t('Files must be less than 50 KB.'), 'Image widget max file size is displayed on article form.'); $this->assertText(t('Allowed file types: ' . $test_image_extension . '.'), 'Image widget allowed file types displayed on article form.'); - $this->assertText(t('Images must be between 10x10 and 100x100 pixels.'), 'Image widget allowed resolution displayed on article form.'); + $this->assertText(t('Images must be at least 10x10 pixels. Images larger than 100x100 pixels will be resized.'), 'Image widget allowed resolution displayed on article form.'); // We have to create the article first and then edit it because the alt // and title fields do not display until the image has been attached. diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc index a1da1d0b57c..4ca74716456 100644 --- a/modules/openid/openid.inc +++ b/modules/openid/openid.inc @@ -142,7 +142,8 @@ function _openid_xrds_parse($raw_xml) { // For PHP version >= 5.2.11, we can use this function to protect against // malicious doctype declarations and other unexpected entity loading. // However, we will not rely on it, and reject any XML with a DOCTYPE. - $disable_entity_loader = function_exists('libxml_disable_entity_loader'); + // libxml_disable_entity_loader() is deprecated in PHP >= 8.0. + $disable_entity_loader = function_exists('libxml_disable_entity_loader') && PHP_VERSION_ID < 80000; if ($disable_entity_loader) { $load_entities = libxml_disable_entity_loader(TRUE); } diff --git a/modules/openid/openid.module b/modules/openid/openid.module index a52dbc3de25..f2c1b8d549f 100644 --- a/modules/openid/openid.module +++ b/modules/openid/openid.module @@ -743,7 +743,7 @@ function openid_association_request($public) { return $request; } -function openid_authentication_request($claimed_id, $identity, $return_to = '', $assoc_handle = '', $service) { +function openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service) { global $base_url; module_load_include('inc', 'openid'); diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index c426ba53ab2..f212b0eb534 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1690,8 +1690,14 @@ protected function tearDown() { $this->fail('Failed to drop all prefixed tables.'); } + // In PHP 8 some tests encounter problems when shutdown code tries to + // access the database connection after it's been explicitly closed, for + // example the destructor of DrupalCacheArray. We avoid this by not fully + // destroying the test database connection. + $close = \PHP_VERSION_ID < 80000; + // Get back to the original connection. - Database::removeConnection('default'); + Database::removeConnection('default', $close); Database::renameConnection('simpletest_original_default', 'default'); // Restore original shutdown callbacks array to prevent original @@ -3084,7 +3090,7 @@ protected function assertNoText($text, $message = '', $group = 'Other') { * @return * TRUE on pass, FALSE on fail. */ - protected function assertTextHelper($text, $message = '', $group, $not_exists) { + protected function assertTextHelper($text, $message, $group, $not_exists) { if ($this->plainTextContent === FALSE) { $this->plainTextContent = filter_xss($this->drupalGetContent(), array()); } @@ -3150,7 +3156,7 @@ protected function assertNoUniqueText($text, $message = '', $group = 'Other') { * @return * TRUE on pass, FALSE on fail. */ - protected function assertUniqueTextHelper($text, $message = '', $group, $be_unique) { + protected function assertUniqueTextHelper($text, $message, $group, $be_unique) { if ($this->plainTextContent === FALSE) { $this->plainTextContent = filter_xss($this->drupalGetContent(), array()); } @@ -3256,7 +3262,7 @@ protected function assertNoTitle($title, $message = '', $group = 'Other') { * @param $callback * The name of the theme function to invoke; e.g. 'links' for theme_links(). * @param $variables - * (optional) An array of variables to pass to the theme function. + * An array of variables to pass to the theme function. * @param $expected * The expected themed output string. * @param $message @@ -3272,7 +3278,7 @@ protected function assertNoTitle($title, $message = '', $group = 'Other') { * @return * TRUE on pass, FALSE on fail. */ - protected function assertThemeOutput($callback, array $variables = array(), $expected, $message = '', $group = 'Other') { + protected function assertThemeOutput($callback, array $variables, $expected, $message = '', $group = 'Other') { $output = theme($callback, $variables); $this->verbose('Variables:' . '
' .  check_plain(var_export($variables, TRUE)) . '
' . '
' . 'Result:' . '
' .  check_plain(var_export($output, TRUE)) . '
' diff --git a/modules/simpletest/simpletest.test b/modules/simpletest/simpletest.test index 5d1c718c1c4..80b841d58fc 100644 --- a/modules/simpletest/simpletest.test +++ b/modules/simpletest/simpletest.test @@ -164,13 +164,16 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase { $this->pass(t('Test ID is @id.', array('@id' => $this->testId))); // Generates a warning. - $i = 1 / 0; + $a = ''; + foreach ($a as $b) { + + } // Call an assert function specific to that class. $this->assertNothing(); - // Generates a warning inside a PHP function. - array_key_exists(NULL, NULL); + // Generates 3 warnings inside a PHP function. + simplexml_load_string(''); debug('Foo', 'Debug'); } @@ -195,19 +198,21 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase { $this->assertAssertion(t('Invalid permission %permission.', array('%permission' => $this->invalid_permission)), 'Role', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); // Check that a warning is caught by simpletest. - $this->assertAssertion('Division by zero', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); + // The exact error message differs between PHP versions so we check only + // the presense of the 'foreach' statement. + $this->assertAssertion('foreach()', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); // Check that the backtracing code works for specific assert function. $this->assertAssertion('This is nothing.', 'Other', 'Pass', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); // Check that errors that occur inside PHP internal functions are correctly reported. // The exact error message differs between PHP versions so we check only - // the function name 'array_key_exists'. - $this->assertAssertion('array_key_exists', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); + // the function name 'simplexml_load_string'. + $this->assertAssertion('simplexml_load_string', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); $this->assertAssertion("Debug: 'Foo'", 'Debug', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); - $this->assertEqual('6 passes, 5 fails, 2 exceptions, and 1 debug message', $this->childTestResults['summary'], 'Stub test summary is correct'); + $this->assertEqual('6 passes, 5 fails, 4 exceptions, and 1 debug message', $this->childTestResults['summary'], 'Stub test summary is correct'); $this->test_ids[] = $test_id = $this->getTestIdFromResults(); $this->assertTrue($test_id, 'Found test ID in results.'); diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index effd04bf2f4..61caf53caeb 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -453,13 +453,13 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { /** * Skips handling of "file not found" errors. */ - public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line) { // Skip error handling if this is a "file not found" error. if (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { $this->getFilenameTestTriggeredError = $message; return; } - _drupal_error_handler($error_level, $message, $filename, $line, $context); + _drupal_error_handler($error_level, $message, $filename, $line); } } @@ -569,13 +569,13 @@ class BootstrapGetFilenameWebTestCase extends DrupalWebTestCase { /** * Skips handling of "file not found" errors. */ - public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line) { // Skip error handling if this is a "file not found" error. if (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { $this->getFilenameTestTriggeredError = $message; return; } - _drupal_error_handler($error_level, $message, $filename, $line, $context); + _drupal_error_handler($error_level, $message, $filename, $line); } /** diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index 6145546f754..ff399676305 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -2633,8 +2633,8 @@ class DrupalErrorCollectionUnitTest extends DrupalWebTestCase { $this->assertEqual(count($this->collectedErrors), 3, 'Three errors were collected'); if (count($this->collectedErrors) == 3) { - $this->assertError($this->collectedErrors[0], 'Notice', 'error_test_generate_warnings()', 'error_test.module', 'Undefined variable: bananas'); - $this->assertError($this->collectedErrors[1], 'Warning', 'error_test_generate_warnings()', 'error_test.module', 'Division by zero'); + $this->assertError($this->collectedErrors[0], 'Notice', 'error_test_generate_warnings()', 'error_test.module', 'Object of class stdClass could not be converted to int'); + $this->assertError($this->collectedErrors[1], 'Warning', 'error_test_generate_warnings()', 'error_test.module', \PHP_VERSION_ID < 80000 ? 'Invalid argument supplied for foreach()' : 'foreach() argument must be of type array|object, string given'); $this->assertError($this->collectedErrors[2], 'User warning', 'error_test_generate_warnings()', 'error_test.module', 'Drupal is awesome'); } else { diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index 04be5c85b84..0f0d2c32094 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -3755,21 +3755,36 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { $transaction = db_transaction(); $this->insertRow('row'); $this->executeDDLStatement(); - // Rollback the outer transaction. + + set_error_handler(array($this, 'rollBackWithoutTransactionErrorHandler')); try { + // Rollback the outer transaction. $transaction->rollback(); - unset($transaction); - // @TODO: an exception should be triggered here, but is not, because - // "ROLLBACK" fails silently in MySQL if there is no transaction active. - // $this->fail(t('Rolling back a transaction containing DDL should fail.')); + // @see \DatabaseConnection_mysql::rollback() + if (PHP_VERSION_ID >= 80000) { + $this->fail('Rolling back a transaction containing DDL should produce a warning.'); + } } - catch (DatabaseTransactionNoActiveException $e) { - $this->pass('Rolling back a transaction containing DDL should fail.'); + catch (Exception $e) { + $this->assertEqual('Rollback attempted when there is no active transaction.', $e->getMessage()); } + restore_error_handler(); + unset($transaction); $this->assertRowPresent('row'); } } + /** + * Special handling of "rollback without transaction" errors. + */ + public function rollBackWithoutTransactionErrorHandler($error_level, $message, $filename, $line) { + // Throw an exception if this is a "rollback without transaction" error. + if (strpos($message, 'Rollback attempted when there is no active transaction.') !== FALSE ) { + throw new Exception('Rollback attempted when there is no active transaction.'); + } + _drupal_error_handler($error_level, $message, $filename, $line); + } + /** * Insert a single row into the testing table. */ diff --git a/modules/simpletest/tests/error.test b/modules/simpletest/tests/error.test index f946e82f1d2..5b56d52d9ba 100644 --- a/modules/simpletest/tests/error.test +++ b/modules/simpletest/tests/error.test @@ -22,13 +22,13 @@ class DrupalErrorHandlerTestCase extends DrupalWebTestCase { function testErrorHandler() { $error_notice = array( '%type' => 'Notice', - '!message' => 'Undefined variable: bananas', + '!message' => 'Object of class stdClass could not be converted to int', '%function' => 'error_test_generate_warnings()', '%file' => drupal_realpath('modules/simpletest/tests/error_test.module'), ); $error_warning = array( '%type' => 'Warning', - '!message' => 'Division by zero', + '!message' => \PHP_VERSION_ID < 80000 ? 'Invalid argument supplied for foreach()' : 'foreach() argument must be of type array|object, string given', '%function' => 'error_test_generate_warnings()', '%file' => drupal_realpath('modules/simpletest/tests/error_test.module'), ); @@ -113,4 +113,3 @@ class DrupalErrorHandlerTestCase extends DrupalWebTestCase { $this->assertNoRaw($message, format_string('Did not find error message: !message.', array('!message' => $message))); } } - diff --git a/modules/simpletest/tests/error_test.module b/modules/simpletest/tests/error_test.module index d062cb067c4..43d0d0c6187 100644 --- a/modules/simpletest/tests/error_test.module +++ b/modules/simpletest/tests/error_test.module @@ -40,9 +40,13 @@ function error_test_generate_warnings($collect_errors = FALSE) { // Tell Drupal error reporter to send errors to Simpletest or not. define('SIMPLETEST_COLLECT_ERRORS', $collect_errors); // This will generate a notice. - $monkey_love = $bananas; + $notice = new \stdClass(); + $notice == 1 ? 1 : 0; // This will generate a warning. - $awesomely_big = 1/0; + $a = ''; + foreach ($a as $b) { + + } // This will generate a user error. trigger_error("Drupal is awesome", E_USER_WARNING); return ""; diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index 49b561a63f4..23af5f4c7fc 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -240,14 +240,14 @@ class FormsTestCase extends DrupalWebTestCase { $values = drupal_json_decode($this->drupalPost(NULL, array('required_checkbox' => 1), t('Submit'))); $expected_values = array( 'disabled_checkbox_on' => 'disabled_checkbox_on', - 'disabled_checkbox_off' => '', + 'disabled_checkbox_off' => 0, 'checkbox_on' => 'checkbox_on', - 'checkbox_off' => '', + 'checkbox_off' => 0, 'zero_checkbox_on' => '0', - 'zero_checkbox_off' => '', + 'zero_checkbox_off' => 0, ); foreach ($expected_values as $widget => $expected_value) { - $this->assertEqual($values[$widget], $expected_value, format_string('Checkbox %widget returns expected value (expected: %expected, got: %value)', array( + $this->assertIdentical($values[$widget], $expected_value, format_string('Checkbox %widget returns expected value (expected: %expected, got: %value)', array( '%widget' => var_export($widget, TRUE), '%expected' => var_export($expected_value, TRUE), '%value' => var_export($values[$widget], TRUE), diff --git a/modules/simpletest/tests/session.test b/modules/simpletest/tests/session.test index 893d03e9f01..5632d44e4d6 100644 --- a/modules/simpletest/tests/session.test +++ b/modules/simpletest/tests/session.test @@ -244,6 +244,187 @@ class SessionTestCase extends DrupalWebTestCase { $this->assertResponse(403, 'An empty session ID is not allowed.'); } + /** + * Test absence of SameSite attribute on session cookies by default. + */ + function testNoSameSiteCookieAttributeDefault() { + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + if (\PHP_VERSION_ID < 70300) { + $this->drupalLogin($user); + } + else { + // PHP often defaults to an empty value for session.cookie_samesite but + // that may vary, so we set an explicit empty value. + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: *EMPTY*'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + } + $this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (default).'); + } + + /** + * Test SameSite attribute = None by default on Secure session cookies. + */ + function testSameSiteCookieAttributeNoneSecure() { + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $headers = array(); + if (\PHP_VERSION_ID >= 70300) { + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: None'; + } + // Test HTTPS session handling by altering the form action to submit the + // login form through https.php, which creates a mock HTTPS request. + $this->drupalGet('user'); + $form = $this->xpath('//form[@id="user-login"]'); + $form[0]['action'] = $this->httpsUrl('user'); + $edit = array('name' => $user->name, 'pass' => $user->pass_raw); + $this->drupalPost(NULL, $edit, t('Log in'), array(), $headers); + $this->assertTrue(preg_match('/SameSite=None/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=None.'); + } + + /** + * Test SameSite attribute = None on session cookies. + */ + function testSameSiteCookieAttributeNone() { + variable_set('samesite_cookie_value', 'None'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->assertTrue(preg_match('/SameSite=None/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=None.'); + } + + /** + * Test SameSite attribute = Lax on session cookies. + */ + function testSameSiteCookieAttributeLax() { + variable_set('samesite_cookie_value', 'Lax'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Lax.'); + } + + /** + * Test SameSite attribute = Strict on session cookies. + */ + function testSameSiteCookieAttributeStrict() { + variable_set('samesite_cookie_value', 'Strict'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->assertTrue(preg_match('/SameSite=Strict/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Strict.'); + } + + /** + * Test disabling the samesite attribute on session cookies via $conf + */ + function testSameSiteCookieAttributeDisabledViaConf() { + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + variable_set('samesite_cookie_value', FALSE); + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->drupalLogin($user); + } + else { + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: Lax'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + } + $this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (conf).'); + } + + /** + * Test disabling the samesite attribute on session cookies via php ini + */ + function testSameSiteCookieAttributeDisabledViaPhpIni() { + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->pass('This test is only for PHP 7.3 and later.'); + return; + } + $user = $this->drupalCreateUser(array('access content')); + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: *EMPTY*'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + $this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (ini).'); + } + + /** + * Test that a PHP setting for session.cookie_samesite is not overridden by + * the default value in Drupal, without a samesite_cookie_value variable. + */ + function testSamesiteCookiePhpSettingLax() { + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->pass('This test is only for PHP 7.3 and later.'); + return; + } + $user = $this->drupalCreateUser(array('access content')); + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: Lax'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + $this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Lax.'); + } + + /** + * Test overriding the PHP setting for session.cookie_samesite with the + * samesite_cookie_value variable. + */ + function testSamesiteCookieOverrideLaxToStrict() { + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->pass('This test is only for PHP 7.3 and later.'); + return; + } + variable_set('samesite_cookie_value', 'Strict'); + $user = $this->drupalCreateUser(array('access content')); + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: Lax'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + $this->assertTrue(preg_match('/SameSite=Strict/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Strict.'); + } + + /** + * Test SameSite attribute = Lax on set-cookie header on logout. + */ + function testSamesiteCookieLogoutLax() { + variable_set('samesite_cookie_value', 'Lax'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->drupalGet('user/logout'); + $this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie deletion includes SameSite=Lax.'); + } + /** * Reset the cookie file so that it refers to the specified user. * @@ -285,6 +466,20 @@ class SessionTestCase extends DrupalWebTestCase { $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', 'Session was not empty.'); } } + + /** + * Builds a URL for submitting a mock HTTPS request to HTTP test environments. + * + * @param $url + * A Drupal path such as 'user'. + * + * @return + * An absolute URL. + */ + protected function httpsUrl($url) { + global $base_url; + return $base_url . '/modules/simpletest/tests/https.php?q=' . $url; + } } /** diff --git a/modules/simpletest/tests/session_test.module b/modules/simpletest/tests/session_test.module index 689ff099ae0..b2d31079b78 100644 --- a/modules/simpletest/tests/session_test.module +++ b/modules/simpletest/tests/session_test.module @@ -64,6 +64,19 @@ function session_test_menu() { return $items; } +/** + * It's very unusual to do anything outside of a function / hook, but in this + * case we want to simulate a given session.cookie_samesite setting in php.ini + * or via ini_set() in settings.php. This would almost never be a good idea + * outside of a test scenario. + */ +if (isset($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'])) { + if (in_array($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'], array('None', 'Lax', 'Strict', '*EMPTY*'))) { + $value = ($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'] == '*EMPTY*') ? '' : $_SERVER['HTTP_X_SESSION_COOKIE_INI_SET']; + ini_set('session.cookie_samesite', $value); + } +} + /** * Implements hook_boot(). */ diff --git a/modules/simpletest/tests/upgrade/upgrade.locale.test b/modules/simpletest/tests/upgrade/upgrade.locale.test index 2a3056b0676..f7b0038d641 100644 --- a/modules/simpletest/tests/upgrade/upgrade.locale.test +++ b/modules/simpletest/tests/upgrade/upgrade.locale.test @@ -124,7 +124,7 @@ class LocaleUpgradePathTestCase extends UpgradePathTestCase { /** * Asserts that a page exists and is in the specified language. */ - public function assertPageInLanguage($path = NULL, $langcode) { + public function assertPageInLanguage($path, $langcode) { if (isset($path)) { $this->drupalGet($path); } diff --git a/modules/system/system.tar.inc b/modules/system/system.tar.inc index 0af6275b401..7a0719e5850 100644 --- a/modules/system/system.tar.inc +++ b/modules/system/system.tar.inc @@ -280,7 +280,7 @@ class Archive_Tar { $this->_close(); // ----- Look for a local copy to delete - if ($this->_temp_tarname != '') { + if ($this->_temp_tarname != '' && (bool) preg_match('/^tar[[:alnum:]]*\.tmp$/', $this->_temp_tarname)) { @drupal_unlink($this->_temp_tarname); } } diff --git a/modules/user/tests/user_form_test.module b/modules/user/tests/user_form_test.module index 2af15cb83b6..4379bd1a55f 100644 --- a/modules/user/tests/user_form_test.module +++ b/modules/user/tests/user_form_test.module @@ -80,3 +80,42 @@ function user_form_test_user_account_submit($form, &$form_state) { // test for bugs that can be triggered in contributed modules. $form_state['rebuild'] = TRUE; } + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function user_form_test_form_user_pass_reset_alter(&$form, &$form_state) { + // An unaltered form has no form values; the uid/timestmap/"confirm" are in + // the URL arguments. (If for some reason a form_alter method needs the + // hash, it can be retrieved from $form['#action'].) + if (!is_numeric(arg(2)) || !is_numeric(arg(3)) || !is_string(arg(4)) || arg(4) !== 'confirm') { + throw new Exception("Something unexpected changed in the user_pass_reset form; we are not getting the UID/timestamp/'confirm' passed anymore."); + } + // Use the variable system to communicate to the test code, since we don't + // share a session with it. + variable_set('user_test_pass_reset_form_build_' . arg(2), TRUE); + + $form['#submit'][] = 'user_form_test_form_user_pass_reset_submit'; + // We must cache the form to ensure the form builder (user_pass_reset()) is + // skipped when processing the submitted form, otherwise we get redirected + // already during form build. + $form_state['cache'] = TRUE; +} + +/** + * Submit function for user_pass_reset(). + */ +function user_form_test_form_user_pass_reset_submit($form, &$form_state) { + // On submit, the hash is in arg(4). + if (!is_numeric(arg(2)) || !is_numeric(arg(3)) || !is_string(arg(4)) || strlen(arg(4)) < 32) { + throw new Exception("Something unexpected changed in the user_pass_reset form; we are not getting the UID/timestamp/hash passed anymore."); + } + variable_set('user_test_pass_reset_form_submit_' . arg(2), TRUE); + // Because the form does no further processing and has no redirect set, + // drupal_redirect_form() will redirect back to ourselves + // (user/reset/UID/TIMESTAMP/HASH/login); we will be logged in and redirected + // while the form is built again. FYI: we cannot set $form_state['rebuild'] + // to get around the first redirect without further hacks, because then the + // form won't pass the hash. (Our original $form_state['build_info']['args'] + // contains "confirm" for the 3rd argument.) +} diff --git a/modules/user/user.admin.inc b/modules/user/user.admin.inc index 6ca330b013c..0db7efb1a1c 100644 --- a/modules/user/user.admin.inc +++ b/modules/user/user.admin.inc @@ -61,7 +61,7 @@ function user_filter_form() { if ($type == 'permission') { // Merge arrays of module permissions into one. // Slice past the first element '[any]' whose value is not an array. - $options = call_user_func_array('array_merge', array_slice($filters[$type]['options'], 1)); + $options = call_user_func_array('array_merge', array_values(array_slice($filters[$type]['options'], 1))); $value = $options[$value]; } else { @@ -1050,4 +1050,3 @@ function user_admin_role_delete_confirm_submit($form, &$form_state) { drupal_set_message(t('The role has been deleted.')); $form_state['redirect'] = 'admin/people/permissions/roles'; } - diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc index 6f997a62ea3..937f7a31e44 100644 --- a/modules/user/user.pages.inc +++ b/modules/user/user.pages.inc @@ -134,10 +134,25 @@ function user_pass_submit($form, &$form_state) { /** * Menu callback; process one time login link and redirects to the user page on success. + * + * In order to never disclose password reset hashes via referrer headers or + * web browser history, this function always issues a redirect when a valid + * password reset hash is in the URL. */ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $action = NULL) { global $user; + // Check for a reset hash in the session. Immediately remove it (to prevent it + // from being reused, for example if this page is visited again via the + // browser history) and store it for later use. + if (isset($_SESSION['pass_reset_hash'])) { + $session_reset_hash = $_SESSION['pass_reset_hash']; + unset($_SESSION['pass_reset_hash']); + } + else { + $session_reset_hash = NULL; + } + // When processing the one-time login link, we have to make sure that a user // isn't already logged in. if ($user->uid) { @@ -182,7 +197,36 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error'); drupal_goto('user/password'); } - elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { + // Validate the reset hash from the URL or from the session. + $is_valid = FALSE; + if ($account->uid && $timestamp >= $account->login && $timestamp <= $current) { + // If the reset hash in the URL is valid, put it in the session and + // redirect to the same URL but with the hash replaced by an invalid + // one ("confirm"). This prevents disclosing the hash via referrer + // headers or web browser history. + if ($hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { + if ($action === 'login') { + // The 'login' action redirects directly to the user edit form. + $is_valid = TRUE; + } + else { + $_SESSION['pass_reset_hash'] = $hashed_pass; + $args = arg(); + foreach ($args as &$arg) { + if ($arg == $hashed_pass) { + $arg = 'confirm'; + } + } + $path = implode('/', $args); + drupal_goto($path, array('query' => drupal_get_query_parameters())); + } + } + // If the reset hash from the session is valid, use that. + elseif ($session_reset_hash && $session_reset_hash == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { + $is_valid = TRUE; + } + } + if ($is_valid) { // First stage is a confirmation form, then login if ($action == 'login') { // Set the new user. @@ -204,7 +248,11 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a $form['help'] = array('#markup' => '

' . t('This login can be used only once.') . '

'); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in')); - $form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login"); + $form['#action'] = url("user/reset/$uid/$timestamp/$session_reset_hash/login"); + // Prevent the browser from storing this page so that the token will + // not be visible in the form action if the back button is used to + // revisit this page. + drupal_add_http_header('Cache-Control', 'no-store'); return $form; } } diff --git a/modules/user/user.test b/modules/user/user.test index 4c16b531c3c..70556bff8ee 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -480,6 +480,10 @@ class UserLoginTestCase extends DrupalWebTestCase { class UserPasswordResetTestCase extends DrupalWebTestCase { protected $profile = 'standard'; + function setUp() { + parent::setUp('user_form_test'); + } + public static function getInfo() { return array( 'name' => 'Reset password', @@ -491,20 +495,38 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { /** * Retrieves password reset email and extracts the login link. */ - public function getResetURL() { + public function getResetURL($bypass_form = FALSE) { // Assume the most recent email. $_emails = $this->drupalGetMails(); $email = end($_emails); $urls = array(); preg_match('#.+user/reset/.+#', $email['body'], $urls); - return $urls[0]; + return $urls[0] . ($bypass_form ? '/login' : ''); + } + + /** + * Generates login link. + */ + public function generateResetURL($account, $bypass_form = FALSE) { + return user_pass_reset_url($account) . ($bypass_form ? '/login' : ''); + } + + /** + * Turns a password reset URL into a 'confirm' URL. + */ + public function getConfirmURL($reset_url) { + // Last part is always the hash; replace with "confirm". + $parts = explode('/', $reset_url); + array_pop($parts); + array_push($parts, 'confirm'); + return implode('/', $parts); } /** * Tests password reset functionality. */ - function testUserPasswordReset() { + function testUserPasswordReset($use_direct_login_link = FALSE) { // Create a user. $account = $this->drupalCreateUser(); $this->drupalLogin($account); @@ -540,11 +562,19 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { ); field_create_instance($instance); - $resetURL = $this->getResetURL(); + variable_del("user_test_pass_reset_form_submit_{$account->uid}"); + $resetURL = $this->getResetURL($use_direct_login_link); $this->drupalGet($resetURL); // Check successful login. - $this->drupalPost(NULL, NULL, t('Log in')); + if (!$use_direct_login_link) { + $this->assertUrl($this->getConfirmURL($resetURL), array(), 'The user is redirected to the reset password confirm form.'); + $this->drupalPost(NULL, NULL, t('Log in')); + // The form was fully processed before redirecting. + $form_submit_handled = variable_get("user_test_pass_reset_form_submit_{$account->uid}", FALSE); + $this->assertTrue($form_submit_handled, 'A custom submit handler executed.'); + } + $this->assertText('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.'); // Make sure the Ajax request from uploading a file does not invalidate the // reset token. @@ -559,6 +589,94 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password); $this->drupalPost(NULL, $edit, t('Save')); $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.'); + + // Ensure blocked and deleted accounts can't access the direct login link. + $this->drupalLogout(); + $reset_url = $this->generateResetURL($account, $use_direct_login_link); + user_save($account, array('status' => 0)); + $this->drupalGet($reset_url); + $this->assertResponse(403); + user_delete($account->uid); + $this->drupalGet($reset_url); + $this->assertResponse(403); + } + + /** + * Test user-based flood control on password reset. + */ + function testPasswordResetFloodControlPerUser() { + // Set a very low limit for testing. + variable_set('user_pass_reset_user_limit', 2); + + // Create a user. + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + $this->drupalLogout(); + + $edit = array('name' => $account->name); + + // Try 2 requests that should not trigger flood control. + for ($i = 0; $i < 2; $i++) { + $this->drupalPost('user/password', $edit, t('E-mail new password')); + // Confirm the password reset. + $this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.'); + // Ensure that flood control was not triggered. + $this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.'); + } + + // A successful password reset should clear flood events. + $resetURL = $this->getResetURL(); + $this->drupalGet($resetURL); + + // Check successful login. + $this->drupalPost(NULL, NULL, t('Log in')); + $this->drupalLogout(); + + // Try 2 requests that should not trigger flood control. + for ($i = 0; $i < 2; $i++) { + $this->drupalPost('user/password', $edit, t('E-mail new password')); + // Confirm the password reset. + $this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.'); + // Ensure that flood control was not triggered. + $this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.'); + } + + // The next request should trigger flood control + $this->drupalPost('user/password', $edit, t('E-mail new password')); + // Confirm the password reset was blocked. + $this->assertNoText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message not displayed for excessive password resets.'); + // Ensure that flood control was triggered. + $this->assertText(t('Sorry, there have been more than 2 password reset attempts for this account. It is temporarily blocked.'), 'Flood control was triggered by excessive password resets for one user.'); + } + + /** + * Test IP-based flood control on password reset. + */ + function testPasswordResetFloodControlPerIp() { + // Set a very low limit for testing. + variable_set('user_pass_reset_ip_limit', 2); + + // Try 2 requests that should not trigger flood control. + for ($i = 0; $i < 2; $i++) { + $name = $this->randomName(); + $edit = array('name' => $name); + $this->drupalPost('user/password', $edit, t('E-mail new password')); + // Confirm the password reset was not blocked. Note that @name is used + // instead of %name as assertText() works with plain text not HTML. + $this->assertText(t('Sorry, @name is not recognized as a user name or an e-mail address.', array('@name' => $name)), 'User name not recognized message displayed.'); + // Ensure that flood control was not triggered. + $this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.'); + } + + // The next request should trigger flood control + $name = $this->randomName(); + $edit = array('name' => $name); + $this->drupalPost('user/password', $edit, t('E-mail new password')); + // Confirm the password reset was blocked early. Note that @name is used + // instead of %name as assertText() works with plain text not HTML. + $this->assertNoText(t('Sorry, @name is not recognized as a user name or an e-mail address.', array('@name' => $name)), 'User name not recognized message not displayed.'); + // Ensure that flood control was triggered. + $this->assertText(t('Sorry, too many password reset attempts from your IP address. This IP address is temporarily blocked.'), 'Flood control was triggered by excessive password resets from one IP.'); } /** @@ -642,21 +760,51 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { /** * Test user password reset while logged in. */ - function testUserPasswordResetLoggedIn() { + function testUserPasswordResetLoggedIn($use_direct_login_link = FALSE) { + $another_account = $this->drupalCreateUser(); $account = $this->drupalCreateUser(); $this->drupalLogin($account); // Make sure the test account has a valid password. user_save($account, array('pass' => user_password())); + // Try to use the login link while logged in as a different user. // Generate one time login link. - $reset_url = user_pass_reset_url($account); + $reset_url = $this->generateResetURL($another_account, $use_direct_login_link); $this->drupalGet($reset_url); + $this->assertRaw(t( + 'Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please logout and try using the link again.', + array('%other_user' => $account->name, '%resetting_user' => $another_account->name, '!logout' => url('user/logout')) + )); - $this->assertText('Reset password'); - $this->drupalPost(NULL, NULL, t('Log in')); + // Test the link for a deleted user while logged in. + user_delete($another_account->uid); + $this->drupalGet($reset_url); + $this->assertText('The one-time login link you clicked is invalid.'); + // Generate a one time login link for the logged-in user. + $fapi_action = $use_direct_login_link ? 'build' : 'submit'; + variable_del("user_test_pass_reset_form_{$fapi_action}_{$account->uid}"); + $reset_url = $this->generateResetURL($account, $use_direct_login_link); + $this->drupalGet($reset_url); + if ($use_direct_login_link) { + // The form is never fully built; user is logged out (session destroyed) + // and redirected to the same URL, then logged in again and redirected + // during form build. + $form_built = variable_get("user_test_pass_reset_form_build_{$account->uid}", FALSE); + $this->assertTrue(!$form_built, 'The password reset form was never fully built.'); + } + else { + $this->assertUrl($this->getConfirmURL($reset_url), array(), 'The user is redirected to the reset password confirm form.'); + $this->assertText('Reset password'); + $this->drupalPost(NULL, NULL, t('Log in')); + // The form was fully processed before redirecting. + $form_submit_handled = variable_get("user_test_pass_reset_form_submit_{$account->uid}", FALSE); + $this->assertTrue($form_submit_handled, 'A custom submit handler executed.'); + } $this->assertText('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.'); + // The user can change the forgotten password on the page they are + // redirected to. $pass = user_password(); $edit = array( 'pass[pass1]' => $pass, @@ -667,6 +815,14 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertText('The changes have been saved.'); } + /** + * Test direct login link that bypasses the password reset form. + */ + function testUserDirectLogin() { + $this->testUserPasswordReset(TRUE); + $this->testUserPasswordResetLoggedIn(TRUE); + } + /** * Attempts login using an expired password reset link. */ @@ -770,7 +926,7 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE)); $this->drupalGet($reset_url); $this->assertText($user1->name, 'The valid password reset page shows the user name.'); - $this->assertUrl($reset_url, array(), 'The user remains on the password reset login page.'); + $this->assertUrl($this->getConfirmURL($reset_url), array(), 'The user is redirected to the reset password confirm form.'); $this->assertNoText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); } diff --git a/profiles/dkan/.probo.yml b/profiles/dkan/.probo.yml index e1d521f422b..a76edb96c20 100644 --- a/profiles/dkan/.probo.yml +++ b/profiles/dkan/.probo.yml @@ -1,4 +1,4 @@ -image: proboci/ubuntu-16.04-lamp:php-7.2 +image: proboci/ubuntu:18.04-php7.3 steps: - name: Setup QA site plugin: Script @@ -10,6 +10,11 @@ steps: apt-get update -y + echo "Use drush 8" + ln -s /usr/local/src/drush8/vendor/bin/drush /usr/local/bin/drush + echo "Use composer 1" + composer self-update --1 + echo "Install DKAN Tools" cd $SRC_DIR git clone https://github.com/GetDKAN/dkan-tools.git diff --git a/profiles/dkan/CHANGELOG.txt b/profiles/dkan/CHANGELOG.txt index 5cc90baf3d1..c85a57504db 100644 --- a/profiles/dkan/CHANGELOG.txt +++ b/profiles/dkan/CHANGELOG.txt @@ -1,3 +1,6 @@ +7.x-1.18.16 +- #3442 Drupal security upgrade to version 7.80. + 7.x-1.18.15 - #3318 Drupal security upgrade to version 7.78. diff --git a/profiles/dkan/README.md b/profiles/dkan/README.md index 4adffc7acb6..aa097c1a5a3 100644 --- a/profiles/dkan/README.md +++ b/profiles/dkan/README.md @@ -1,4 +1,4 @@ -# DKAN Open Data Platform (7.x-1.18.15) +# DKAN Open Data Platform (7.x-1.18.16) DKAN is an open source open data platform with a full suite of cataloging, publishing and visualization features that allow organizations to easily publish data to the public. diff --git a/profiles/dkan/dkan.info b/profiles/dkan/dkan.info index cd3ee8833d3..1d86dbb8095 100644 --- a/profiles/dkan/dkan.info +++ b/profiles/dkan/dkan.info @@ -127,4 +127,4 @@ dependencies[] = dkan_datastore_simple_import dependencies[] = dkan_datastore_api dependencies[] = open_data_schema_map_dkan dependencies[] = visualization_entity_charts_dkan -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/drupal-org-core.make b/profiles/dkan/drupal-org-core.make index ef9e01fd623..2a81631345e 100644 --- a/profiles/dkan/drupal-org-core.make +++ b/profiles/dkan/drupal-org-core.make @@ -3,7 +3,7 @@ core: 7.x projects: drupal: type: core - version: '7.78' + version: '7.80' # Use vocabulary machine name for permissions, see http://drupal.org/node/995156 patch: 995156: 'https://drupal.org/files/issues/995156-5_portable_taxonomy_permissions.patch' diff --git a/profiles/dkan/modules/dkan/dkan_data_dashboard/dkan_data_dashboard.info b/profiles/dkan/modules/dkan/dkan_data_dashboard/dkan_data_dashboard.info index 3f4e71110dc..9f3d942865a 100644 --- a/profiles/dkan/modules/dkan/dkan_data_dashboard/dkan_data_dashboard.info +++ b/profiles/dkan/modules/dkan/dkan_data_dashboard/dkan_data_dashboard.info @@ -30,4 +30,4 @@ features[variable][] = panelizer_node:data_dashboard_default features[views_view][] = data_dashboards features[views_view][] = front_page_dashboards_list features_exclude[dependencies][dkan_topics] = dkan_topics -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_data_story/dkan_data_story.info b/profiles/dkan/modules/dkan/dkan_data_story/dkan_data_story.info index 2c616ba0243..c24da2c6ce5 100644 --- a/profiles/dkan/modules/dkan/dkan_data_story/dkan_data_story.info +++ b/profiles/dkan/modules/dkan/dkan_data_story/dkan_data_story.info @@ -53,4 +53,4 @@ features_exclude[dependencies][image] = image features_exclude[dependencies][strongarm] = strongarm features_exclude[dependencies][taxonomy] = taxonomy no autodetect = 1 -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_dataset/dkan_dataset.info b/profiles/dkan/modules/dkan/dkan_dataset/dkan_dataset.info index d79d9b41694..5ba6add4f2f 100644 --- a/profiles/dkan/modules/dkan/dkan_dataset/dkan_dataset.info +++ b/profiles/dkan/modules/dkan/dkan_dataset/dkan_dataset.info @@ -33,4 +33,4 @@ features[ctools][] = views:views_default:3.0 features[features_api][] = api:2 features[variable][] = pathauto_node_dataset_pattern features[variable][] = pathauto_node_resource_pattern -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_content_types/dkan_dataset_content_types.info b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_content_types/dkan_dataset_content_types.info index 0514241e005..66ab95887f1 100644 --- a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_content_types/dkan_dataset_content_types.info +++ b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_content_types/dkan_dataset_content_types.info @@ -125,4 +125,4 @@ features_exclude[dependencies][og] = og features_exclude[dependencies][dkan_featured_topics] = dkan_featured_topics features_exclude[field_base][og_group_ref] = og_group_ref features_exclude[field_instance][node-dataset-og_group_ref] = node-dataset-og_group_ref -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/dkan_dataset_groups.info b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/dkan_dataset_groups.info index 627b363afa2..12262017ba0 100644 --- a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/dkan_dataset_groups.info +++ b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/dkan_dataset_groups.info @@ -74,4 +74,4 @@ features[views_view][] = front_page_group_grid features[views_view][] = front_page_group_list features[views_view][] = group_block features[views_view][] = groups_page -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/modules/dkan_dataset_groups_perms/dkan_dataset_groups_perms.info b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/modules/dkan_dataset_groups_perms/dkan_dataset_groups_perms.info index 3b95c145478..bceeb0e4748 100644 --- a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/modules/dkan_dataset_groups_perms/dkan_dataset_groups_perms.info +++ b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_groups/modules/dkan_dataset_groups_perms/dkan_dataset_groups_perms.info @@ -32,4 +32,4 @@ features[og_features_permission][] = node:group:update own dataset content features[og_features_permission][] = node:group:update own resource content features[og_features_permission][] = node:group:view any unpublished dataset content features[og_features_permission][] = node:group:view any unpublished resource content -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_rest_api/dkan_dataset_rest_api.info b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_rest_api/dkan_dataset_rest_api.info index 37821341a33..4124c44cff8 100644 --- a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_rest_api/dkan_dataset_rest_api.info +++ b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_rest_api/dkan_dataset_rest_api.info @@ -10,4 +10,4 @@ dependencies[] = services features[ctools][] = services:services:3 features[features_api][] = api:2 features[services_endpoint][] = dkan_dataset_api -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_voting/dkan_dataset_voting.info b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_voting/dkan_dataset_voting.info index d4c68d5cbd6..317833d97c7 100644 --- a/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_voting/dkan_dataset_voting.info +++ b/profiles/dkan/modules/dkan/dkan_dataset/modules/dkan_dataset_voting/dkan_dataset_voting.info @@ -17,4 +17,4 @@ features[field_instance][] = comment-comment_node_dataset-field_rating features[field_instance][] = node-dataset-field_rating features[variable][] = ajax_comments_node_types features[variable][] = ajax_comments_notify -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_dataset/tests/dkan_dataset_test.info b/profiles/dkan/modules/dkan/dkan_dataset/tests/dkan_dataset_test.info index 14e53a034cc..f2632aa97d5 100644 --- a/profiles/dkan/modules/dkan/dkan_dataset/tests/dkan_dataset_test.info +++ b/profiles/dkan/modules/dkan/dkan_dataset/tests/dkan_dataset_test.info @@ -4,4 +4,4 @@ core = 7.x dependencies[] = dkan_dataset dependencies[] = dkan_dataset_rest_api hidden = TRUE -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_datastore/dkan_datastore.info b/profiles/dkan/modules/dkan/dkan_datastore/dkan_datastore.info index 7c49993afdc..6f95b2ca8af 100644 --- a/profiles/dkan/modules/dkan/dkan_datastore/dkan_datastore.info +++ b/profiles/dkan/modules/dkan/dkan_datastore/dkan_datastore.info @@ -12,4 +12,4 @@ features[features_api][] = api:2 features[field_base][] = field_datastore_status features[field_instance][] = node-resource-field_datastore_status features[views_view][] = datasets -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_api/dkan_datastore_api.info b/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_api/dkan_datastore_api.info index e4324a29669..ee1c52d828f 100644 --- a/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_api/dkan_datastore_api.info +++ b/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_api/dkan_datastore_api.info @@ -4,4 +4,4 @@ package = DKAN API core = 7.x dependencies[] = services dependencies[] = dkan_datastore -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_fast_import/dkan_datastore_fast_import.info b/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_fast_import/dkan_datastore_fast_import.info index 60273fc513f..c5b43dd72fb 100644 --- a/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_fast_import/dkan_datastore_fast_import.info +++ b/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_fast_import/dkan_datastore_fast_import.info @@ -3,4 +3,4 @@ description = Enable fast import for resources core = 7.x package = DKAN dependencies[] = dkan_datastore -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_simple_import/dkan_datastore_simple_import.info b/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_simple_import/dkan_datastore_simple_import.info index ff684339b24..d3531c6c5ee 100644 --- a/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_simple_import/dkan_datastore_simple_import.info +++ b/profiles/dkan/modules/dkan/dkan_datastore/modules/dkan_datastore_simple_import/dkan_datastore_simple_import.info @@ -3,4 +3,4 @@ description = A datastore importer that uses MySQL insert statements, and a cust core = 7.x package = DKAN dependencies[] = dkan_datastore -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_environment/dkan_environment.info b/profiles/dkan/modules/dkan/dkan_environment/dkan_environment.info index cc9fa26fa94..f940a1dbd57 100755 --- a/profiles/dkan/modules/dkan/dkan_environment/dkan_environment.info +++ b/profiles/dkan/modules/dkan/dkan_environment/dkan_environment.info @@ -3,4 +3,4 @@ description = Basic environments management (Local, Development, Production, etc core = 7.x dependencies[] = environment dependencies[] = environment_indicator -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_fixtures/dkan_fixtures.info b/profiles/dkan/modules/dkan/dkan_fixtures/dkan_fixtures.info index 9406f0d0996..28cc3532996 100644 --- a/profiles/dkan/modules/dkan/dkan_fixtures/dkan_fixtures.info +++ b/profiles/dkan/modules/dkan/dkan_fixtures/dkan_fixtures.info @@ -14,4 +14,4 @@ files[] = includes/page.inc files[] = includes/panelized_node.inc files[] = includes/resource.inc files[] = includes/visualization_entity.inc -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_harvest/dkan_harvest.info b/profiles/dkan/modules/dkan/dkan_harvest/dkan_harvest.info index f078be1817a..ea1e414fa1c 100644 --- a/profiles/dkan/modules/dkan/dkan_harvest/dkan_harvest.info +++ b/profiles/dkan/modules/dkan/dkan_harvest/dkan_harvest.info @@ -73,4 +73,4 @@ files[] = includes/HarvestCache.php files[] = includes/HarvestMigrateSQLMap.php files[] = includes/HarvestMigrateSourceList.php files[] = includes/XmlHarvestMigration.php -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_dashboard/dkan_harvest_dashboard.info b/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_dashboard/dkan_harvest_dashboard.info index 0ced4ceea12..ba8ccabbfb0 100644 --- a/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_dashboard/dkan_harvest_dashboard.info +++ b/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_dashboard/dkan_harvest_dashboard.info @@ -23,4 +23,4 @@ features[views_view][] = dkan_harvest_datasets files[] = views/handlers/views_handler_field_date_harvest_date.inc files[] = views/handlers/views_handler_field_numeric_harvest_count.inc files[] = views/handlers/views_handler_field_harvest_status.inc -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_datajson/dkan_harvest_datajson.info b/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_datajson/dkan_harvest_datajson.info index 31152d9ad6a..fd62da90f31 100644 --- a/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_datajson/dkan_harvest_datajson.info +++ b/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_datajson/dkan_harvest_datajson.info @@ -5,4 +5,4 @@ core = 7.x dependencies[] = dkan_harvest files[] = dkan_harvest_datajson.migrate.inc -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_test/dkan_harvest_test.info b/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_test/dkan_harvest_test.info index 4e9a467890d..413304a9ad2 100644 --- a/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_test/dkan_harvest_test.info +++ b/profiles/dkan/modules/dkan/dkan_harvest/modules/dkan_harvest_test/dkan_harvest_test.info @@ -3,4 +3,4 @@ description = Test module for dkan_harvest core = 7.x dependencies[] = 'dkan_harvest' -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_ipe/dkan_ipe.info b/profiles/dkan/modules/dkan/dkan_ipe/dkan_ipe.info index 739b70d3abf..83f72734ca2 100644 --- a/profiles/dkan/modules/dkan/dkan_ipe/dkan_ipe.info +++ b/profiles/dkan/modules/dkan/dkan_ipe/dkan_ipe.info @@ -8,4 +8,4 @@ dependencies[] = panels dependencies[] = strongarm features[features_api][] = api:2 project path = profiles/dkan/modules/dkan -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_linkchecker/dkan_linkchecker.info b/profiles/dkan/modules/dkan/dkan_linkchecker/dkan_linkchecker.info index bc3664fbb98..e0646cb0af6 100644 --- a/profiles/dkan/modules/dkan/dkan_linkchecker/dkan_linkchecker.info +++ b/profiles/dkan/modules/dkan/dkan_linkchecker/dkan_linkchecker.info @@ -22,4 +22,4 @@ features[variable][] = linkchecker_scan_node_resource features[views_view][] = dkan_linkchecker_reports features_exclude[dependencies][ctools] = ctools project path = profiles/dkan/modules/dkan -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_migrate_base/dkan_migrate_base.info b/profiles/dkan/modules/dkan/dkan_migrate_base/dkan_migrate_base.info index 58de99c7d57..04202738527 100644 --- a/profiles/dkan/modules/dkan/dkan_migrate_base/dkan_migrate_base.info +++ b/profiles/dkan/modules/dkan/dkan_migrate_base/dkan_migrate_base.info @@ -11,4 +11,4 @@ files[] = dkan_migrate_base_group.inc files[] = dkan_migrate_base_dataset.inc files[] = dkan_migrate_base_resource.inc files[] = dkan_migrate_base_data_json.inc -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_migrate_base/modules/dkan_migrate_base_example/dkan_migrate_base_example.info b/profiles/dkan/modules/dkan/dkan_migrate_base/modules/dkan_migrate_base_example/dkan_migrate_base_example.info index 5802950eaa7..ec2bfd6c26c 100644 --- a/profiles/dkan/modules/dkan/dkan_migrate_base/modules/dkan_migrate_base_example/dkan_migrate_base_example.info +++ b/profiles/dkan/modules/dkan/dkan_migrate_base/modules/dkan_migrate_base_example/dkan_migrate_base_example.info @@ -4,4 +4,4 @@ package = "DKAN" core = 7.x dependencies[] = dkan_migrate_base dependencies[] = open_data_federal_extras -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_periodic_updates/dkan_periodic_updates.info b/profiles/dkan/modules/dkan/dkan_periodic_updates/dkan_periodic_updates.info index 8a8145ec703..b138b060daf 100644 --- a/profiles/dkan/modules/dkan/dkan_periodic_updates/dkan_periodic_updates.info +++ b/profiles/dkan/modules/dkan/dkan_periodic_updates/dkan_periodic_updates.info @@ -4,4 +4,4 @@ core = 7.x package = DKAN dependencies[] = dkan_dataset dependencies[] = dkan_datastore -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_permissions/dkan_permissions.info b/profiles/dkan/modules/dkan/dkan_permissions/dkan_permissions.info index 57de983c096..bd303e2574f 100644 --- a/profiles/dkan/modules/dkan/dkan_permissions/dkan_permissions.info +++ b/profiles/dkan/modules/dkan/dkan_permissions/dkan_permissions.info @@ -11,4 +11,4 @@ features[roles_permissions][] = editor features[roles_permissions][] = site manager features_exclude[dependencies][features] = features project path = profiles/dkan/modules/dkan -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_plugins/dkan_plugins.info b/profiles/dkan/modules/dkan/dkan_plugins/dkan_plugins.info index 7c05e229792..8f255b4d194 100644 --- a/profiles/dkan/modules/dkan/dkan_plugins/dkan_plugins.info +++ b/profiles/dkan/modules/dkan/dkan_plugins/dkan_plugins.info @@ -6,4 +6,4 @@ dependencies[] = ctools dependencies[] = panels project path = profiles/dkan/modules/dkan scripts[] = js/colorPicker.behavior.js -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/dkan_sitewide.info b/profiles/dkan/modules/dkan/dkan_sitewide/dkan_sitewide.info index fcfa6687b36..5f353e289e9 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/dkan_sitewide.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/dkan_sitewide.info @@ -56,4 +56,4 @@ features[views_view][] = dkan_administration_files features[views_view][] = dkan_administration_nodes features[views_view][] = dkan_administration_users features[views_view][] = popular_tags -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_context/dkan_sitewide_context.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_context/dkan_sitewide_context.info index 15dfa47d406..280a019d83d 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_context/dkan_sitewide_context.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_context/dkan_sitewide_context.info @@ -19,4 +19,4 @@ features[context][] = sitewide features[ctools][] = context:context:3 features[ctools][] = strongarm:strongarm:1 features[features_api][] = api:2 -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_demo_front/dkan_sitewide_demo_front.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_demo_front/dkan_sitewide_demo_front.info index dc0a28ac676..cde9fe2c79f 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_demo_front/dkan_sitewide_demo_front.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_demo_front/dkan_sitewide_demo_front.info @@ -9,4 +9,4 @@ features[ctools][] = context:context:3 features[ctools][] = page_manager:pages_default:1 features[features_api][] = api:2 fetures[context][] = front -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_menu/dkan_sitewide_menu.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_menu/dkan_sitewide_menu.info index 535e73f3554..2fd0aa58339 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_menu/dkan_sitewide_menu.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_menu/dkan_sitewide_menu.info @@ -44,4 +44,4 @@ features[menu_links][] = menu-command-center-menu_site-information:admin/config/ features[menu_links][] = menu-command-center-menu_taxonomy:admin/structure/taxonomy features[menu_links][] = menu-command-center-menu_visualization:admin/structure/entity-type/visualization/ve_chart/add features[menu_links][] = menu-command-center-menu_visualizations:admin/structure/entity-type/visualization -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panelizer/dkan_sitewide_panelizer.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panelizer/dkan_sitewide_panelizer.info index 6782c654e66..6057add8bb5 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panelizer/dkan_sitewide_panelizer.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panelizer/dkan_sitewide_panelizer.info @@ -17,4 +17,4 @@ features[variable][] = panelizer_node:page_allowed_layouts_default features[variable][] = panelizer_node:page_allowed_types features[variable][] = panelizer_node:page_allowed_types_default features[variable][] = panelizer_node:page_default -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panels/dkan_sitewide_panels.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panels/dkan_sitewide_panels.info index 6f4b72bb4f8..a3359be490d 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panels/dkan_sitewide_panels.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_panels/dkan_sitewide_panels.info @@ -48,4 +48,4 @@ features[views_view][] = dkan_datasets_filtered features[views_view][] = dkan_groups features[views_view][] = entity_reference_groups_list features[views_view][] = list_of_users_groups -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_search_db/dkan_sitewide_search_db.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_search_db/dkan_sitewide_search_db.info index 0937c32f5f8..b79bf7b3db7 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_search_db/dkan_sitewide_search_db.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_search_db/dkan_sitewide_search_db.info @@ -18,4 +18,4 @@ features[features_api][] = api:2 features[search_api_server][] = datasets features[variable][] = facetapi_pretty_paths_searcher_search_api@datasets features[variable][] = facetapi_pretty_paths_searcher_search_api@datasets_options -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_user/dkan_sitewide_user.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_user/dkan_sitewide_user.info index 725c03c4789..e147c9287b0 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_user/dkan_sitewide_user.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/dkan_sitewide_user/dkan_sitewide_user.info @@ -21,4 +21,4 @@ features[field_group][] = group_user_tabs|user|user|default features[field_instance][] = user-user-field_about features[views_view][] = user_profile_fields features[views_view][] = user_profile_search -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_sitewide/modules/facet_icons/facet_icons.info b/profiles/dkan/modules/dkan/dkan_sitewide/modules/facet_icons/facet_icons.info index f4d431a1d94..db470a2a0cd 100644 --- a/profiles/dkan/modules/dkan/dkan_sitewide/modules/facet_icons/facet_icons.info +++ b/profiles/dkan/modules/dkan/dkan_sitewide/modules/facet_icons/facet_icons.info @@ -6,4 +6,4 @@ dependencies[] = facetapi dependencies[] = panels_style_collapsible files[] = widget_term_icons.inc files[] = widget_content_type_icons.inc -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_topics/dkan_topics.info b/profiles/dkan/modules/dkan/dkan_topics/dkan_topics.info index f2fa7f298ad..2e631af44af 100755 --- a/profiles/dkan/modules/dkan/dkan_topics/dkan_topics.info +++ b/profiles/dkan/modules/dkan/dkan_topics/dkan_topics.info @@ -57,4 +57,4 @@ features_exclude[dependencies][dkan_dataset_groups] = dkan_dataset_groups features_exclude[dependencies][dkan_topics] = dkan_topics no autodetect = 1 project path = profiles/dkan/modules/dkan -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_topics/modules/dkan_default_topics/dkan_default_topics.info b/profiles/dkan/modules/dkan/dkan_topics/modules/dkan_default_topics/dkan_default_topics.info index 4972ee53ff7..7f8a7dd95c7 100755 --- a/profiles/dkan/modules/dkan/dkan_topics/modules/dkan_default_topics/dkan_default_topics.info +++ b/profiles/dkan/modules/dkan/dkan_topics/modules/dkan_default_topics/dkan_default_topics.info @@ -5,4 +5,4 @@ package = DKAN Features dependencies[] = dkan_topics dependencies[] = taxonomy_fixtures dependencies[] = taxonomy -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_workflow/dkan_workflow.info b/profiles/dkan/modules/dkan/dkan_workflow/dkan_workflow.info index 1ca9159d4f5..c33cb2442b9 100644 --- a/profiles/dkan/modules/dkan/dkan_workflow/dkan_workflow.info +++ b/profiles/dkan/modules/dkan/dkan_workflow/dkan_workflow.info @@ -33,4 +33,4 @@ features[workbench_moderation_transitions][] = needs_review:published features[workbench_moderation_transitions][] = published:needs_review features_exclude[dependencies][ctools] = ctools features_exclude[dependencies][dkan_dataset_content_types] = dkan_dataset_content_types -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_workflow/modules/dkan_workflow_permissions/dkan_workflow_permissions.info b/profiles/dkan/modules/dkan/dkan_workflow/modules/dkan_workflow_permissions/dkan_workflow_permissions.info index 8890a89f2f6..f8fe5098e80 100644 --- a/profiles/dkan/modules/dkan/dkan_workflow/modules/dkan_workflow_permissions/dkan_workflow_permissions.info +++ b/profiles/dkan/modules/dkan/dkan_workflow/modules/dkan_workflow_permissions/dkan_workflow_permissions.info @@ -9,4 +9,4 @@ features[roles_permissions][] = Workflow Contributor features[roles_permissions][] = Workflow Moderator features[roles_permissions][] = Workflow Supervisor project path = profiles/dkan/modules/dkan/dkan_workflow/modules -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/dkan_workflow/modules/views_dkan_workflow_tree/views_dkan_workflow_tree.info b/profiles/dkan/modules/dkan/dkan_workflow/modules/views_dkan_workflow_tree/views_dkan_workflow_tree.info index cc4ed59cf21..5344bbf93bf 100644 --- a/profiles/dkan/modules/dkan/dkan_workflow/modules/views_dkan_workflow_tree/views_dkan_workflow_tree.info +++ b/profiles/dkan/modules/dkan/dkan_workflow/modules/views_dkan_workflow_tree/views_dkan_workflow_tree.info @@ -6,4 +6,4 @@ dependencies[] = views dependencies[] = workbench_moderation files[] = ViewsDkanWorkflowTreePluginStyle.inc stylesheets[all][] = views_dkan_workflow_tree.css -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/open_data_federal_extras/open_data_federal_extras.info b/profiles/dkan/modules/dkan/open_data_federal_extras/open_data_federal_extras.info index 13afa6378d5..110ae9f10e3 100644 --- a/profiles/dkan/modules/dkan/open_data_federal_extras/open_data_federal_extras.info +++ b/profiles/dkan/modules/dkan/open_data_federal_extras/open_data_federal_extras.info @@ -27,4 +27,4 @@ features[field_instance][] = node-dataset-field_odfe_data_quality features[field_instance][] = node-dataset-field_odfe_investment_uii features[field_instance][] = node-dataset-field_odfe_program_code features[field_instance][] = node-dataset-field_odfe_system_of_records -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/profiles/dkan/modules/dkan/open_data_schema_map_dkan/open_data_schema_map_dkan.info b/profiles/dkan/modules/dkan/open_data_schema_map_dkan/open_data_schema_map_dkan.info index 0b31fe7dd83..9f8c6d6295a 100644 --- a/profiles/dkan/modules/dkan/open_data_schema_map_dkan/open_data_schema_map_dkan.info +++ b/profiles/dkan/modules/dkan/open_data_schema_map_dkan/open_data_schema_map_dkan.info @@ -21,4 +21,4 @@ features[open_data_schema_apis][] = data_json_1_1 features[open_data_schema_apis][] = dcat_ap_v1_1_dataset features[open_data_schema_apis][] = dcat_v1_1 features[open_data_schema_apis][] = dcat_v1_1_json -version = 7.x-1.18.15 +version = 7.x-1.18.16 diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 713662df69f..3e88c383430 100755 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -323,7 +323,7 @@ * * To see what PHP settings are possible, including whether they can be set at * runtime (by using ini_set()), read the PHP documentation: - * http://www.php.net/manual/en/ini.list.php + * http://www.php.net/manual/ini.list.php * See drupal_environment_initialize() in includes/bootstrap.inc for required * runtime settings and the .htaccess file for non-runtime settings. Settings * defined there should not be duplicated here so as to avoid conflict issues. @@ -359,7 +359,7 @@ * output filter may not have sufficient memory to process it. If you * experience this issue, you may wish to uncomment the following two lines * and increase the limits of these variables. For more information, see - * http://php.net/manual/en/pcre.configuration.php. + * http://php.net/manual/pcre.configuration.php. */ # ini_set('pcre.backtrack_limit', 200000); # ini_set('pcre.recursion_limit', 200000); @@ -634,15 +634,6 @@ */ # $conf['allow_authorize_operations'] = FALSE; -/** - * Smart start: - * - * If you would prefer to be redirected to the installation system when a - * valid settings.php file is present but no tables are installed, remove - * the leading hash sign below. - */ -# $conf['pressflow_smart_start'] = TRUE; - /** * Theme debugging: * @@ -711,6 +702,15 @@ */ # $conf['variable_initialize_wait_for_lock'] = FALSE; +/** + * Opt in to field_sql_storage_field_storage_write() optimization. + * + * To reduce unnecessary writes field_sql_storage_field_storage_write() can skip + * fields where values have apparently not changed. To opt in to this + * optimization, set this variable to TRUE. + */ +$conf['field_sql_storage_skip_writing_unchanged_fields'] = TRUE; + /** * Use site name as display-name in outgoing mail. * @@ -725,3 +725,23 @@ * @see drupal_mail() */ $conf['mail_display_name_site_name'] = TRUE; + +/** + * SameSite cookie attribute. + * + * This variable can be used to set a value for the SameSite cookie attribute. + * + * Versions of PHP before 7.3 have no native support for the SameSite attribute + * so it is emulated. + * + * The session.cookie-samesite setting in PHP 7.3 and later will be overridden + * by this variable for Drupal session cookies, and any other cookies managed + * with drupal_setcookie(). + * + * Setting this variable to FALSE disables the SameSite attribute on cookies. + * + * @see drupal_setcookie() + * @see drupal_session_start() + * @see https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite + */ +#$conf['samesite_cookie_value'] = 'None';