diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f2b2dd257..2c3b93c64 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,13 @@ +Drupal 7.77, 2020-12-03 +----------------------- +- Hotfix for schema.prefixed tables + +Drupal 7.76, 2020-12-02 +----------------------- +- Support for MySQL 8 +- Core tests pass in SQLite +- Better user flood control logging + Drupal 7.75, 2020-11-26 ----------------------- - Fixed security issues: diff --git a/LUGGAGE_CHANGELOG.txt b/LUGGAGE_CHANGELOG.txt index 5ecb6fafa..75867d8e0 100644 --- a/LUGGAGE_CHANGELOG.txt +++ b/LUGGAGE_CHANGELOG.txt @@ -2,6 +2,11 @@ How to read this changelog: The LUGG- prefix refers to JIRA issue numbers; the # prefix refers to GitHub issue numbers. +Luggage 3.6.17, 2020-12-23 +Drupal 7.77, 2020-12-03 +------------------------- +- LUGG-1221 - Drupal 7.77 + Luggage 3.6.16, 2020-12-02 Drupal 7.75, 2020-11-25 ------------------------- diff --git a/LUGGAGE_ISU_CHANGELOG.txt b/LUGGAGE_ISU_CHANGELOG.txt index f28cf1d49..f0245979e 100644 --- a/LUGGAGE_ISU_CHANGELOG.txt +++ b/LUGGAGE_ISU_CHANGELOG.txt @@ -6,6 +6,12 @@ The Luggage_ISU version number shows the upstream Luggage version it is based on as well as the Luggage_ISU version. For example, Luggage_ISU 3.5.0-5.0 is based on the upstream Luggage release 3.5.0. +Luggage_ISU 3.6.17-6.17. 2020-12-23 +Drupal 7.77, 2020-12-03 +------------------------- +Merged with upstream Luggage 3.6.17 +- LUGG-1221 - Drupal 7.77 + Luggage_ISU 3.6.16-6.16. 2020-12-02 Drupal 7.75, 2020-11-25 ------------------------- diff --git a/LUGGAGE_ISU_VERSION.php b/LUGGAGE_ISU_VERSION.php index b36207548..5d741b34e 100644 --- a/LUGGAGE_ISU_VERSION.php +++ b/LUGGAGE_ISU_VERSION.php @@ -1,3 +1,3 @@ data; } else { - // Cache miss. Avoid a stampede. + // Cache miss. Avoid a stampede by acquiring a lock. If the lock fails to + // acquire, optionally just continue with uncached processing. $name = 'variable_init'; - if (!lock_acquire($name, 1)) { - // Another request is building the variable cache. - // Wait, then re-run this function. + $lock_acquired = lock_acquire($name, 1); + if (!$lock_acquired && variable_get('variable_initialize_wait_for_lock', FALSE)) { lock_wait($name); return variable_initialize($conf); } else { - // Proceed with variable rebuild. + // Load the variables from the table. $variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed()); - cache_set('variables', $variables, 'cache_bootstrap'); - lock_release($name); + if ($lock_acquired) { + cache_set('variables', $variables, 'cache_bootstrap'); + lock_release($name); + } } } diff --git a/includes/common.inc b/includes/common.inc index 07373ac47..7b7955855 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -6653,30 +6653,41 @@ function element_children(&$elements, $sort = FALSE) { $sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort; // Filter out properties from the element, leaving only children. - $children = array(); + $count = count($elements); + $child_weights = array(); + $i = 0; $sortable = FALSE; foreach ($elements as $key => $value) { if (is_int($key) || $key === '' || $key[0] !== '#') { - $children[$key] = $value; if (is_array($value) && isset($value['#weight'])) { + $weight = $value['#weight']; $sortable = TRUE; } + else { + $weight = 0; + } + // Support weights with up to three digit precision and conserve the + // insertion order. + $child_weights[$key] = floor($weight * 1000) + $i / $count; } + $i++; } + // Sort the children if necessary. if ($sort && $sortable) { - uasort($children, 'element_sort'); + asort($child_weights); // Put the sorted children back into $elements in the correct order, to // preserve sorting if the same element is passed through // element_children() twice. - foreach ($children as $key => $child) { + foreach ($child_weights as $key => $weight) { + $value = $elements[$key]; unset($elements[$key]); - $elements[$key] = $child; + $elements[$key] = $value; } $elements['#sorted'] = TRUE; } - return array_keys($children); + return array_keys($child_weights); } /** diff --git a/includes/database/database.inc b/includes/database/database.inc index 6879f6991..d4d2d8f02 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -310,6 +310,13 @@ abstract class DatabaseConnection extends PDO { */ protected $escapedAliases = array(); + /** + * List of un-prefixed table names, keyed by prefixed table names. + * + * @var array + */ + protected $unprefixedTablesMap = array(); + function __construct($dsn, $username, $password, $driver_options = array()) { // Initialize and prepare the connection prefix. $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : ''); @@ -338,7 +345,9 @@ abstract class DatabaseConnection extends PDO { // Destroy all references to this connection by setting them to NULL. // The Statement class attribute only accepts a new value that presents a // proper callable, so we reset it to PDOStatement. - $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); + if (!empty($this->statementClass)) { + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); + } $this->schema = NULL; } @@ -442,6 +451,13 @@ abstract class DatabaseConnection extends PDO { $this->prefixReplace[] = $this->prefixes['default']; $this->prefixSearch[] = '}'; $this->prefixReplace[] = ''; + + // Set up a map of prefixed => un-prefixed tables. + foreach ($this->prefixes as $table_name => $prefix) { + if ($table_name !== 'default') { + $this->unprefixedTablesMap[$prefix . $table_name] = $table_name; + } + } } /** @@ -477,6 +493,17 @@ abstract class DatabaseConnection extends PDO { } } + /** + * Gets a list of individually prefixed table names. + * + * @return array + * An array of un-prefixed table names, keyed by their fully qualified table + * names (i.e. prefix + table_name). + */ + public function getUnprefixedTablesMap() { + return $this->unprefixedTablesMap; + } + /** * Prepares a query string and returns the prepared statement. * @@ -2840,7 +2867,6 @@ function db_field_exists($table, $field) { * * @param $table_expression * An SQL expression, for example "simpletest%" (without the quotes). - * BEWARE: this is not prefixed, the caller should take care of that. * * @return * Array, both the keys and the values are the matching tables. @@ -2849,6 +2875,23 @@ function db_find_tables($table_expression) { return Database::getConnection()->schema()->findTables($table_expression); } +/** + * Finds all tables that are like the specified base table name. This is a + * backport of the change made to db_find_tables in Drupal 8 to work with + * virtual, un-prefixed table names. The original function is retained for + * Backwards Compatibility. + * @see https://www.drupal.org/node/2552435 + * + * @param $table_expression + * An SQL expression, for example "simpletest%" (without the quotes). + * + * @return + * Array, both the keys and the values are the matching tables. + */ +function db_find_tables_d8($table_expression) { + return Database::getConnection()->schema()->findTablesD8($table_expression); +} + function _db_create_keys_sql($spec) { return Database::getConnection()->schema()->createKeysSql($spec); } diff --git a/includes/database/mysql/database.inc b/includes/database/mysql/database.inc index 356e039f7..00df3c13e 100644 --- a/includes/database/mysql/database.inc +++ b/includes/database/mysql/database.inc @@ -5,6 +5,11 @@ * Database interface code for MySQL database servers. */ +/** + * The default character for quoting identifiers in MySQL. + */ +define('MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT', '`'); + /** * @addtogroup database * @{ @@ -19,6 +24,277 @@ class DatabaseConnection_mysql extends DatabaseConnection { */ protected $needsCleanup = FALSE; + /** + * The list of MySQL reserved key words. + * + * @link https://dev.mysql.com/doc/refman/8.0/en/keywords.html + */ + private $reservedKeyWords = array( + 'accessible', + 'add', + 'admin', + 'all', + 'alter', + 'analyze', + 'and', + 'as', + 'asc', + 'asensitive', + 'before', + 'between', + 'bigint', + 'binary', + 'blob', + 'both', + 'by', + 'call', + 'cascade', + 'case', + 'change', + 'char', + 'character', + 'check', + 'collate', + 'column', + 'condition', + 'constraint', + 'continue', + 'convert', + 'create', + 'cross', + 'cube', + 'cume_dist', + 'current_date', + 'current_time', + 'current_timestamp', + 'current_user', + 'cursor', + 'database', + 'databases', + 'day_hour', + 'day_microsecond', + 'day_minute', + 'day_second', + 'dec', + 'decimal', + 'declare', + 'default', + 'delayed', + 'delete', + 'dense_rank', + 'desc', + 'describe', + 'deterministic', + 'distinct', + 'distinctrow', + 'div', + 'double', + 'drop', + 'dual', + 'each', + 'else', + 'elseif', + 'empty', + 'enclosed', + 'escaped', + 'except', + 'exists', + 'exit', + 'explain', + 'false', + 'fetch', + 'first_value', + 'float', + 'float4', + 'float8', + 'for', + 'force', + 'foreign', + 'from', + 'fulltext', + 'function', + 'generated', + 'get', + 'grant', + 'group', + 'grouping', + 'groups', + 'having', + 'high_priority', + 'hour_microsecond', + 'hour_minute', + 'hour_second', + 'if', + 'ignore', + 'in', + 'index', + 'infile', + 'inner', + 'inout', + 'insensitive', + 'insert', + 'int', + 'int1', + 'int2', + 'int3', + 'int4', + 'int8', + 'integer', + 'interval', + 'into', + 'io_after_gtids', + 'io_before_gtids', + 'is', + 'iterate', + 'join', + 'json_table', + 'key', + 'keys', + 'kill', + 'lag', + 'last_value', + 'lead', + 'leading', + 'leave', + 'left', + 'like', + 'limit', + 'linear', + 'lines', + 'load', + 'localtime', + 'localtimestamp', + 'lock', + 'long', + 'longblob', + 'longtext', + 'loop', + 'low_priority', + 'master_bind', + 'master_ssl_verify_server_cert', + 'match', + 'maxvalue', + 'mediumblob', + 'mediumint', + 'mediumtext', + 'middleint', + 'minute_microsecond', + 'minute_second', + 'mod', + 'modifies', + 'natural', + 'not', + 'no_write_to_binlog', + 'nth_value', + 'ntile', + 'null', + 'numeric', + 'of', + 'on', + 'optimize', + 'optimizer_costs', + 'option', + 'optionally', + 'or', + 'order', + 'out', + 'outer', + 'outfile', + 'over', + 'partition', + 'percent_rank', + 'persist', + 'persist_only', + 'precision', + 'primary', + 'procedure', + 'purge', + 'range', + 'rank', + 'read', + 'reads', + 'read_write', + 'real', + 'recursive', + 'references', + 'regexp', + 'release', + 'rename', + 'repeat', + 'replace', + 'require', + 'resignal', + 'restrict', + 'return', + 'revoke', + 'right', + 'rlike', + 'row', + 'rows', + 'row_number', + 'schema', + 'schemas', + 'second_microsecond', + 'select', + 'sensitive', + 'separator', + 'set', + 'show', + 'signal', + 'smallint', + 'spatial', + 'specific', + 'sql', + 'sqlexception', + 'sqlstate', + 'sqlwarning', + 'sql_big_result', + 'sql_calc_found_rows', + 'sql_small_result', + 'ssl', + 'starting', + 'stored', + 'straight_join', + 'system', + 'table', + 'terminated', + 'then', + 'tinyblob', + 'tinyint', + 'tinytext', + 'to', + 'trailing', + 'trigger', + 'true', + 'undo', + 'union', + 'unique', + 'unlock', + 'unsigned', + 'update', + 'usage', + 'use', + 'using', + 'utc_date', + 'utc_time', + 'utc_timestamp', + 'values', + 'varbinary', + 'varchar', + 'varcharacter', + 'varying', + 'virtual', + 'when', + 'where', + 'while', + 'window', + 'with', + 'write', + 'xor', + 'year_month', + 'zerofill', + ); + public function __construct(array $connection_options = array()) { // This driver defaults to transaction support, except if explicitly passed FALSE. $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE); @@ -86,15 +362,95 @@ class DatabaseConnection_mysql extends DatabaseConnection { $connection_options += array( 'init_commands' => array(), ); + + $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', '<')) { + $sql_mode .= ',NO_AUTO_CREATE_USER'; + } $connection_options['init_commands'] += array( - 'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'", + 'sql_mode' => "SET sql_mode = '$sql_mode'", ); + // Execute initial commands. foreach ($connection_options['init_commands'] as $sql) { $this->exec($sql); } } + /** + * {@inheritdoc}} + */ + protected function setPrefix($prefix) { + parent::setPrefix($prefix); + // Successive versions of MySQL have become increasingly strict about the + // use of reserved keywords as table names. Drupal 7 uses at least one such + // table (system). Therefore we surround all table names with quotes. + $quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT); + foreach ($this->prefixSearch as $i => $prefixSearch) { + if (substr($prefixSearch, 0, 1) === '{') { + // If the prefix already contains one or more quotes remove them. + // This can happen when - for example - DrupalUnitTestCase sets up a + // "temporary prefixed database". Also if there's a dot in the prefix, + // wrap it in quotes to cater for schema names in prefixes. + $search = array($quote_char, '.'); + $replace = array('', $quote_char . '.' . $quote_char); + $this->prefixReplace[$i] = $quote_char . str_replace($search, $replace, $this->prefixReplace[$i]); + } + if (substr($prefixSearch, -1) === '}') { + $this->prefixReplace[$i] .= $quote_char; + } + } + } + + /** + * {@inheritdoc} + */ + public function escapeField($field) { + $field = parent::escapeField($field); + return $this->quoteIdentifier($field); + } + + public function escapeFields(array $fields) { + foreach ($fields as &$field) { + $field = $this->escapeField($field); + } + return $fields; + } + + /** + * {@inheritdoc} + */ + public function escapeAlias($field) { + $field = parent::escapeAlias($field); + return $this->quoteIdentifier($field); + } + + /** + * Quotes an identifier if it matches a MySQL reserved keyword. + * + * @param string $identifier + * The field to check. + * + * @return string + * The identifier, quoted if it matches a MySQL reserved keyword. + */ + private function quoteIdentifier($identifier) { + // Quote identifiers so that MySQL reserved words like 'function' can be + // used as column names. Sometimes the 'table.column_name' format is passed + // in. For example, menu_load_links() adds a condition on "ml.menu_name". + if (strpos($identifier, '.') !== FALSE) { + list($table, $identifier) = explode('.', $identifier, 2); + } + if (in_array(strtolower($identifier), $this->reservedKeyWords, TRUE)) { + // Quote the string for MySQL reserved keywords. + $quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT); + $identifier = $quote_char . $identifier . $quote_char; + } + return isset($table) ? $table . '.' . $identifier : $identifier; + } + public function __destruct() { if ($this->needsCleanup) { $this->nextIdDelete(); diff --git a/includes/database/mysql/query.inc b/includes/database/mysql/query.inc index d3d2d9eec..3f0bcb796 100644 --- a/includes/database/mysql/query.inc +++ b/includes/database/mysql/query.inc @@ -48,6 +48,10 @@ class InsertQuery_mysql extends InsertQuery { // Default fields are always placed first for consistency. $insert_fields = array_merge($this->defaultFields, $this->insertFields); + if (method_exists($this->connection, 'escapeFields')) { + $insert_fields = $this->connection->escapeFields($insert_fields); + } + // If we're selecting from a SelectQuery, finish building the query and // pass it back, as any remaining options are irrelevant. if (!empty($this->fromQuery)) { @@ -89,6 +93,20 @@ class InsertQuery_mysql extends InsertQuery { class TruncateQuery_mysql extends TruncateQuery { } +class UpdateQuery_mysql extends UpdateQuery { + public function __toString() { + if (method_exists($this->connection, 'escapeField')) { + $escapedFields = array(); + foreach ($this->fields as $field => $data) { + $field = $this->connection->escapeField($field); + $escapedFields[$field] = $data; + } + $this->fields = $escapedFields; + } + return parent::__toString(); + } +} + /** * @} End of "addtogroup database". */ diff --git a/includes/database/mysql/schema.inc b/includes/database/mysql/schema.inc index 9ba1c7339..7d6e33395 100644 --- a/includes/database/mysql/schema.inc +++ b/includes/database/mysql/schema.inc @@ -57,6 +57,11 @@ class DatabaseSchema_mysql extends DatabaseSchema { protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) { $info = $this->connection->getConnectionOptions(); + // Ensure the table name is not surrounded with quotes as that is not + // appropriate for schema queries. + $quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT); + $table_name = str_replace($quote_char, '', $table_name); + $table_info = $this->getPrefixInfo($table_name, $add_prefix); $condition = new DatabaseCondition('AND'); @@ -494,11 +499,11 @@ class DatabaseSchema_mysql extends DatabaseSchema { $condition->condition('column_name', $column); $condition->compile($this->connection, $this); // Don't use {} around information_schema.columns table. - return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField(); + return $this->connection->query("SELECT column_comment AS column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField(); } $condition->compile($this->connection, $this); // Don't use {} around information_schema.tables table. - $comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField(); + $comment = $this->connection->query("SELECT table_comment AS table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField(); // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379 return preg_replace('/; InnoDB free:.*$/', '', $comment); } diff --git a/includes/database/schema.inc b/includes/database/schema.inc index 31862db39..faa521623 100644 --- a/includes/database/schema.inc +++ b/includes/database/schema.inc @@ -169,6 +169,11 @@ require_once dirname(__FILE__) . '/query.inc'; */ abstract class DatabaseSchema implements QueryPlaceholderInterface { + /** + * The database connection. + * + * @var DatabaseConnection + */ protected $connection; /** @@ -343,7 +348,70 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface { // couldn't use db_select() here because it would prefix // information_schema.tables and the query would fail. // Don't use {} around information_schema.tables table. - return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0); + return $this->connection->query("SELECT table_name AS table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0); + } + + /** + * Finds all tables that are like the specified base table name. This is a + * backport of the change made to findTables in Drupal 8 to work with virtual, + * un-prefixed table names. The original function is retained for Backwards + * Compatibility. + * @see https://www.drupal.org/node/2552435 + * + * @param string $table_expression + * An SQL expression, for example "cache_%" (without the quotes). + * + * @return array + * Both the keys and the values are the matching tables. + */ + public function findTablesD8($table_expression) { + // Load all the tables up front in order to take into account per-table + // prefixes. The actual matching is done at the bottom of the method. + $condition = $this->buildTableNameCondition('%', 'LIKE'); + $condition->compile($this->connection, $this); + + $individually_prefixed_tables = $this->connection->getUnprefixedTablesMap(); + $default_prefix = $this->connection->tablePrefix(); + $default_prefix_length = strlen($default_prefix); + $tables = array(); + // Normally, we would heartily discourage the use of string + // concatenation for conditionals like this however, we + // couldn't use db_select() here because it would prefix + // information_schema.tables and the query would fail. + // Don't use {} around information_schema.tables table. + $results = $this->connection->query("SELECT table_name AS table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments()); + foreach ($results as $table) { + // Take into account tables that have an individual prefix. + if (isset($individually_prefixed_tables[$table->table_name])) { + $prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->table_name])); + } + elseif ($default_prefix && substr($table->table_name, 0, $default_prefix_length) !== $default_prefix) { + // This table name does not start the default prefix, which means that + // it is not managed by Drupal so it should be excluded from the result. + continue; + } + else { + $prefix_length = $default_prefix_length; + } + + // Remove the prefix from the returned tables. + $unprefixed_table_name = substr($table->table_name, $prefix_length); + + // The pattern can match a table which is the same as the prefix. That + // will become an empty string when we remove the prefix, which will + // probably surprise the caller, besides not being a prefixed table. So + // remove it. + if (!empty($unprefixed_table_name)) { + $tables[$unprefixed_table_name] = $unprefixed_table_name; + } + } + + // Convert the table expression from its SQL LIKE syntax to a regular + // expression and escape the delimiter that will be used for matching. + $table_expression = str_replace(array('%', '_'), array('.*?', '.'), preg_quote($table_expression, '/')); + $tables = preg_grep('/^' . $table_expression . '$/i', $tables); + + return $tables; } /** diff --git a/includes/database/select.inc b/includes/database/select.inc index 8d84460e8..84098bdf7 100644 --- a/includes/database/select.inc +++ b/includes/database/select.inc @@ -1520,13 +1520,16 @@ class SelectQuery extends Query implements SelectQueryInterface { $fields = array(); foreach ($this->tables as $alias => $table) { if (!empty($table['all_fields'])) { - $fields[] = $this->connection->escapeTable($alias) . '.*'; + $fields[] = $this->connection->escapeAlias($alias) . '.*'; } } foreach ($this->fields as $alias => $field) { + // Note that $field['table'] holds the table alias. + // @see \SelectQuery::addField + $table = isset($field['table']) ? $this->connection->escapeAlias($field['table']) . '.' : ''; // Always use the AS keyword for field aliases, as some // databases require it (e.g., PostgreSQL). - $fields[] = (isset($field['table']) ? $this->connection->escapeTable($field['table']) . '.' : '') . $this->connection->escapeField($field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']); + $fields[] = $table . $this->connection->escapeField($field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']); } foreach ($this->expressions as $alias => $expression) { $fields[] = $expression['expression'] . ' AS ' . $this->connection->escapeAlias($expression['alias']); @@ -1555,7 +1558,7 @@ class SelectQuery extends Query implements SelectQueryInterface { // Don't use the AS keyword for table aliases, as some // databases don't support it (e.g., Oracle). - $query .= $table_string . ' ' . $this->connection->escapeTable($table['alias']); + $query .= $table_string . ' ' . $this->connection->escapeAlias($table['alias']); if (!empty($table['condition'])) { $query .= ' ON ' . $table['condition']; diff --git a/includes/database/sqlite/database.inc b/includes/database/sqlite/database.inc index 589a17287..c50f08ec5 100644 --- a/includes/database/sqlite/database.inc +++ b/includes/database/sqlite/database.inc @@ -107,6 +107,18 @@ class DatabaseConnection_sqlite extends DatabaseConnection { $this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3); $this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand')); + // Enable the Write-Ahead Logging (WAL) option for SQLite if supported. + // @see https://www.drupal.org/node/2348137 + // @see https://sqlite.org/wal.html + if (version_compare($version, '3.7') >= 0) { + $connection_options += array( + 'init_commands' => array(), + ); + $connection_options['init_commands'] += array( + 'wal' => "PRAGMA journal_mode=WAL", + ); + } + // Execute sqlite init_commands. if (isset($connection_options['init_commands'])) { $this->exec(implode('; ', $connection_options['init_commands'])); @@ -128,10 +140,10 @@ class DatabaseConnection_sqlite extends DatabaseConnection { $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField(); // We can prune the database file if it doesn't have any tables. - if ($count == 0) { - // Detach the database. - $this->query('DETACH DATABASE :schema', array(':schema' => $prefix)); - // Destroy the database file. + if ($count == 0 && $this->connectionOptions['database'] != ':memory:') { + // Detaching the database fails at this point, but no other queries + // are executed after the connection is destructed so we can simply + // remove the database file. unlink($this->connectionOptions['database'] . '-' . $prefix); } } @@ -143,6 +155,18 @@ class DatabaseConnection_sqlite extends DatabaseConnection { } } + /** + * Gets all the attached databases. + * + * @return array + * An array of attached database names. + * + * @see DatabaseConnection_sqlite::__construct(). + */ + public function getAttachedDatabases() { + return $this->attachedDatabases; + } + /** * SQLite compatibility implementation for the IF() SQL function. */ diff --git a/includes/database/sqlite/query.inc b/includes/database/sqlite/query.inc index c9c028bb0..45c6a3024 100644 --- a/includes/database/sqlite/query.inc +++ b/includes/database/sqlite/query.inc @@ -23,7 +23,7 @@ class InsertQuery_sqlite extends InsertQuery { if (!$this->preExecute()) { return NULL; } - if (count($this->insertFields)) { + if (count($this->insertFields) || !empty($this->fromQuery)) { return parent::execute(); } else { @@ -36,7 +36,10 @@ class InsertQuery_sqlite extends InsertQuery { $comments = $this->connection->makeComment($this->comments); // Produce as many generic placeholders as necessary. - $placeholders = array_fill(0, count($this->insertFields), '?'); + $placeholders = array(); + if (!empty($this->insertFields)) { + $placeholders = array_fill(0, count($this->insertFields), '?'); + } // If we're selecting from a SelectQuery, finish building the query and // pass it back, as any remaining options are irrelevant. diff --git a/includes/database/sqlite/schema.inc b/includes/database/sqlite/schema.inc index 281d8fc6b..43ea6d61c 100644 --- a/includes/database/sqlite/schema.inc +++ b/includes/database/sqlite/schema.inc @@ -668,6 +668,9 @@ class DatabaseSchema_sqlite extends DatabaseSchema { $this->alterTable($table, $old_schema, $new_schema); } + /** + * {@inheritdoc} + */ public function findTables($table_expression) { // Don't add the prefix, $table_expression already includes the prefix. $info = $this->getPrefixInfo($table_expression, FALSE); @@ -680,4 +683,32 @@ class DatabaseSchema_sqlite extends DatabaseSchema { )); return $result->fetchAllKeyed(0, 0); } + + /** + * {@inheritdoc} + */ + public function findTablesD8($table_expression) { + $tables = array(); + + // The SQLite implementation doesn't need to use the same filtering strategy + // as the parent one because individually prefixed tables live in their own + // schema (database), which means that neither the main database nor any + // attached one will contain a prefixed table name, so we just need to loop + // over all known schemas and filter by the user-supplied table expression. + $attached_dbs = $this->connection->getAttachedDatabases(); + foreach ($attached_dbs as $schema) { + // Can't use query placeholders for the schema because the query would + // have to be :prefixsqlite_master, which does not work. We also need to + // ignore the internal SQLite tables. + $result = db_query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", array( + ':type' => 'table', + ':table_name' => $table_expression, + ':pattern' => 'sqlite_%', + )); + $tables += $result->fetchAllKeyed(0, 0); + } + + return $tables; + } + } diff --git a/includes/form.inc b/includes/form.inc index 1158fd031..f2557e832 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -1361,7 +1361,10 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) { // The following errors are always shown. if (isset($elements['#needs_validation'])) { // Verify that the value is not longer than #maxlength. - if (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#maxlength']) { + if (isset($elements['#maxlength']) && (isset($elements['#value']) && !is_scalar($elements['#value']))) { + form_error($elements, $t('An illegal value has been detected. Please contact the site administrator.')); + } + elseif (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#maxlength']) { form_error($elements, $t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => drupal_strlen($elements['#value'])))); } @@ -4124,9 +4127,17 @@ function form_process_weight($element) { $max_elements = variable_get('drupal_weight_select_max', DRUPAL_WEIGHT_SELECT_MAX); if ($element['#delta'] <= $max_elements) { $element['#type'] = 'select'; + $weights = array(); for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { $weights[$n] = $n; } + if (isset($element['#default_value'])) { + $default_value = (int) $element['#default_value']; + if (!isset($weights[$default_value])) { + $weights[$default_value] = $default_value; + ksort($weights); + } + } $element['#options'] = $weights; $element += element_info('select'); } diff --git a/includes/mail.inc b/includes/mail.inc index 0e5c17804..a97c788f0 100644 --- a/includes/mail.inc +++ b/includes/mail.inc @@ -12,6 +12,12 @@ */ define('MAIL_LINE_ENDINGS', isset($_SERVER['WINDIR']) || (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE) ? "\r\n" : "\n"); + +/** + * Special characters, defined in RFC_2822. + */ +define('MAIL_RFC_2822_SPECIALS', '()<>[]:;@\,."'); + /** * Composes and optionally sends an e-mail message. * @@ -148,8 +154,13 @@ function drupal_mail($module, $key, $to, $language, $params = array(), $from = N // Return-Path headers should have a domain authorized to use the originating // SMTP server. $headers['From'] = $headers['Sender'] = $headers['Return-Path'] = $default_from; + + if (variable_get('mail_display_name_site_name', FALSE)) { + $display_name = variable_get('site_name', 'Drupal'); + $headers['From'] = drupal_mail_format_display_name($display_name) . ' <' . $default_from . '>'; + } } - if ($from) { + if ($from && $from != $default_from) { $headers['From'] = $from; } $message['headers'] = $headers; @@ -557,10 +568,59 @@ function drupal_html_to_text($string, $allowed_tags = NULL) { return $output . $footnotes; } +/** + * Return a RFC-2822 compliant "display-name" component. + * + * The "display-name" component is used in mail header "Originator" fields + * (From, Sender, Reply-to) to give a human-friendly description of the + * address, i.e. From: My Display Name . RFC-822 and + * RFC-2822 define its syntax and rules. This method gets as input a string + * to be used as "display-name" and formats it to be RFC compliant. + * + * @param string $string + * A string to be used as "display-name". + * + * @return string + * A RFC compliant version of the string, ready to be used as + * "display-name" in mail originator header fields. + */ +function drupal_mail_format_display_name($string) { + // Make sure we don't process html-encoded characters. They may create + // unneeded trouble if left encoded, besides they will be correctly + // processed if decoded. + $string = decode_entities($string); + + // If string contains non-ASCII characters it must be (short) encoded + // according to RFC-2047. The output of a "B" (Base64) encoded-word is + // always safe to be used as display-name. + $safe_display_name = mime_header_encode($string, TRUE); + + // Encoded-words are always safe to be used as display-name because don't + // contain any RFC 2822 "specials" characters. However + // mimeHeaderEncode() encodes a string only if it contains any + // non-ASCII characters, and leaves its value untouched (un-encoded) if + // ASCII only. For this reason in order to produce a valid display-name we + // still need to make sure there are no "specials" characters left. + if (preg_match('/[' . preg_quote(MAIL_RFC_2822_SPECIALS) . ']/', $safe_display_name)) { + + // If string is already quoted, it may or may not be escaped properly, so + // don't trust it and reset. + if (preg_match('/^"(.+)"$/', $safe_display_name, $matches)) { + $safe_display_name = str_replace(array('\\\\', '\\"'), array('\\', '"'), $matches[1]); + } + + // Transform the string in a RFC-2822 "quoted-string" by wrapping it in + // double-quotes. Also make sure '"' and '\' occurrences are escaped. + $safe_display_name = '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $safe_display_name) . '"'; + } + + return $safe_display_name; +} + /** * Wraps words on a single line. * - * Callback for array_walk() winthin drupal_wrap_mail(). + * Callback for array_walk() within drupal_wrap_mail(). */ function _drupal_wrap_mail_line(&$line, $key, $values) { // Use soft-breaks only for purely quoted or unindented text. diff --git a/includes/menu.inc b/includes/menu.inc index 2b489d886..22e6dba97 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -1067,7 +1067,7 @@ function menu_tree_output($tree) { // the active class accordingly. But local tasks do not appear in menu // trees, so if the current path is a local task, and this link is its // tab root, then we have to set the class manually. - if ($data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != $_GET['q']) { + if ($router_item && $data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != $_GET['q']) { $data['link']['localized_options']['attributes']['class'][] = 'active'; } diff --git a/misc/jquery.js b/misc/jquery.js index e900c19a3..8f3ca2e2d 100644 --- a/misc/jquery.js +++ b/misc/jquery.js @@ -1,4 +1,3 @@ - /*! * jQuery JavaScript Library v1.4.4 * http://jquery.com/ 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 7c88ac776..b2eb50652 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.test +++ b/modules/field/modules/field_sql_storage/field_sql_storage.test @@ -313,9 +313,13 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase { $field = array('field_name' => 'test_text', 'type' => 'text', 'settings' => array('max_length' => 255)); $field = field_create_field($field); - // Attempt to update the field in a way that would break the storage. + // Attempt to update the field in a way that would break the storage. The + // parenthesis suffix is needed because SQLite has *very* relaxed rules for + // data types, so we actually need to provide an invalid SQL syntax in order + // to break it. + // @see https://www.sqlite.org/datatype3.html $prior_field = $field; - $field['settings']['max_length'] = -1; + $field['settings']['max_length'] = '-1)'; try { field_update_field($field); $this->fail(t('Update succeeded.')); diff --git a/modules/file/file.module b/modules/file/file.module index eea58470f..1f1d59447 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -281,10 +281,11 @@ function file_ajax_upload() { } // Otherwise just add the new content class on a placeholder. else { - $form['#suffix'] .= ''; + $form['#suffix'] = (isset($form['#suffix']) ? $form['#suffix'] : '') . ''; } - $form['#prefix'] .= theme('status_messages'); + $form['#prefix'] = (isset($form['#prefix']) ? $form['#prefix'] : '') . theme('status_messages'); + $output = drupal_render($form); $js = drupal_add_js(); $settings = drupal_array_merge_deep_array($js['settings']['data']); diff --git a/modules/locale/locale.module b/modules/locale/locale.module index 768fead67..93a4657f0 100644 --- a/modules/locale/locale.module +++ b/modules/locale/locale.module @@ -564,6 +564,7 @@ function locale_language_types_info() { * Implements hook_language_negotiation_info(). */ function locale_language_negotiation_info() { + require_once DRUPAL_ROOT . '/includes/locale.inc'; $file = 'includes/locale.inc'; $providers = array(); diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index a0872c234..c426ba53a 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1677,15 +1677,12 @@ protected function tearDown() { file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); // Remove all prefixed tables. - $tables = db_find_tables($this->databasePrefix . '%'); - $connection_info = Database::getConnectionInfo('default'); - $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%'); + $tables = db_find_tables_d8('%'); if (empty($tables)) { $this->fail('Failed to find test tables to drop.'); } - $prefix_length = strlen($connection_info['default']['prefix']['default']); foreach ($tables as $table) { - if (db_drop_table(substr($table, $prefix_length))) { + if (db_drop_table($table)) { unset($tables[$table]); } } diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index cf8304781..6a60d5949 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -563,7 +563,7 @@ function simpletest_clean_environment() { * Removed prefixed tables from the database that are left over from crashed tests. */ function simpletest_clean_database() { - $tables = db_find_tables(Database::getConnection()->prefixTables('{simpletest}') . '%'); + $tables = db_find_tables_d8(Database::getConnection()->prefixTables('{simpletest}') . '%'); $schema = drupal_get_schema_unprocessed('simpletest'); $count = 0; foreach (array_diff_key($tables, $schema) as $table) { diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index bd774dc19..6145546f7 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -2051,6 +2051,32 @@ class DrupalRenderTestCase extends DrupalWebTestCase { // The elements should appear in output in the same order as the array. $this->assertTrue(strpos($output, $second) < strpos($output, $first), 'Elements were not sorted.'); + + // The order of children with same weight should be preserved. + $element_mixed_weight = array( + 'child5' => array('#weight' => 10), + 'child3' => array('#weight' => -10), + 'child1' => array(), + 'child4' => array('#weight' => 10), + 'child2' => array(), + 'child6' => array('#weight' => 10), + 'child9' => array(), + 'child8' => array('#weight' => 10), + 'child7' => array(), + ); + + $expected = array( + 'child3', + 'child1', + 'child2', + 'child9', + 'child7', + 'child5', + 'child4', + 'child6', + 'child8', + ); + $this->assertEqual($expected, element_children($element_mixed_weight, TRUE), 'Order of elements with the same weight is preserved.'); } /** diff --git a/modules/simpletest/tests/database_test.install b/modules/simpletest/tests/database_test.install index 11361151f..44ed5ee0a 100644 --- a/modules/simpletest/tests/database_test.install +++ b/modules/simpletest/tests/database_test.install @@ -217,5 +217,24 @@ function database_test_schema() { ), ); + $schema['virtual'] = array( + 'description' => 'Basic test table with a reserved name.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'function' => array( + 'description' => "A column with a reserved name.", + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + 'default' => '', + ), + ), + 'primary key' => array('id'), + ); + return $schema; } diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index 59d2e5d62..04be5c85b 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -163,6 +163,12 @@ class DatabaseTestCase extends DrupalWebTestCase { 'priority' => 3, )) ->execute(); + + db_insert('virtual') + ->fields(array( + 'function' => 'Function value 1', + )) + ->execute(); } } @@ -3457,7 +3463,6 @@ class DatabaseQueryTestCase extends DatabaseTestCase { ->fetchField(); $this->assertFalse($result, 'SQL injection attempt did not result in a row being inserted in the database table.'); } - } /** @@ -4033,6 +4038,8 @@ class ConnectionUnitTest extends DrupalUnitTestCase { protected $monitor; protected $originalCount; + protected $skipTest; + public static function getInfo() { return array( 'name' => 'Connection unit tests', @@ -4053,7 +4060,7 @@ class ConnectionUnitTest extends DrupalUnitTestCase { // @todo Make this test driver-agnostic, or find a proper way to skip it. // @see http://drupal.org/node/1273478 $connection_info = Database::getConnectionInfo('default'); - $this->skipTest = (bool) $connection_info['default']['driver'] != 'mysql'; + $this->skipTest = (bool) ($connection_info['default']['driver'] != 'mysql'); if ($this->skipTest) { // Insert an assertion to prevent Simpletest from interpreting the test // as failure. @@ -4238,5 +4245,178 @@ class ConnectionUnitTest extends DrupalUnitTestCase { // Verify that we are back to the original connection count. $this->assertNoConnection($id); } +} + +/** + * Test reserved keyword handling (introduced for MySQL 8+) +*/ +class DatabaseReservedKeywordTestCase extends DatabaseTestCase { + public static function getInfo() { + return array( + 'name' => 'Reserved Keywords', + 'description' => 'Test handling of reserved keywords.', + 'group' => 'Database', + ); + } + + function setUp() { + parent::setUp('database_test'); + } + + public function testTableNameQuoting() { + // Test db_query with {table} pattern. + $record = db_query('SELECT * FROM {system} LIMIT 1')->fetchObject(); + $this->assertTrue(isset($record->filename), 'Successfully queried the {system} table.'); + + $connection = Database::getConnection()->getConnectionOptions(); + if ($connection['driver'] === 'sqlite') { + // In SQLite simpletest's prefixed db tables exist in their own schema + // (e.g. simpletest124904.system), so we cannot test the schema.{table} + // syntax here as the table name will have the schema name prepended to it + // when prefixes are processed. + $this->assert(TRUE, 'Skipping schema.{system} test for SQLite.'); + } + else { + $database = $connection['database']; + // Test db_query with schema.{table} pattern + db_query('SELECT * FROM ' . $database . '.{system} LIMIT 1')->fetchObject(); + $this->assertTrue(isset($record->filename), 'Successfully queried the schema.{system} table.'); + } + } + + public function testSelectReservedWordTableCount() { + $rows = db_select('virtual') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($rows, 1, 'Successful count query on a table with a reserved name.'); + } + + public function testSelectReservedWordTableSpecificField() { + $record = db_select('virtual') + ->fields('virtual', array('function')) + ->execute() + ->fetchAssoc(); + $this->assertEqual($record['function'], 'Function value 1', 'Successfully read a field from a table with a name and column which are reserved words.'); + } + + public function testSelectReservedWordTableAllFields() { + $record = db_select('virtual') + ->fields('virtual') + ->execute() + ->fetchAssoc(); + $this->assertEqual($record['function'], 'Function value 1', 'Successful all_fields query from a table with a name and column which are reserved words.'); + } + + public function testSelectReservedWordAliasCount() { + $rows = db_select('test', 'character') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($rows, 4, 'Successful count query using an alias which is a reserved word.'); + } + public function testSelectReservedWordAliasSpecificFields() { + $record = db_select('test', 'high_priority') + ->fields('high_priority', array('name')) + ->condition('age', 27) + ->execute()->fetchAssoc(); + $this->assertEqual($record['name'], 'George', 'Successful query using an alias which is a reserved word.'); + } + + public function testSelectReservedWordAliasAllFields() { + $record = db_select('test', 'high_priority') + ->fields('high_priority') + ->condition('age', 27) + ->execute()->fetchAssoc(); + $this->assertEqual($record['name'], 'George', 'Successful all_fields query using an alias which is a reserved word.'); + } + + public function testInsertReservedWordTable() { + $num_records_before = db_query('SELECT COUNT(*) FROM {virtual}')->fetchField(); + db_insert('virtual') + ->fields(array( + 'function' => 'Inserted function', + )) + ->execute(); + $num_records_after = db_query('SELECT COUNT(*) FROM {virtual}')->fetchField(); + $this->assertIdentical($num_records_before + 1, (int) $num_records_after, 'Successful insert into a table with a name and column which are reserved words.'); + } + + public function testDeleteReservedWordTable() { + $delete = db_delete('virtual') + ->condition('function', 'Function value 1'); + $num_deleted = $delete->execute(); + $this->assertEqual($num_deleted, 1, "Deleted 1 record from a table with a name and column which are reserved words.."); + } + + function testTruncateReservedWordTable() { + db_truncate('virtual')->execute(); + $num_records_after = db_query("SELECT COUNT(*) FROM {virtual}")->fetchField(); + $this->assertEqual(0, $num_records_after, 'Truncated a table with a reserved name.'); + } + + function testUpdateReservedWordTable() { + $num_updated = db_update('virtual') + ->fields(array('function' => 'Updated function')) + ->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record in a table with a name and column which are reserved words.'); + } + + function testMergeReservedWordTable() { + $key = db_query('SELECT id FROM {virtual} LIMIT 1')->fetchField(); + $num_records_before = db_query('SELECT COUNT(*) FROM {virtual}')->fetchField(); + db_merge('virtual') + ->key(array('id' => $key)) + ->fields(array('function' => 'Merged function')) + ->execute(); + $num_records_after = db_query('SELECT COUNT(*) FROM {virtual}')->fetchField(); + $this->assertIdentical($num_records_before, $num_records_after, 'Successful merge query on a table with a name and column which are reserved words.'); + } +} + +/** + * Test table prefix handling. +*/ +class DatabaseTablePrefixTestCase extends DatabaseTestCase { + public static function getInfo() { + return array( + 'name' => 'Table prefixes', + 'description' => 'Test handling of table prefixes.', + 'group' => 'Database', + ); + } + + public function testSchemaDotTablePrefixes() { + // Get a copy of the default connection options. + $db = Database::getConnection('default', 'default'); + $connection_options = $db->getConnectionOptions(); + + if ($connection_options['driver'] === 'sqlite') { + // In SQLite simpletest's prefixed db tables exist in their own schema + // (e.g. simpletest124904.system), so we cannot test the schema.table + // prefix syntax here. + $this->assert(TRUE, 'Skipping schema.table prefixed tables test for SQLite.'); + return; + } + + $db_name = $connection_options['database']; + // This prefix is usually something like simpletest12345 + $test_prefix = $connection_options['prefix']['default']; + + // Set up a new connection with table prefixes in the form "schema.table" + $prefixed = $connection_options; + $prefixed['prefix'] = array( + 'default' => $test_prefix, + 'users' => $db_name . '.' . $test_prefix, + 'role' => $db_name . '.' . $test_prefix, + ); + Database::addConnectionInfo('default', 'prefixed', $prefixed); + + // Test that the prefixed database connection can query the prefixed tables. + $num_users_prefixed = Database::getConnection('prefixed', 'default')->query('SELECT COUNT(1) FROM {users}')->fetchField(); + $this->assertTrue((int) $num_users_prefixed > 0, 'Successfully queried the users table using a schema.table prefix'); + $num_users_default = Database::getConnection('default', 'default')->query('SELECT COUNT(1) FROM {users}')->fetchField(); + $this->assertEqual($num_users_default, $num_users_prefixed, 'Verified results of query using a connection with schema.table prefixed tables'); + } } diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index d1be69d72..49b561a63 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -591,6 +591,19 @@ class FormElementTestCase extends DrupalWebTestCase { ))); } } + + /** + * Tests Weight form element #default_value behavior. + */ + public function testWeightDefaultValue() { + $element = array( + '#type' => 'weight', + '#delta' => 10, + '#default_value' => 15, + ); + $element = form_process_weight($element); + $this->assertTrue(isset($element['#options'][$element['#default_value']]), 'Default value exists in #options list'); + } } /** diff --git a/modules/simpletest/tests/mail.test b/modules/simpletest/tests/mail.test index 3e40e13a8..307c77b29 100644 --- a/modules/simpletest/tests/mail.test +++ b/modules/simpletest/tests/mail.test @@ -59,6 +59,81 @@ class MailTestCase extends DrupalWebTestCase implements MailSystemInterface { $this->assertNull(self::$sent_message, 'Message was canceled.'); } + /** + * Checks for the site name in an auto-generated From: header. + */ + function testFromHeader() { + global $language; + $default_from = variable_get('site_mail', ini_get('sendmail_from')); + $site_name = variable_get('site_name', 'Drupal'); + + // Reset the class variable holding a copy of the last sent message. + self::$sent_message = NULL; + // Send an e-mail with a sender address specified. + $from_email = 'someone_else@example.com'; + $message = drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language, array(), $from_email); + // Test that the from e-mail is just the e-mail and not the site name and + // default sender e-mail. + $this->assertEqual($from_email, self::$sent_message['headers']['From']); + + // Check default behavior is only email in FROM header. + self::$sent_message = NULL; + // Send an e-mail and check that the From-header contains only default mail address. + variable_del('mail_display_name_site_name'); + $message = drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language); + $this->assertEqual($default_from, self::$sent_message['headers']['From']); + + self::$sent_message = NULL; + // Send an e-mail and check that the From-header contains the site name. + variable_set('mail_display_name_site_name', TRUE); + $message = drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language); + $this->assertEqual($site_name . ' <' . $default_from . '>', self::$sent_message['headers']['From']); + } + + /** + * Checks for the site name in an auto-generated From: header. + */ + function testFromHeaderRfc2822Compliant() { + global $language; + $default_from = variable_get('site_mail', ini_get('sendmail_from')); + + // Enable adding a site name to From. + variable_set('mail_display_name_site_name', TRUE); + + $site_names = array( + // Simple ASCII characters. + 'Test site' => 'Test site', + // ASCII with html entity. + 'Test & site' => 'Test & site', + // Non-ASCII characters. + 'Tést site' => '=?UTF-8?B?VMOpc3Qgc2l0ZQ==?=', + // Non-ASCII with special characters. + 'Tést; site' => '=?UTF-8?B?VMOpc3Q7IHNpdGU=?=', + // Non-ASCII with html entity. + 'Tést; site' => '=?UTF-8?B?VMOpc3Q7IHNpdGU=?=', + // ASCII with special characters. + 'Test; site' => '"Test; site"', + // ASCII with special characters as html entity. + 'Test < site' => '"Test < site"', + // ASCII with special characters and '\'. + 'Test; \ "site"' => '"Test; \\\\ \"site\""', + // String already RFC-2822 compliant. + '"Test; site"' => '"Test; site"', + // String already RFC-2822 compliant. + '"Test; \\\\ \"site\""' => '"Test; \\\\ \"site\""', + ); + + foreach ($site_names as $original_name => $safe_string) { + variable_set('site_name', $original_name); + + // Reset the class variable holding a copy of the last sent message. + self::$sent_message = NULL; + // Send an e-mail and check that the From-header contains is RFC-2822 compliant. + drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language); + $this->assertEqual($safe_string . ' <' . $default_from . '>', self::$sent_message['headers']['From']); + } + } + /** * Concatenate and wrap the e-mail body for plain-text mails. * diff --git a/modules/simpletest/tests/schema.test b/modules/simpletest/tests/schema.test index 41994284e..070b2911e 100644 --- a/modules/simpletest/tests/schema.test +++ b/modules/simpletest/tests/schema.test @@ -381,4 +381,60 @@ class SchemaTestCase extends DrupalWebTestCase { db_drop_field($table_name, $field_name); } + + /** + * Tests the findTables() method. + */ + public function testFindTables() { + // We will be testing with three tables, two of them using the default + // prefix and the third one with an individually specified prefix. + + // Set up a new connection with different connection info. + $connection_info = Database::getConnectionInfo(); + + // Add per-table prefix to the second table. + $new_connection_info = $connection_info['default']; + $new_connection_info['prefix']['test_2_table'] = $new_connection_info['prefix']['default'] . '_shared_'; + Database::addConnectionInfo('test', 'default', $new_connection_info); + + Database::setActiveConnection('test'); + + // Create the tables. + $table_specification = array( + 'description' => 'Test table.', + 'fields' => array( + 'id' => array( + 'type' => 'int', + 'default' => NULL, + ), + ), + ); + Database::getConnection()->schema()->createTable('test_1_table', $table_specification); + Database::getConnection()->schema()->createTable('test_2_table', $table_specification); + Database::getConnection()->schema()->createTable('the_third_table', $table_specification); + + // Check the "all tables" syntax. + $tables = Database::getConnection()->schema()->findTablesD8('%'); + sort($tables); + $expected = array( + 'test_1_table', + // This table uses a per-table prefix, yet it is returned as un-prefixed. + 'test_2_table', + 'the_third_table', + ); + + $this->assertTrue(!array_diff($expected, $tables), 'All tables were found.'); + + // Check the restrictive syntax. + $tables = Database::getConnection()->schema()->findTablesD8('test_%'); + sort($tables); + $expected = array( + 'test_1_table', + 'test_2_table', + ); + $this->assertEqual($tables, $expected, 'Two tables were found.'); + + // Go back to the initial connection. + Database::setActiveConnection('default'); + } } diff --git a/modules/system/system.test b/modules/system/system.test index 270311ecb..45c6648c4 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -28,7 +28,7 @@ class ModuleTestCase extends DrupalWebTestCase { * specified base table. Defaults to TRUE. */ function assertTableCount($base_table, $count = TRUE) { - $tables = db_find_tables(Database::getConnection()->prefixTables('{' . $base_table . '}') . '%'); + $tables = db_find_tables_d8($base_table . '%'); if ($count) { return $this->assertTrue($tables, format_string('Tables matching "@base_table" found.', array('@base_table' => $base_table))); @@ -779,14 +779,14 @@ class IPAddressBlockingTestCase extends DrupalWebTestCase { $submit_ip = $_SERVER['REMOTE_ADDR'] = '192.168.1.1'; system_block_ip_action(); system_block_ip_action(); - $ip_count = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->rowCount(); + $ip_count = db_query("SELECT COUNT(*) from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->fetchColumn(); $this->assertEqual('1', $ip_count); drupal_static_reset('ip_address'); $submit_ip = $_SERVER['REMOTE_ADDR'] = ' '; system_block_ip_action(); system_block_ip_action(); system_block_ip_action(); - $ip_count = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->rowCount(); + $ip_count = db_query("SELECT COUNT(*) from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->fetchColumn(); $this->assertEqual('1', $ip_count); } } diff --git a/modules/user/tests/user_flood_test.info b/modules/user/tests/user_flood_test.info new file mode 100644 index 000000000..909997a77 --- /dev/null +++ b/modules/user/tests/user_flood_test.info @@ -0,0 +1,6 @@ +name = "User module flood control tests" +description = "Support module for user flood control testing." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE diff --git a/modules/user/tests/user_flood_test.module b/modules/user/tests/user_flood_test.module new file mode 100644 index 000000000..f7388690f --- /dev/null +++ b/modules/user/tests/user_flood_test.module @@ -0,0 +1,18 @@ + $username, '%ip' => $ip)); + } + else { + watchdog('user_flood_test', 'hook_user_flood_control was passed IP %ip.', array('%ip' => $ip)); + } +} diff --git a/modules/user/tests/user_form_test.module b/modules/user/tests/user_form_test.module index 382bc57b8..2af15cb83 100644 --- a/modules/user/tests/user_form_test.module +++ b/modules/user/tests/user_form_test.module @@ -35,7 +35,7 @@ function user_form_test_current_password($form, &$form_state, $account) { '#description' => t('A field that would require a correct password to change.'), '#required' => TRUE, ); - + $form['current_pass'] = array( '#type' => 'password', '#title' => t('Current password'), diff --git a/modules/user/user.api.php b/modules/user/user.api.php index f205a85b5..b9dc95f15 100644 --- a/modules/user/user.api.php +++ b/modules/user/user.api.php @@ -472,6 +472,36 @@ function hook_user_role_delete($role) { ->execute(); } +/** + * Respond to user flood control events. + * + * This hook allows you act when an unsuccessful user login has triggered + * flood control. This means that either an IP address or a specific user + * account has been temporarily blocked from logging in. + * + * @param $ip + * The IP address that triggered flood control. + * @param $username + * The username that has been temporarily blocked. + * + * @see user_login_final_validate() + */ +function hook_user_flood_control($ip, $username = FALSE) { + if (!empty($username)) { + // Do something with the blocked $username and $ip. For example, send an + // e-mail to the user and/or site administrator. + + // Drupal core uses this hook to log the event: + watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array('%user' => $username, '%ip' => $ip)); + } + else { + // Do something with the blocked $ip. For example, add it to a block-list. + + // Drupal core uses this hook to log the event: + watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip)); + } +} + /** * @} End of "addtogroup hooks". */ diff --git a/modules/user/user.module b/modules/user/user.module index 2309aa929..dfa05978c 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -2225,11 +2225,17 @@ function user_login_final_validate($form, &$form_state) { if (isset($form_state['flood_control_triggered'])) { if ($form_state['flood_control_triggered'] == 'user') { form_set_error('name', format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or request a new password.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); + module_invoke_all('user_flood_control', ip_address(), $form_state['values']['name']); } else { // We did not find a uid, so the limit is IP-based. form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); + module_invoke_all('user_flood_control', ip_address()); } + // We cannot call drupal_access_denied() here as that can result in an + // infinite loop if the login form is rendered on the 403 page (e.g. in a + // block). So add the 403 header and allow form processing to finish. + drupal_add_http_header('Status', '403 Forbidden'); } else { // Use $form_state['input']['name'] here to guarantee that we send @@ -2247,6 +2253,23 @@ function user_login_final_validate($form, &$form_state) { } } +/** + * Implements hook_user_flood_control(). + */ +function user_user_flood_control($ip, $username = FALSE) { + if (variable_get('log_user_flood_control', TRUE)) { + if (!empty($username)) { + watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array( + '%user' => $username, + '%ip' => $ip + )); + } + else { + watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip)); + } + } +} + /** * Try to validate the user's login credentials locally. * diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc index 2a1b291b1..6f997a62e 100644 --- a/modules/user/user.pages.inc +++ b/modules/user/user.pages.inc @@ -66,6 +66,22 @@ function user_pass() { * @see user_pass_submit() */ function user_pass_validate($form, &$form_state) { + if (isset($form_state['values']['name']) && !is_scalar($form_state['values']['name'])) { + form_set_error('name', t('An illegal value has been detected. Please contact the site administrator.')); + return; + } + $user_pass_reset_ip_window = variable_get('user_pass_reset_ip_window', 3600); + // Do not allow any password reset from the current user's IP if the limit + // has been reached. Default is 50 attempts allowed in one hour. This is + // independent of the per-user limit to catch attempts from one IP to request + // resets for many different user accounts. We have a reasonably high limit + // since there may be only one apparent IP for all users at an institution. + if (!flood_is_allowed('pass_reset_ip', variable_get('user_pass_reset_ip_limit', 50), $user_pass_reset_ip_window)) { + form_set_error('name', t('Sorry, too many password reset attempts from your IP address. This IP address is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); + return; + } + // Always register an per-IP event. + flood_register_event('pass_reset_ip', $user_pass_reset_ip_window); $name = trim($form_state['values']['name']); // Try to load by email. $users = user_load_multiple(array(), array('mail' => $name, 'status' => '1')); @@ -76,6 +92,19 @@ function user_pass_validate($form, &$form_state) { $account = reset($users); } if (isset($account->uid)) { + // Register user flood events based on the uid only, so they can be cleared + // when a password is reset successfully. + $identifier = $account->uid; + $user_pass_reset_user_window = variable_get('user_pass_reset_user_window', 21600); + $user_pass_reset_user_limit = variable_get('user_pass_reset_user_limit', 5); + // Don't allow password reset if the limit for this user has been reached. + // Default is to allow 5 passwords resets every 6 hours. + if (!flood_is_allowed('pass_reset_user', $user_pass_reset_user_limit, $user_pass_reset_user_window, $identifier)) { + form_set_error('name', format_plural($user_pass_reset_user_limit, 'Sorry, there has been more than one password reset attempt for this account. It is temporarily blocked. Try again later or login with your password.', 'Sorry, there have been more than @count password reset attempts for this account. It is temporarily blocked. Try again later or login with your password.', array('@url' => url('user/login')))); + return; + } + // Register a per-user event. + flood_register_event('pass_reset_user', $user_pass_reset_user_window, $identifier); form_set_value(array('#parents' => array('account')), $account, $form_state); } else { @@ -161,6 +190,8 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a // user_login_finalize() also updates the login timestamp of the // user, which invalidates further use of the one-time login link. user_login_finalize(); + // Clear any password reset flood events for this user. + flood_clear_event('pass_reset_user', $account->uid); watchdog('user', 'User %name used one-time login link at time %timestamp.', array('%name' => $account->name, '%timestamp' => $timestamp)); drupal_set_message(t('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.')); // Let the user's password be changed without the current password check. diff --git a/modules/user/user.test b/modules/user/user.test index 835154b25..4c16b531c 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -322,7 +322,7 @@ class UserLoginTestCase extends DrupalWebTestCase { } function setUp() { - parent::setUp('user_session_test'); + parent::setUp('user_session_test', 'user_flood_test'); } /** @@ -453,12 +453,19 @@ class UserLoginTestCase extends DrupalWebTestCase { $this->drupalPost('user', $edit, t('Log in')); $this->assertNoFieldByXPath("//input[@name='pass' and @value!='']", NULL, 'Password value attribute is blank.'); if (isset($flood_trigger)) { + $this->assertResponse(403); + $user_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user'))->fetchField(); + $user_flood_test_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user_flood_test'))->fetchField(); if ($flood_trigger == 'user') { - $this->assertRaw(format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or request a new password.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); + $this->assertRaw(t('Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password'), '@count' => variable_get('user_failed_login_user_limit', 5)))); + $this->assertEqual('Flood control blocked login attempt for %user from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per user'); + $this->assertEqual('hook_user_flood_control was passed username %username and IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per user'); } else { // No uid, so the limit is IP-based. $this->assertRaw(t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); + $this->assertEqual('Flood control blocked login attempt from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per IP'); + $this->assertEqual('hook_user_flood_control was passed IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per IP'); } } else { @@ -507,6 +514,8 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $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 single password reset.'); // Create an image field to enable an Ajax request on the user profile page. $field = array( @@ -552,6 +561,84 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.'); } + /** + * 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.'); + } + /** * Test user password reset while logged in. */ diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 8c0be40ef..f5c4a144f 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -264,6 +264,10 @@ function simpletest_script_init($server_software) { // '_' is an environment variable set by the shell. It contains the command that was executed. $php = $php_env; } + elseif (defined('PHP_BINARY') && $php_env = PHP_BINARY) { + // 'PHP_BINARY' specifies the PHP binary path during script execution. Available since PHP 5.4. + $php = $php_env; + } elseif ($sudo = getenv('SUDO_COMMAND')) { // 'SUDO_COMMAND' is an environment variable set by the sudo program. // Extract only the PHP interpreter, not the rest of the command. diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index a34596b17..bf367b208 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -246,6 +246,24 @@ */ $databases = array(); +/** + * Quoting of identifiers in MySQL. + * + * To allow compatibility with newer versions of MySQL, Drupal will quote table + * names and some other identifiers. The ANSI standard character for identifier + * quoting is the double quote (") and that can be used by MySQL along with the + * sql_mode setting of ANSI_QUOTES. However, MySQL's own default is to use + * backticks (`). Drupal 7 uses backticks for compatibility. If you need to + * change this, you can do so with this variable. It's possible to switch off + * identifier quoting altogether by setting this variable to an empty string. + * + * @see https://www.drupal.org/project/drupal/issues/2978575 + * @see https://dev.mysql.com/doc/refman/8.0/en/identifiers.html + * @see \DatabaseConnection_mysql::setPrefix + * @see \DatabaseConnection_mysql::quoteIdentifier + */ +# $conf['mysql_identifier_quote_character'] = '"'; + /** * Access control for update.php script. * @@ -659,3 +677,42 @@ 'node_modules', 'bower_components', ); + +/** + * Logging of user flood control events. + * + * Drupal's user module will place a temporary block on a given IP address or + * user account if there are excessive failed login attempts. By default these + * flood control events will be logged. This can be useful for identifying + * brute force login attacks. Set this variable to FALSE to disable logging, for + * example if you are using the dblog module and want to avoid database writes. + * + * @see user_login_final_validate() + * @see user_user_flood_control() + */ +# $conf['log_user_flood_control'] = FALSE; + +/** + * Opt out of variable_initialize() locking optimization. + * + * After lengthy discussion in https://www.drupal.org/node/973436 a change was + * made in variable_initialize() in order to avoid excessive waiting under + * certain conditions. Set this variable to TRUE in order to opt out of this + * optimization and revert to the original behaviour. + */ +# $conf['variable_initialize_wait_for_lock'] = FALSE; + +/** + * Use site name as display-name in outgoing mail. + * + * Drupal can use the site name (i.e. the value of the site_name variable) as + * the display-name when sending e-mail. For example this would mean the sender + * might be "Acme Website" as opposed to just the e-mail + * address alone. In order to avoid disruption this is not enabled by default + * for existing sites. The feature can be enabled by setting this variable to + * TRUE. + * + * @see https://tools.ietf.org/html/rfc2822 + * @see drupal_mail() + */ +$conf['mail_display_name_site_name'] = TRUE;