Skip to content
......@@ -12,6 +12,13 @@
class DatabaseSchema_pgsql extends DatabaseSchema {
/**
* PostgreSQL's temporary namespace name.
*
* @var string
*/
protected $tempNamespaceName;
/**
* A cache of information about blob columns and sequences of tables.
*
......@@ -23,6 +30,64 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
*/
protected $tableInformation = array();
/**
* The maximum allowed length for index, primary key and constraint names.
*
* Value will usually be set to a 63 chars limit but PostgreSQL allows
* to higher this value before compiling, so we need to check for that.
*
* @var int
*/
protected $maxIdentifierLength;
/**
* Make sure to limit identifiers according to PostgreSQL compiled in length.
*
* PostgreSQL allows in standard configuration identifiers no longer than 63
* chars for table/relation names, indexes, primary keys, and constraints. So
* we map all identifiers that are too long to drupal_base64hash_tag, where
* tag is one of:
* - idx for indexes
* - key for constraints
* - pkey for primary keys
* - seq for sequences
*
* @param string $table_identifier_part
* The first argument used to build the identifier string. This usually
* refers to a table/relation name.
* @param string $column_identifier_part
* The second argument used to build the identifier string. This usually
* refers to one or more column names.
* @param string $tag
* The identifier tag. It can be one of 'idx', 'key', 'pkey' or 'seq'.
*
* @return string
* The index/constraint/pkey identifier.
*/
protected function ensureIdentifiersLength($table_identifier_part, $column_identifier_part, $tag) {
$info = $this->getPrefixInfo($table_identifier_part);
$table_identifier_part = $info['table'];
// Filters out potentially empty $column_identifier_part to ensure
// compatibility with old naming convention (see prefixNonTable()).
$identifiers = array_filter(array($table_identifier_part, $column_identifier_part, $tag));
$identifierName = implode('_', $identifiers);
// Retrieve the max identifier length which is usually 63 characters
// but can be altered before PostgreSQL is compiled so we need to check.
if (empty($this->maxIdentifierLength)) {
$this->maxIdentifierLength = $this->connection->query("SHOW max_identifier_length")->fetchField();
}
if (strlen($identifierName) > $this->maxIdentifierLength) {
$saveIdentifier = 'drupal_' . $this->hashBase64($identifierName) . '_' . $tag;
}
else {
$saveIdentifier = $identifierName;
}
return $saveIdentifier;
}
/**
* Fetch the list of blobs and sequences used on a table.
*
......@@ -39,23 +104,47 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
public function queryTableInformation($table) {
// Generate a key to reference this table's information on.
$key = $this->connection->prefixTables('{' . $table . '}');
if (!strpos($key, '.')) {
// Take into account that temporary tables are stored in a different schema.
// \DatabaseConnection::generateTemporaryTableName() sets 'db_temporary_'
// prefix to all temporary tables.
if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) {
$key = 'public.' . $key;
}
else {
$key = $this->getTempNamespaceName() . '.' . $key;
}
if (!isset($this->tableInformation[$key])) {
// Split the key into schema and table for querying.
list($schema, $table_name) = explode('.', $key);
$table_information = (object) array(
'blob_fields' => array(),
'sequences' => array(),
);
// Don't use {} around information_schema.columns table.
$result = $this->connection->query("SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = :schema AND table_name = :table AND (data_type = 'bytea' OR (numeric_precision IS NOT NULL AND column_default LIKE :default))", array(
':schema' => $schema,
':table' => $table_name,
':default' => '%nextval%',
// The bytea columns and sequences for a table can be found in
// pg_attribute, which is significantly faster than querying the
// information_schema. The data type of a field can be found by lookup
// of the attribute ID, and the default value must be extracted from the
// node tree for the attribute definition instead of the historical
// human-readable column, adsrc.
$sql = <<<'EOD'
SELECT pg_attribute.attname AS column_name, format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type, pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) AS column_default
FROM pg_attribute
LEFT JOIN pg_attrdef ON pg_attrdef.adrelid = pg_attribute.attrelid AND pg_attrdef.adnum = pg_attribute.attnum
WHERE pg_attribute.attnum > 0
AND NOT pg_attribute.attisdropped
AND pg_attribute.attrelid = :key::regclass
AND (format_type(pg_attribute.atttypid, pg_attribute.atttypmod) = 'bytea'
OR pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) LIKE 'nextval%')
EOD;
$result = $this->connection->query($sql, array(
':key' => $key,
));
if (empty($result)) {
return $table_information;
}
foreach ($result as $column) {
if ($column->data_type == 'bytea') {
$table_information->blob_fields[$column->column_name] = TRUE;
......@@ -73,6 +162,19 @@ public function queryTableInformation($table) {
return $this->tableInformation[$key];
}
/**
* Gets PostgreSQL's temporary namespace name.
*
* @return string
* PostgreSQL's temporary namespace anme.
*/
protected function getTempNamespaceName() {
if (!isset($this->tempNamespaceName)) {
$this->tempNamespaceName = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField();
}
return $this->tempNamespaceName;
}
/**
* Fetch the list of CHECK constraints used on a field.
*
......@@ -124,11 +226,11 @@ protected function createTableSql($name, $table) {
$sql_keys = array();
if (isset($table['primary key']) && is_array($table['primary key'])) {
$sql_keys[] = 'PRIMARY KEY (' . implode(', ', $table['primary key']) . ')';
$sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . implode(', ', $table['primary key']) . ')';
}
if (isset($table['unique keys']) && is_array($table['unique keys'])) {
foreach ($table['unique keys'] as $key_name => $key) {
$sql_keys[] = 'CONSTRAINT ' . $this->prefixNonTable($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
$sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
}
}
......@@ -312,6 +414,68 @@ protected function _createKeySql($fields) {
return implode(', ', $return);
}
/**
* {@inheritdoc}
*/
public function tableExists($table) {
// In PostgreSQL "unquoted names are always folded to lower case."
// @see DatabaseSchema_pgsql::buildTableNameCondition().
$prefixInfo = $this->getPrefixInfo(strtolower($table), TRUE);
return (bool) $this->connection->query("SELECT 1 FROM pg_tables WHERE schemaname = :schema AND tablename = :table", array(':schema' => $prefixInfo['schema'], ':table' => $prefixInfo['table']))->fetchField();
}
/**
* {@inheritdoc}
*/
public function findTables($table_expression) {
$individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
$default_prefix = $this->connection->tablePrefix();
$default_prefix_length = strlen($default_prefix);
$tables = array();
// 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.
$results = $this->connection->query("SELECT tablename FROM pg_tables WHERE schemaname = :schema", array(':schema' => $this->defaultSchema));
foreach ($results as $table) {
// Take into account tables that have an individual prefix.
if (isset($individually_prefixed_tables[$table->tablename])) {
$prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->tablename]));
}
elseif ($default_prefix && substr($table->tablename, 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->tablename, $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;
}
}
// Need to use strtolower on the table name as it was used previously by
// DatabaseSchema_pgsql::buildTableNameCondition().
// @see https://www.drupal.org/project/drupal/issues/3262341
$table_expression = strtolower($table_expression);
// 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;
}
function renameTable($table, $new_name) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename @table to @table_new: table @table doesn't exist.", array('@table' => $table, '@table_new' => $new_name)));
......@@ -328,10 +492,31 @@ function renameTable($table, $new_name) {
// rename them when renaming the table.
$indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', array(':schema' => $old_schema, ':table' => $old_table_name));
foreach ($indexes as $index) {
if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)$/', $index->indexname, $matches)) {
// Get the index type by suffix, e.g. idx/key/pkey
$index_type = substr($index->indexname, strrpos($index->indexname, '_') + 1);
// If the index is already rewritten by ensureIdentifiersLength() to not
// exceed the 63 chars limit of PostgreSQL, we need to take care of that.
// Example (drupal_Gk7Su_T1jcBHVuvSPeP22_I3Ni4GrVEgTYlIYnBJkro_idx).
if (strpos($index->indexname, 'drupal_') !== FALSE) {
preg_match('/^drupal_(.*)_' . preg_quote($index_type) . '/', $index->indexname, $matches);
$index_name = $matches[1];
$this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name);
}
else {
if ($index_type == 'pkey') {
// Primary keys do not have a specific name in D7.
$index_name = '';
}
else {
// Make sure to remove the suffix from index names, because
// ensureIdentifiersLength() will add the suffix again and thus
// would result in a wrong index name.
preg_match('/^' . preg_quote($old_full_name) . '_(.*)_' . preg_quote($index_type) . '/', $index->indexname, $matches);
$index_name = $matches[1];
}
}
$this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO ' . $this->ensureIdentifiersLength($new_name, $index_name, $index_type));
}
// Now rename the table.
......@@ -414,9 +599,20 @@ public function fieldSetNoDefault($table, $field) {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
}
/**
* {@inheritdoc}
*/
public function fieldExists($table, $column) {
// In PostgreSQL "unquoted names are always folded to lower case."
// @see DatabaseSchema_pgsql::buildTableNameCondition().
$prefixInfo = $this->getPrefixInfo(strtolower($table));
return (bool) $this->connection->query("SELECT 1 FROM pg_attribute WHERE attrelid = :key::regclass AND attname = :column AND NOT attisdropped AND attnum > 0", array(':key' => $prefixInfo['schema'] . '.' . $prefixInfo['table'], ':column' => $column))->fetchField();
}
public function indexExists($table, $name) {
// Details http://www.postgresql.org/docs/8.3/interactive/view-pg-indexes.html
$index_name = '{' . $table . '}_' . $name . '_idx';
// Details https://www.postgresql.org/docs/10/view-pg-indexes.html
$index_name = $this->ensureIdentifiersLength($table, $name, 'idx');
return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField();
}
......@@ -429,7 +625,18 @@ public function indexExists($table, $name) {
* The name of the constraint (typically 'pkey' or '[constraint]_key').
*/
protected function constraintExists($table, $name) {
$constraint_name = '{' . $table . '}_' . $name;
// ensureIdentifiersLength() expects three parameters, thus we split our
// constraint name in a proper name and a suffix.
if ($name == 'pkey') {
$suffix = $name;
$name = '';
}
else {
$pos = strrpos($name, '_');
$suffix = substr($name, $pos + 1);
$name = substr($name, 0, $pos);
}
$constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix);
return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField();
}
......@@ -441,7 +648,7 @@ public function addPrimaryKey($table, $fields) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . implode(',', $fields) . ')');
$this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey') . ' PRIMARY KEY (' . implode(',', $fields) . ')');
}
public function dropPrimaryKey($table) {
......@@ -449,7 +656,7 @@ public function dropPrimaryKey($table) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->prefixNonTable($table, 'pkey'));
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey'));
return TRUE;
}
......@@ -461,7 +668,7 @@ function addUniqueKey($table, $name, $fields) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", array('@table' => $table, '@name' => $name)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
$this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->ensureIdentifiersLength($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
}
public function dropUniqueKey($table, $name) {
......@@ -469,7 +676,7 @@ public function dropUniqueKey($table, $name) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '"');
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $this->ensureIdentifiersLength($table, $name, 'key') . '"');
return TRUE;
}
......@@ -489,7 +696,7 @@ public function dropIndex($table, $name) {
return FALSE;
}
$this->connection->query('DROP INDEX ' . $this->prefixNonTable($table, $name, 'idx'));
$this->connection->query('DROP INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx'));
return TRUE;
}
......@@ -580,7 +787,7 @@ public function changeField($table, $field, $field_new, $spec, $new_keys = array
}
protected function _createIndexSql($table, $name, $fields) {
$query = 'CREATE INDEX "' . $this->prefixNonTable($table, $name, 'idx') . '" ON {' . $table . '} (';
$query = 'CREATE INDEX "' . $this->ensureIdentifiersLength($table, $name, 'idx') . '" ON {' . $table . '} (';
$query .= $this->_createKeySql($fields) . ')';
return $query;
}
......@@ -614,4 +821,36 @@ public function getComment($table, $column = NULL) {
return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', array('pg_class', $info['table']))->fetchField();
}
}
/**
* Calculates a base-64 encoded, PostgreSQL-safe sha-256 hash per PostgreSQL
* documentation: 4.1. Lexical Structure.
*
* @param $data
* String to be hashed.
*
* @return string
* A base-64 encoded sha-256 hash, with + and / replaced with _ and any =
* padding characters removed.
*/
protected function hashBase64($data) {
// Ensure lowercase as D7's pgsql driver does not quote identifiers
// consistently, and they are therefore folded to lowercase by PostgreSQL.
$hash = strtolower(base64_encode(hash('sha256', $data, TRUE)));
// Modify the hash so it's safe to use in PostgreSQL identifiers.
return strtr($hash, array('+' => '_', '/' => '_', '=' => ''));
}
/**
* Build a condition to match a table name against a standard information_schema.
*
* In PostgreSQL "unquoted names are always folded to lower case." The pgsql
* driver does not quote table names, so they are therefore always lowercase.
*
* @see https://www.postgresql.org/docs/14/sql-syntax-lexical.html
*/
protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
return parent::buildTableNameCondition(strtolower($table_name), $operator, $add_prefix);
}
}
......@@ -268,6 +268,7 @@ public function setFetchMode($fetchStyle, $a2 = NULL, $a3 = NULL) {
* @return
* The current row formatted as requested.
*/
#[\ReturnTypeWillChange]
public function current() {
if (isset($this->currentRow)) {
switch ($this->fetchStyle) {
......@@ -285,7 +286,7 @@ public function current() {
case PDO::FETCH_OBJ:
return (object) $this->currentRow;
case PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE:
$class_name = array_unshift($this->currentRow);
$class_name = array_shift($this->currentRow);
// Deliberate no break.
case PDO::FETCH_CLASS:
if (!isset($class_name)) {
......@@ -320,14 +321,17 @@ public function current() {
/* Implementations of Iterator. */
#[\ReturnTypeWillChange]
public function key() {
return $this->currentKey;
}
#[\ReturnTypeWillChange]
public function rewind() {
// Nothing to do: our DatabaseStatement can't be rewound.
}
#[\ReturnTypeWillChange]
public function next() {
if (!empty($this->data)) {
$this->currentRow = reset($this->data);
......@@ -339,6 +343,7 @@ public function next() {
}
}
#[\ReturnTypeWillChange]
public function valid() {
return isset($this->currentRow);
}
......
......@@ -871,8 +871,14 @@ public function __toString() {
$query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
if (count($this->condition)) {
$this->condition->compile($this->connection, $this);
try {
$this->condition->compile($this->connection, $this);
}
// PHP does not allow exceptions to be thrown in __toString(), so trigger
// a fatal error instead.
catch (InvalidQueryConditionOperatorException $e) {
drupal_trigger_fatal_error($e->getMessage());
}
$query .= "\nWHERE " . $this->condition;
}
......@@ -1204,7 +1210,14 @@ public function __toString() {
$query = $comments . 'UPDATE {' . $this->connection->escapeTable($this->table) . '} SET ' . implode(', ', $update_fields);
if (count($this->condition)) {
$this->condition->compile($this->connection, $this);
try {
$this->condition->compile($this->connection, $this);
}
// PHP does not allow exceptions to be thrown in __toString(), so trigger
// a fatal error instead.
catch (InvalidQueryConditionOperatorException $e) {
drupal_trigger_fatal_error($e->getMessage());
}
// There is an implicit string cast on $this->condition.
$query .= "\nWHERE " . $this->condition;
}
......@@ -1274,6 +1287,15 @@ class MergeQuery extends Query implements QueryConditionInterface {
*/
protected $conditionTable;
/**
* The condition object for this query.
*
* Condition handling is handled via composition.
*
* @var DatabaseCondition
*/
protected $condition;
/**
* An array of fields on which to insert.
*
......@@ -1680,6 +1702,13 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
*/
protected $queryPlaceholderIdentifier;
/**
* Contains the string version of the Condition.
*
* @var string
*/
protected $stringVersion;
/**
* Constructs a DataBaseCondition object.
*
......@@ -1697,6 +1726,7 @@ public function __construct($conjunction) {
* size of its conditional array minus one, because one element is the
* conjunction.
*/
#[\ReturnTypeWillChange]
public function count() {
return count($this->conditions) - 1;
}
......@@ -1789,6 +1819,8 @@ public function arguments() {
/**
* Implements QueryConditionInterface::compile().
*
* @throws InvalidQueryConditionOperatorException
*/
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
// Re-compile if this condition changed or if we are compiled against a
......@@ -1819,6 +1851,12 @@ public function compile(DatabaseConnection $connection, QueryPlaceholderInterfac
$arguments += $condition['field']->arguments();
}
else {
// If the operator contains an invalid character, throw an
// exception to protect against SQL injection attempts.
if (stripos($condition['operator'], 'UNION') !== FALSE || strpbrk($condition['operator'], '[-\'"();') !== FALSE) {
throw new InvalidQueryConditionOperatorException('Invalid characters in query operator: ' . $condition['operator']);
}
// For simplicity, we treat all operators as the same data structure.
// In the typical degenerate case, this won't get changed.
$operator_defaults = array(
......@@ -1883,7 +1921,7 @@ public function compiled() {
public function __toString() {
// If the caller forgot to call compile() first, refuse to run.
if ($this->changed) {
return NULL;
return '';
}
return $this->stringVersion;
}
......
......@@ -169,6 +169,11 @@
*/
abstract class DatabaseSchema implements QueryPlaceholderInterface {
/**
* The database connection.
*
* @var DatabaseConnection
*/
protected $connection;
/**
......@@ -343,7 +348,70 @@ public function findTables($table_expression) {
// 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;
}
/**
......
......@@ -883,7 +883,7 @@ class SelectQuery extends Query implements SelectQueryInterface {
* 'type' => $join_type (one of INNER, LEFT OUTER, RIGHT OUTER),
* 'table' => $table,
* 'alias' => $alias_of_the_table,
* 'condition' => $condition_clause_on_which_to_join,
* 'condition' => $join_condition (string or Condition object),
* 'arguments' => $array_of_arguments_for_placeholders_in_the condition.
* 'all_fields' => TRUE to SELECT $alias.*, FALSE or NULL otherwise.
* )
......@@ -891,6 +891,10 @@ class SelectQuery extends Query implements SelectQueryInterface {
* If $table is a string, it is taken as the name of a table. If it is
* a SelectQuery object, it is taken as a subquery.
*
* If $join_condition is a Condition object, any arguments should be
* incorporated into the object; a separate array of arguments does not need
* to be provided.
*
* @var array
*/
protected $tables = array();
......@@ -940,6 +944,20 @@ class SelectQuery extends Query implements SelectQueryInterface {
*/
protected $range;
/**
* The query metadata for alter purposes.
*
* @var array
*/
public $alterMetaData;
/**
* The query tags.
*
* @var array
*/
public $alterTags;
/**
* An array whose elements specify a query to UNION, and the UNION type. The
* 'type' key may be '', 'ALL', or 'DISTINCT' to represent a 'UNION',
......@@ -964,7 +982,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');
......@@ -1028,6 +1046,10 @@ public function arguments() {
if ($table['table'] instanceof SelectQueryInterface) {
$args += $table['table']->arguments();
}
// If the join condition is an object, grab its arguments recursively.
if (!empty($table['condition']) && $table['condition'] instanceof QueryConditionInterface) {
$args += $table['condition']->arguments();
}
}
foreach ($this->expressions as $expression) {
......@@ -1079,6 +1101,10 @@ public function compile(DatabaseConnection $connection, QueryPlaceholderInterfac
if ($table['table'] instanceof SelectQueryInterface) {
$table['table']->compile($connection, $queryPlaceholder);
}
// Make sure join conditions are also compiled.
if (!empty($table['condition']) && $table['condition'] instanceof QueryConditionInterface) {
$table['condition']->compile($connection, $queryPlaceholder);
}
}
// If there are any dependent queries to UNION, compile it recursively.
......@@ -1099,6 +1125,11 @@ public function compiled() {
return FALSE;
}
}
if (!empty($table['condition']) && $table['condition'] instanceof QueryConditionInterface) {
if (!$table['condition']->compiled()) {
return FALSE;
}
}
}
foreach ($this->union as $union) {
......@@ -1504,7 +1535,14 @@ public function __toString() {
// the query will be executed, it will be recompiled using the proper
// placeholder generator anyway.
if (!$this->compiled()) {
$this->compile($this->connection, $this);
try {
$this->compile($this->connection, $this);
}
// PHP does not allow exceptions to be thrown in __toString(), so trigger
// a fatal error instead.
catch (InvalidQueryConditionOperatorException $e) {
drupal_trigger_fatal_error($e->getMessage());
}
}
// Create a sanitized comment string to prepend to the query.
......@@ -1520,13 +1558,16 @@ public function __toString() {
$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,10 +1596,10 @@ public function __toString() {
// 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'];
$query .= ' ON ' . (string) $table['condition'];
}
}
......@@ -1579,6 +1620,14 @@ public function __toString() {
$query .= "\nHAVING " . $this->having;
}
// UNION is a little odd, as the select queries to combine are passed into
// this query, but syntactically they all end up on the same level.
if ($this->union) {
foreach ($this->union as $union) {
$query .= ' ' . $union['type'] . ' ' . (string) $union['query'];
}
}
// ORDER BY
if ($this->order) {
$query .= "\nORDER BY ";
......@@ -1598,14 +1647,6 @@ public function __toString() {
$query .= "\nLIMIT " . (int) $this->range['length'] . " OFFSET " . (int) $this->range['start'];
}
// UNION is a little odd, as the select queries to combine are passed into
// this query, but syntactically they all end up on the same level.
if ($this->union) {
foreach ($this->union as $union) {
$query .= ' ' . $union['type'] . ' ' . (string) $union['query'];
}
}
if ($this->forUpdate) {
$query .= ' FOR UPDATE';
}
......@@ -1614,6 +1655,8 @@ public function __toString() {
}
public function __clone() {
parent::__clone();
// On cloning, also clone the dependent objects. However, we do not
// want to clone the database connection object as that would duplicate the
// connection itself.
......@@ -1623,6 +1666,11 @@ public function __clone() {
foreach ($this->union as $key => $aggregate) {
$this->union[$key]['query'] = clone($aggregate['query']);
}
foreach ($this->tables as $alias => $table) {
if ($table['table'] instanceof SelectQueryInterface) {
$this->tables[$alias]['table'] = clone $table['table'];
}
}
}
}
......
......@@ -107,9 +107,21 @@ public function __construct(array $connection_options = array()) {
$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']));
$this->connection->exec(implode('; ', $connection_options['init_commands']));
}
}
......@@ -128,10 +140,10 @@ public function __destruct() {
$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 @@ public function __destruct() {
}
}
/**
* 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.
*/
......@@ -235,7 +259,7 @@ public function prepare($query, $options = array()) {
* 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()) {
......@@ -326,7 +350,7 @@ public function rollback($savepoint_name = 'drupal_transaction') {
}
}
if ($this->supportsTransactions()) {
PDO::rollBack();
$this->connection->rollBack();
}
}
......@@ -341,7 +365,7 @@ public function pushTransaction($name) {
throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
}
if (!$this->inTransaction()) {
PDO::beginTransaction();
$this->connection->beginTransaction();
}
$this->transactionLayers[$name] = $name;
}
......@@ -366,9 +390,9 @@ public function popTransaction($name) {
// 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();
}
}
......
......@@ -23,7 +23,7 @@ public function execute() {
if (!$this->preExecute()) {
return NULL;
}
if (count($this->insertFields)) {
if (count($this->insertFields) || !empty($this->fromQuery)) {
return parent::execute();
}
else {
......@@ -36,7 +36,10 @@ public function __toString() {
$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.
......
......@@ -433,7 +433,7 @@ protected function introspectSchema($table) {
'type' => $type,
'size' => $size,
'not null' => !empty($row->notnull),
'default' => trim($row->dflt_value, "'"),
'default' => trim((string) $row->dflt_value, "'"),
);
if ($length) {
$schema['fields'][$row->name]['length'] = $length;
......@@ -668,6 +668,9 @@ public function fieldSetNoDefault($table, $field) {
$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 @@ public function findTables($table_expression) {
));
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;
}
}
......@@ -254,7 +254,10 @@ protected function cleanIds(&$ids) {
* Callback for array_filter that removes non-integer IDs.
*/
protected function filterId($id) {
return is_numeric($id) && $id == (int) $id;
// ctype_digit() is used here instead of a strict comparison as sometimes
// the id is passed as a string containing '0' which may represent a bug
// elsewhere but would fail with a strict comparison.
return is_numeric($id) && $id == (int) $id && ctype_digit((string) $id);
}
/**
......@@ -618,7 +621,7 @@ class EntityFieldQuery {
*
* @see EntityFieldQuery::execute().
*/
public $orderedResults = array();
public $ordered_results = array();
/**
* The method executing the query, if it is overriding the default.
......@@ -1176,7 +1179,7 @@ public function addMetaData($key, $object) {
/**
* Executes the query.
*
* After executing the query, $this->orderedResults will contain a list of
* After executing the query, $this->ordered_results will contain a list of
* the same stub entities in the order returned by the query. This is only
* relevant if there are multiple entity types in the returned value and
* a field ordering was requested. In every other case, the returned value
......
......@@ -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];
......@@ -62,7 +59,8 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
require_once DRUPAL_ROOT . '/includes/common.inc';
}
// We treat recoverable errors as fatal.
// We treat recoverable errors as fatal, and also allow fatal errors to be
// explicitly triggered by drupal_trigger_fatal_error().
_drupal_log_error(array(
'%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
// The standard PHP error handler considers that the error messages
......@@ -72,7 +70,7 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
'%file' => $caller['file'],
'%line' => $caller['line'],
'severity_level' => $severity_level,
), $error_level == E_RECOVERABLE_ERROR);
), $error_level == E_RECOVERABLE_ERROR || drupal_static('drupal_trigger_fatal_error'));
}
}
......@@ -105,8 +103,8 @@ function _drupal_decode_exception($exception) {
// We remove that call.
array_shift($backtrace);
}
if (isset($exception->query_string, $exception->args)) {
$message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
if (isset($exception->errorInfo['query_string'], $exception->errorInfo['args'])) {
$message .= ": " . $exception->errorInfo['query_string'] . "; " . print_r($exception->errorInfo['args'], TRUE);
}
}
$caller = _drupal_get_last_caller($backtrace);
......@@ -218,7 +216,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
if ($fatal) {
// When called from CLI, simply output a plain text message.
print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n";
exit;
exit(1);
}
}
......@@ -269,7 +267,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
function _drupal_get_last_caller($backtrace) {
// Errors that occur inside PHP internal functions do not generate
// information about file and line. Ignore black listed functions.
$blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
$blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler', 'drupal_trigger_fatal_error');
while (($backtrace && !isset($backtrace[0]['line'])) ||
(isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) {
array_shift($backtrace);
......
......@@ -69,6 +69,13 @@
*/
define('FILE_STATUS_PERMANENT', 1);
/**
* A pipe-separated list of insecure extensions.
*
* @see file_munge_filename(), file_save_upload()
*/
define('FILE_INSECURE_EXTENSIONS', 'php|phar|pl|py|cgi|asp|js|phtml');
/**
* Provides Drupal stream wrapper registry.
*
......@@ -197,7 +204,7 @@ function file_stream_wrapper_get_class($scheme) {
* @see file_uri_target()
*/
function file_uri_scheme($uri) {
$position = strpos($uri, '://');
$position = strpos((string) $uri, '://');
return $position ? substr($uri, 0, $position) : FALSE;
}
......@@ -437,10 +444,10 @@ function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS)
// Check if directory exists.
if (!is_dir($directory)) {
// Let mkdir() recursively create directories and use the default directory
// permissions.
if (($options & FILE_CREATE_DIRECTORY) && @drupal_mkdir($directory, NULL, TRUE)) {
return drupal_chmod($directory);
// Let drupal_mkdir() recursively create directories and use the default
// directory permissions.
if ($options & FILE_CREATE_DIRECTORY) {
return @drupal_mkdir($directory, NULL, TRUE);
}
return FALSE;
}
......@@ -532,6 +539,13 @@ function file_htaccess_lines($private = TRUE) {
<IfModule mod_php5.c>
php_flag engine off
</IfModule>
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
# From PHP 8 there is no number in the module name.
<IfModule mod_php.c>
php_flag engine off
</IfModule>
EOF;
if ($private) {
......@@ -993,8 +1007,15 @@ function file_build_uri($path) {
* @return
* The destination filepath, or FALSE if the file already exists
* and FILE_EXISTS_ERROR is specified.
*
* @throws RuntimeException
* Thrown if the filename contains invalid UTF-8.
*/
function file_destination($destination, $replace) {
$basename = drupal_basename($destination);
if (!drupal_validate_utf8($basename)) {
throw new RuntimeException(sprintf("Invalid filename '%s'", $basename));
}
if (file_exists($destination)) {
switch ($replace) {
case FILE_EXISTS_REPLACE:
......@@ -1002,7 +1023,6 @@ function file_destination($destination, $replace) {
break;
case FILE_EXISTS_RENAME:
$basename = drupal_basename($destination);
$directory = drupal_dirname($destination);
$destination = file_create_filename($basename, $directory);
break;
......@@ -1138,8 +1158,8 @@ function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXIST
* exploit.php_.pps.
*
* Specifically, this function adds an underscore to all extensions that are
* between 2 and 5 characters in length, internal to the file name, and not
* included in $extensions.
* between 2 and 5 characters in length, internal to the file name, and either
* included in the list of unsafe extensions, or not included in $extensions.
*
* Function behavior is also controlled by the Drupal variable
* 'allow_insecure_uploads'. If 'allow_insecure_uploads' evaluates to TRUE, no
......@@ -1148,7 +1168,8 @@ function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXIST
* @param $filename
* File name to modify.
* @param $extensions
* A space-separated list of extensions that should not be altered.
* A space-separated list of extensions that should not be altered. Note that
* extensions that are unsafe will be altered regardless of this parameter.
* @param $alerts
* If TRUE, drupal_set_message() will be called to display a message if the
* file name was changed.
......@@ -1166,6 +1187,9 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
$whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
// Remove unsafe extensions from the list of allowed extensions.
$whitelist = array_diff($whitelist, explode('|', FILE_INSECURE_EXTENSIONS));
// Split the filename up by periods. The first part becomes the basename
// the last part the final extension.
$filename_parts = explode('.', $filename);
......@@ -1218,11 +1242,20 @@ function file_unmunge_filename($filename) {
* @return
* File path consisting of $directory and a unique filename based off
* of $basename.
*
* @throws RuntimeException
* Thrown if the $basename is not valid UTF-8 or another error occurs
* stripping control characters.
*/
function file_create_filename($basename, $directory) {
$original = $basename;
// Strip control characters (ASCII value < 32). Though these are allowed in
// some filesystems, not many applications handle them well.
$basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
if (preg_last_error() !== PREG_NO_ERROR) {
throw new RuntimeException(sprintf("Invalid filename '%s'", $original));
}
if (substr(PHP_OS, 0, 3) == 'WIN') {
// These characters are not allowed in Windows filenames
$basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
......@@ -1524,25 +1557,35 @@ function file_save_upload($form_field_name, $validators = array(), $destination
$validators['file_validate_extensions'][0] = $extensions;
}
if (!empty($extensions)) {
// Munge the filename to protect against possible malicious extension hiding
// within an unknown file type (ie: filename.html.foo).
$file->filename = file_munge_filename($file->filename, $extensions);
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|phar|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
$file->filemime = 'text/plain';
// The destination filename will also later be used to create the URI.
$file->filename .= '.txt';
// The .txt extension may not be in the allowed list of extensions. We have
// to add it here or else the file upload will fail.
if (!variable_get('allow_insecure_uploads', 0)) {
if (!empty($extensions)) {
$validators['file_validate_extensions'][0] .= ' txt';
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename)));
// Munge the filename to protect against possible malicious extension hiding
// within an unknown file type (ie: filename.html.foo).
$file->filename = file_munge_filename($file->filename, $extensions);
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php_.foo_.txt and
// filename.php_.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (preg_match('/\.(' . FILE_INSECURE_EXTENSIONS . ')(\.|$)/i', $file->filename)) {
// If the file will be rejected anyway due to a disallowed extension, it
// should not be renamed; rather, we'll let file_validate_extensions()
// reject it below.
if (!isset($validators['file_validate_extensions']) || !file_validate_extensions($file, $extensions)) {
$file->filemime = 'text/plain';
if (substr($file->filename, -4) != '.txt') {
// The destination filename will also later be used to create the URI.
$file->filename .= '.txt';
}
$file->filename = file_munge_filename($file->filename, $extensions, FALSE);
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename)));
// The .txt extension may not be in the allowed list of extensions. We have
// to add it here or else the file upload will fail.
if (!empty($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
}
}
}
}
......@@ -1563,7 +1606,13 @@ function file_save_upload($form_field_name, $validators = array(), $destination
if (substr($destination, -1) != '/') {
$destination .= '/';
}
$file->destination = file_destination($destination . $file->filename, $replace);
try {
$file->destination = file_destination($destination . $file->filename, $replace);
}
catch (RuntimeException $e) {
drupal_set_message(t('The file %source could not be uploaded because the name is invalid.', array('%source' => $form_field_name)), 'error');
return FALSE;
}
// If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
// there's an existing file so we need to bail.
if ($file->destination === FALSE) {
......@@ -1704,7 +1753,18 @@ function file_validate(stdClass &$file, $validators = array()) {
}
// Let other modules perform validation on the new file.
return array_merge($errors, module_invoke_all('file_validate', $file));
$errors = array_merge($errors, module_invoke_all('file_validate', $file));
// Ensure the file does not contain a malicious extension. At this point
// file_save_upload() will have munged the file so it does not contain a
// malicious extension. Contributed and custom code that calls this method
// needs to take similar steps if they need to permit files with malicious
// extensions to be uploaded.
if (empty($errors) && !variable_get('allow_insecure_uploads', 0) && preg_match('/\.(' . FILE_INSECURE_EXTENSIONS . ')(\.|$)/i', $file->filename)) {
$errors[] = t('For security reasons, your upload has been rejected.');
}
return $errors;
}
/**
......@@ -1802,7 +1862,7 @@ function file_validate_is_image(stdClass $file) {
$info = image_get_info($file->uri);
if (!$info || empty($info['extension'])) {
$errors[] = t('Only JPEG, PNG and GIF images are allowed.');
$errors[] = t('The image file is invalid or the image type is not allowed. Allowed types: %allowed_types', array('%allowed_types' => t('Only JPEG, PNG and GIF images are allowed.')));
}
return $errors;
......@@ -1845,6 +1905,8 @@ function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0,
if ($image = image_load($file->uri)) {
image_scale($image, $width, $height);
image_save($image);
// Update the image info since we have resized it.
$info = $image->info;
$file->filesize = $image->info['file_size'];
drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions)));
}
......@@ -2019,6 +2081,7 @@ function file_download() {
$scheme = array_shift($args);
$target = implode('/', $args);
$uri = $scheme . '://' . $target;
$uri = file_uri_normalize_dot_segments($uri);
if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
$headers = file_download_headers($uri);
if (count($headers)) {
......@@ -2130,9 +2193,33 @@ function file_download_access($uri) {
* 'filename', and 'name' members corresponding to the matching files.
*/
function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
// Default nomask option.
$nomask = '/(\.\.?|CVS)$/';
// Overrides the $nomask variable accordingly if $options['nomask'] is set.
//
// Allow directories specified in settings.php to be ignored. You can use this
// to not check for files in common special-purpose directories. For example,
// node_modules and bower_components. Ignoring irrelevant directories is a
// performance boost.
if (!isset($options['nomask'])) {
$ignore_directories = variable_get(
'file_scan_ignore_directories',
array()
);
foreach ($ignore_directories as $index => $ignore_directory) {
$ignore_directories[$index] = preg_quote($ignore_directory, '/');
}
if (!empty($ignore_directories)) {
$nomask = '/^(\.\.?)|CVS|' . implode('|', $ignore_directories) . '$/';
}
}
// Merge in defaults.
$options += array(
'nomask' => '/(\.\.?|CVS)$/',
'nomask' => $nomask,
'callback' => 0,
'recurse' => TRUE,
'key' => 'uri',
......@@ -2407,19 +2494,21 @@ function drupal_basename($uri, $suffix = NULL) {
}
/**
* Creates a directory using Drupal's default mode.
*
* PHP's mkdir() does not respect Drupal's default permissions mode. If a mode
* is not provided, this function will make sure that Drupal's is used.
* Creates a directory, optionally creating missing components in the path to
* the directory.
*
* Compatibility: normal paths and stream wrappers.
* When PHP's mkdir() creates a directory, the requested mode is affected by the
* process's umask. This function overrides the umask and sets the mode
* explicitly for all directory components created.
*
* @param $uri
* A URI or pathname.
* @param $mode
* By default the Drupal mode is used.
* Mode given to created directories. Defaults to the directory mode
* configured in the Drupal installation. It must have a leading zero.
* @param $recursive
* Default to FALSE.
* Create directories recursively, defaults to FALSE. Cannot work with a mode
* which denies writing or execution to the owner of the process.
* @param $context
* Refer to http://php.net/manual/ref.stream.php
*
......@@ -2435,7 +2524,71 @@ function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
$mode = variable_get('file_chmod_directory', 0775);
}
if (!isset($context)) {
// If the URI has a scheme, don't override the umask - schemes can handle this
// issue in their own implementation.
if (file_uri_scheme($uri)) {
return _drupal_mkdir_call($uri, $mode, $recursive, $context);
}
// If recursive, create each missing component of the parent directory
// individually and set the mode explicitly to override the umask.
if ($recursive) {
// Ensure the path is using DIRECTORY_SEPARATOR, and trim off any trailing
// slashes because they can throw off the loop when creating the parent
// directories.
$uri = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $uri), DIRECTORY_SEPARATOR);
// Determine the components of the path.
$components = explode(DIRECTORY_SEPARATOR, $uri);
// If the filepath is absolute the first component will be empty as there
// will be nothing before the first slash.
if ($components[0] == '') {
$recursive_path = DIRECTORY_SEPARATOR;
// Get rid of the empty first component.
array_shift($components);
}
else {
$recursive_path = '';
}
// Don't handle the top-level directory in this loop.
array_pop($components);
// Create each component if necessary.
foreach ($components as $component) {
$recursive_path .= $component;
if (!file_exists($recursive_path)) {
$success = _drupal_mkdir_call($recursive_path, $mode, FALSE, $context);
// If the operation failed, check again if the directory was created
// by another process/server, only report a failure if not.
if (!$success && !file_exists($recursive_path)) {
return FALSE;
}
// Not necessary to use self::chmod() as there is no scheme.
if (!chmod($recursive_path, $mode)) {
return FALSE;
}
}
$recursive_path .= DIRECTORY_SEPARATOR;
}
}
// Do not check if the top-level directory already exists, as this condition
// must cause this function to fail.
if (!_drupal_mkdir_call($uri, $mode, FALSE, $context)) {
return FALSE;
}
// Not necessary to use drupal_chmod() as there is no scheme.
return chmod($uri, $mode);
}
/**
* Helper function. Ensures we don't pass a NULL as a context resource to
* mkdir().
*
* @see drupal_mkdir()
*/
function _drupal_mkdir_call($uri, $mode, $recursive, $context) {
if (is_null($context)) {
return mkdir($uri, $mode, $recursive);
}
else {
......@@ -2586,6 +2739,58 @@ function file_get_content_headers($file) {
);
}
/**
* Normalize dot segments in a URI.
*
* @param $uri
* A stream, referenced as "scheme://target".
*
* @return string
* The URI with dot segments removed and slashes as directory separator.
*/
function file_uri_normalize_dot_segments($uri) {
$scheme = file_uri_scheme($uri);
if (file_stream_wrapper_valid_scheme($scheme)) {
$target = file_uri_target($uri);
if ($target !== FALSE) {
if (!in_array($scheme, variable_get('file_sa_core_2023_005_schemes', array()))) {
$class = file_stream_wrapper_get_class($scheme);
$is_local = is_subclass_of($class, 'DrupalLocalStreamWrapper');
if ($is_local) {
$target = str_replace(DIRECTORY_SEPARATOR, '/', $target);
}
$parts = explode('/', $target);
$normalized_parts = array();
while ($parts) {
$part = array_shift($parts);
if ($part === '' || $part === '.') {
continue;
}
elseif ($part === '..' && $is_local && $normalized_parts === array()) {
$normalized_parts[] = $part;
break;
}
elseif ($part === '..') {
array_pop($normalized_parts);
}
else {
$normalized_parts[] = $part;
}
}
$target = implode('/', array_merge($normalized_parts, $parts));
}
$uri = $scheme . '://' . $target;
}
}
return $uri;
}
/**
* @} End of "defgroup file".
*/
......@@ -18,7 +18,21 @@ function file_register_phar_wrapper() {
include_once $directory . '/Helper.php';
include_once $directory . '/Manager.php';
include_once $directory . '/PharStreamWrapper.php';
include_once $directory . '/Collectable.php';
include_once $directory . '/Interceptor/ConjunctionInterceptor.php';
include_once $directory . '/Interceptor/PharMetaDataInterceptor.php';
include_once $directory . '/Phar/Container.php';
include_once $directory . '/Phar/DeserializationException.php';
include_once $directory . '/Phar/Manifest.php';
include_once $directory . '/Phar/Reader.php';
include_once $directory . '/Phar/ReaderException.php';
include_once $directory . '/Phar/Stub.php';
include_once $directory . '/Resolvable.php';
include_once $directory . '/Resolver/PharInvocation.php';
include_once $directory . '/Resolver/PharInvocationCollection.php';
include_once $directory . '/Resolver/PharInvocationResolver.php';
include_once DRUPAL_ROOT . '/misc/typo3/drupal-security/PharExtensionInterceptor.php';
include_once DRUPAL_ROOT . '/misc/brumann/polyfill-unserialize/src/Unserialize.php';
// Set up a stream wrapper to handle insecurities due to PHP's built-in
// phar stream wrapper.
......
......@@ -9,11 +9,13 @@
* the password should always be asked from the user and never stored. For
* safety, all methods operate only inside a "jail", by default the Drupal root.
*/
#[AllowDynamicProperties]
abstract class FileTransfer {
protected $username;
protected $password;
protected $hostname = 'localhost';
protected $port;
protected $jail;
/**
* The constructor for the UpdateConnection class. This method is also called
......@@ -301,7 +303,7 @@ function findChroot() {
$parts = explode('/', $path);
$chroot = '';
while (count($parts)) {
$check = implode($parts, '/');
$check = implode('/', $parts);
if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
// Remove the trailing slash.
return substr($chroot, 0, -1);
......@@ -364,7 +366,7 @@ class FileTransferException extends Exception {
public $arguments;
function __construct($message, $code = 0, $arguments = array()) {
parent::__construct($message, $code);
parent::__construct($message, (int) $code);
$this->arguments = $arguments;
}
}
......@@ -409,11 +411,13 @@ function __construct($path) {
$this->skipdots();
}
#[\ReturnTypeWillChange]
function rewind() {
parent::rewind();
$this->skipdots();
}
#[\ReturnTypeWillChange]
function next() {
parent::next();
$this->skipdots();
......
......@@ -1135,12 +1135,8 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
* Helper function to call form_set_error() if there is a token error.
*/
function _drupal_invalid_token_set_form_error() {
$path = current_path();
$query = drupal_get_query_parameters();
$url = url($path, array('query' => $query));
// Setting this error will cause the form to fail validation.
form_set_error('form_token', t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url)));
form_set_error('form_token', t('The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.'));
}
/**
......@@ -1181,6 +1177,11 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
if (!empty($form['#token'])) {
if (!drupal_valid_token($form_state['values']['form_token'], $form['#token']) || !empty($form_state['invalid_token'])) {
_drupal_invalid_token_set_form_error();
// Ignore all submitted values.
$form_state['input'] = array();
$_POST = array();
// Make sure file uploads do not get processed.
$_FILES = array();
// Stop here and don't run any further validation handlers, because they
// could invoke non-safe operations which opens the door for CSRF
// vulnerabilities.
......@@ -1360,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']))));
}
......@@ -1389,7 +1393,10 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
// identical to the empty option's value, we reset the element's value
// to NULL to trigger the regular #required handling below.
// @see form_process_select()
elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) {
elseif ($elements['#type'] == 'select' && $elements['#required']
&& (!array_key_exists('#multiple', $elements) || !$elements['#multiple'])
&& !isset($elements['#default_value'])
&& $elements['#value'] === $elements['#empty_value']) {
$elements['#value'] = NULL;
form_set_value($elements, NULL, $form_state);
}
......@@ -1675,7 +1682,10 @@ function form_clear_error() {
}
/**
* Returns an associative array of all errors.
* Returns an associative array of all errors if any.
*
* @return array|null
* The form errors if any, NULL otherwise.
*/
function form_get_errors() {
$form = form_set_error();
......@@ -1848,6 +1858,9 @@ function form_builder($form_id, &$element, &$form_state) {
_drupal_invalid_token_set_form_error();
// This value is checked in _form_builder_handle_input_element().
$form_state['invalid_token'] = TRUE;
// Ignore all submitted values.
$form_state['input'] = array();
$_POST = array();
// Make sure file uploads do not get processed.
$_FILES = array();
}
......@@ -2077,7 +2090,7 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
// #access=FALSE on an element usually allow access for some users, so forms
// submitted with drupal_form_submit() may bypass access restriction and be
// treated as high-privilege users instead.
$process_input = empty($element['#disabled']) && (($form_state['programmed'] && $form_state['programmed_bypass_access_check']) || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])));
$process_input = empty($element['#disabled']) && ($element['#type'] !== 'value') && (($form_state['programmed'] && $form_state['programmed_bypass_access_check']) || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])));
// Set the element's #value property.
if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
......@@ -2297,8 +2310,8 @@ function form_state_values_clean(&$form_state) {
* A keyed array containing the current state of the form.
*
* @return
* The data that will appear in the $form_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_image_button_value($form, $input, $form_state) {
if ($input !== FALSE) {
......@@ -2343,8 +2356,8 @@ function form_type_image_button_value($form, $input, $form_state) {
* the element's default value should be returned.
*
* @return
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_checkbox_value($element, $input = FALSE) {
if ($input === FALSE) {
......@@ -2384,8 +2397,8 @@ function form_type_checkbox_value($element, $input = FALSE) {
* the element's default value should be returned.
*
* @return
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_checkboxes_value($element, $input = FALSE) {
if ($input === FALSE) {
......@@ -2425,8 +2438,8 @@ function form_type_checkboxes_value($element, $input = FALSE) {
* the element's default value should be returned.
*
* @return
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_tableselect_value($element, $input = FALSE) {
// If $element['#multiple'] == FALSE, then radio buttons are displayed and
......@@ -2461,8 +2474,8 @@ function form_type_tableselect_value($element, $input = FALSE) {
* element's default value is returned. Defaults to FALSE.
*
* @return
* The data that will appear in the $element_state['values'] collection for
* this element.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_radios_value(&$element, $input = FALSE) {
if ($input !== FALSE) {
......@@ -2500,8 +2513,8 @@ function form_type_radios_value(&$element, $input = FALSE) {
* the element's default value should be returned.
*
* @return
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_password_confirm_value($element, $input = FALSE) {
if ($input === FALSE) {
......@@ -2531,8 +2544,8 @@ function form_type_password_confirm_value($element, $input = FALSE) {
* the element's default value should be returned.
*
* @return
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_select_value($element, $input = FALSE) {
if ($input !== FALSE) {
......@@ -2568,12 +2581,12 @@ function form_type_select_value($element, $input = FALSE) {
* @param array $element
* The form element whose value is being populated.
* @param mixed $input
* The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned.
* The incoming input to populate the form element. If this is FALSE, the
* element's default value should be returned.
*
* @return string
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_textarea_value($element, $input = FALSE) {
if ($input !== FALSE && $input !== NULL) {
......@@ -2593,8 +2606,8 @@ function form_type_textarea_value($element, $input = FALSE) {
* the element's default value should be returned.
*
* @return
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_textfield_value($element, $input = FALSE) {
if ($input !== FALSE && $input !== NULL) {
......@@ -2617,8 +2630,8 @@ function form_type_textfield_value($element, $input = FALSE) {
* the element's default value should be returned.
*
* @return
* The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default.
* The data that will appear in $form_state['values'] for this element, or
* nothing to use the default.
*/
function form_type_token_value($element, $input = FALSE) {
if ($input !== FALSE) {
......@@ -3367,6 +3380,30 @@ function form_process_actions($element, &$form_state) {
return $element;
}
/**
* Processes a form button element.
*
* @param $element
* An associative array containing the properties and children of the
* form button.
* @param $form_state
* The $form_state array for the form this element belongs to.
*
* @return
* The processed element.
*/
function form_process_button($element, &$form_state) {
// We normally want to add drupal.form-single-submit so that the double submit
// protection can be added to the site, however, with the addition of
// javascript_always_use_jquery, this would make most pages with a login
// block or a search form have jquery always added, changing what people who
// set the javascript_always_use_jquery variable to FALSE would have expected.
if (variable_get('javascript_always_use_jquery', TRUE) && variable_get('javascript_use_double_submit_protection', TRUE)) {
$element['#attached']['library'][] = array('system', 'drupal.form-single-submit');
}
return $element;
}
/**
* Processes a container element.
*
......@@ -3917,6 +3954,11 @@ function theme_button($variables) {
$element['#attributes']['type'] = 'submit';
element_set_attributes($element, array('id', 'name', 'value'));
// Remove name attribute, if empty, for W3C compliance.
if (isset($element['#attributes']['name']) && $element['#attributes']['name'] === '') {
unset($element['#attributes']['name']);
}
$element['#attributes']['class'][] = 'form-' . $element['#button_type'];
if (!empty($element['#attributes']['disabled'])) {
$element['#attributes']['class'][] = 'form-button-disabled';
......@@ -4120,9 +4162,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');
}
......@@ -4304,10 +4354,14 @@ function theme_form_required_marker($variables) {
* required. That is especially important for screenreader users to know
* which field is required.
*
* To associate the label with a different field, set the #label_for property
* to the ID of the desired field.
*
* @param $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #required, #title, #id, #value, #description.
* Properties used: #required, #title, #id, #value, #description,
* #label_for.
*
* @ingroup themeable
*/
......@@ -4336,7 +4390,14 @@ function theme_form_element_label($variables) {
$attributes['class'] = 'element-invisible';
}
if (!empty($element['#id'])) {
// Use the element's ID as the default value of the "for" attribute (to
// associate the label with this form element), but allow this to be
// overridden in order to associate the label with a different form element
// instead.
if (!empty($element['#label_for'])) {
$attributes['for'] = $element['#label_for'];
}
elseif (!empty($element['#id'])) {
$attributes['for'] = $element['#id'];
}
......
......@@ -1505,8 +1505,19 @@ function install_configure_form($form, &$form_state, &$install_state) {
// especially out of place on the last page of the installer, where it would
// distract from the message that the Drupal installation has completed
// successfully.)
if (empty($_POST) && (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_file, FILE_EXIST|FILE_READABLE|FILE_NOT_WRITABLE) || !drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_dir, FILE_NOT_WRITABLE, 'dir'))) {
drupal_set_message(st('All necessary changes to %dir and %file have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the <a href="@handbook_url">online handbook</a>.', array('%dir' => $settings_dir, '%file' => $settings_file, '@handbook_url' => 'http://drupal.org/server-permissions')), 'warning');
$skip_permissions_hardening = variable_get('skip_permissions_hardening', FALSE);
// Allow system administrators to ignore permissions hardening for the site
// directory. This allows additional files in the site directory to be
// updated when they are managed in a version control system.
if (!$skip_permissions_hardening) {
if (empty($_POST) && (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_file, FILE_EXIST | FILE_READABLE | FILE_NOT_WRITABLE) || !drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_dir, FILE_NOT_WRITABLE, 'dir'))) {
drupal_set_message(st('All necessary changes to %dir and %file have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the <a href="@handbook_url">online handbook</a>.', array(
'%dir' => $settings_dir,
'%file' => $settings_file,
'@handbook_url' => 'http://drupal.org/server-permissions'
)), 'warning');
}
}
drupal_add_js(drupal_get_path('module', 'system') . '/system.js');
......
......@@ -830,11 +830,14 @@ function drupal_uninstall_modules($module_list = array(), $uninstall_dependents
* An optional bitmask created from various FILE_* constants.
* @param $type
* The type of file. Can be file (default), dir, or link.
* @param bool $autofix
* (optional) Determines whether to attempt fixing the permissions according
* to the provided $mask. Defaults to TRUE.
*
* @return
* TRUE on success or FALSE on failure. A message is set for the latter.
*/
function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
function drupal_verify_install_file($file, $mask = NULL, $type = 'file', $autofix = TRUE) {
$return = TRUE;
// Check for files that shouldn't be there.
if (isset($mask) && ($mask & FILE_NOT_EXIST) && file_exists($file)) {
......@@ -856,7 +859,7 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
switch ($current_mask) {
case FILE_EXIST:
if (!file_exists($file)) {
if ($type == 'dir') {
if ($type == 'dir' && $autofix) {
drupal_install_mkdir($file, $mask);
}
if (!file_exists($file)) {
......@@ -865,32 +868,32 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
}
break;
case FILE_READABLE:
if (!is_readable($file) && !drupal_install_fix_file($file, $mask)) {
if (!is_readable($file)) {
$return = FALSE;
}
break;
case FILE_WRITABLE:
if (!is_writable($file) && !drupal_install_fix_file($file, $mask)) {
if (!is_writable($file)) {
$return = FALSE;
}
break;
case FILE_EXECUTABLE:
if (!is_executable($file) && !drupal_install_fix_file($file, $mask)) {
if (!is_executable($file)) {
$return = FALSE;
}
break;
case FILE_NOT_READABLE:
if (is_readable($file) && !drupal_install_fix_file($file, $mask)) {
if (is_readable($file)) {
$return = FALSE;
}
break;
case FILE_NOT_WRITABLE:
if (is_writable($file) && !drupal_install_fix_file($file, $mask)) {
if (is_writable($file)) {
$return = FALSE;
}
break;
case FILE_NOT_EXECUTABLE:
if (is_executable($file) && !drupal_install_fix_file($file, $mask)) {
if (is_executable($file)) {
$return = FALSE;
}
break;
......@@ -898,6 +901,9 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
}
}
}
if (!$return && $autofix) {
return drupal_install_fix_file($file, $mask);
}
return $return;
}
......
......@@ -327,7 +327,7 @@ function _locale_get_predefined_list() {
'da' => array('Danish', 'Dansk'),
'de' => array('German', 'Deutsch'),
'dv' => array('Maldivian'),
'dz' => array('Bhutani'),
'dz' => array('Dzongkha', 'རྫོང་ཁ'),
'ee' => array('Ewe', 'Ɛʋɛ'),
'el' => array('Greek', 'Ελληνικά'),
'en' => array('English'),
......
......@@ -615,7 +615,7 @@ function locale_add_language($langcode, $name = NULL, $native = NULL, $direction
'direction' => $direction,
'domain' => $domain,
'prefix' => $prefix,
'enabled' => $enabled,
'enabled' => $enabled ? 1 : 0,
))
->execute();
......@@ -1093,7 +1093,7 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
*
* @param $report
* Report array summarizing the number of changes done in the form:
* array(inserts, updates, deletes).
* array(additions, deletes, skips, updates).
* @param $langcode
* Language code to import string into.
* @param $context
......@@ -1603,7 +1603,7 @@ function _locale_parse_js_file($filepath) {
if ($source) {
// We already have this source string and now have to add the location
// to the location column, if this file is not yet present in there.
$locations = preg_split('~\s*;\s*~', $source->location);
$locations = preg_split('~\s*;\s*~', (string) $source->location);
if (!in_array($filepath, $locations)) {
$locations[] = $filepath;
......@@ -2058,6 +2058,14 @@ function _locale_rebuild_js($langcode = NULL) {
$translations[$data->context][$data->source] = $data->translation;
}
// Include custom string overrides.
$custom_strings = variable_get('locale_custom_strings_' . $language->language, array());
foreach ($custom_strings as $context => $strings) {
foreach ($strings as $source => $translation) {
$translations[$context][$source] = $translation;
}
}
// Construct the JavaScript file, if there are translations.
$data_hash = NULL;
$data = $status = '';
......
......@@ -17,15 +17,14 @@
* hook implementations to get and process data from all active modules, and
* then delete the current data in the database to insert the new afterwards.
*
* This is a cooperative, advisory lock system. Any long-running operation
* that could potentially be attempted in parallel by multiple requests should
* try to acquire a lock before proceeding. By obtaining a lock, one request
* notifies any other requests that a specific operation is in progress which
* must not be executed in parallel.
* To avoid these types of conflicts, Drupal has a cooperative, advisory lock
* system. Any long-running operation that could potentially be attempted in
* parallel by multiple requests should try to acquire a lock before
* proceeding. By obtaining a lock, one request notifies any other requests that
* a specific operation is in progress which must not be executed in parallel.
*
* To use this API, pick a unique name for the lock. A sensible choice is the
* name of the function performing the operation. A very simple example use of
* this API:
* name of the function performing the operation. Here is a simple example:
* @code
* function mymodule_long_operation() {
* if (lock_acquire('mymodule_long_operation')) {
......@@ -54,6 +53,26 @@
* lock_acquire() and lock_wait() will automatically break (delete) a lock
* whose duration has exceeded the timeout specified when it was acquired.
*
* The following limitations in this implementation should be carefully noted:
* - Time: Timestamps are derived from the local system clock of the environment
* the code is executing in. The orderly progression of time from this
* viewpoint can be disrupted by external events such as NTP synchronization
* and operator intervention. Where multiple web servers are involved in
* serving the site, they will have their own independent clocks, introducing
* another source of error in the time keeping process. Timeout values applied
* to locks must therefore be considered approximate, and should not be relied
* upon.
* - Uniqueness: Uniqueness of lock names is not enforced. The impact of the
* use of a common lock name will depend on what processes and resources the
* lock is being used to manage.
* - Sharing: There is limited support for resources shared across sites.
* The locks are stored as rows in the semaphore table and, as such, they
* have the same visibility as the table. If resources managed by a lock are
* shared across sites then the semaphore table must be shared across sites
* as well. This is a binary situation: either all resources are shared and
* the semaphore table is shared or no resources are shared and the semaphore
* table is not shared. Mixed mode operation is not supported.
*
* Alternative implementations of this API (such as APC) may be substituted
* by setting the 'lock_inc' variable to an alternate include filepath. Since
* this is an API intended to support alternative implementations, code using
......
......@@ -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 <xyz@example.org>. 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.
......