Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • develop
  • issue-424
  • master
  • master-no-logins
  • svn-staging
  • svn-testing
  • svn-trunk
  • topics/add-contribution-info
  • 2010-11-11
  • 2010-12-15
  • 2011-01-11
  • 2011-01-18
  • 2011-01-26
  • 2011-02-10
  • 2011-02-23
  • 2011-03-09
  • 2011-03-15
  • 2011-03-30
  • 2011-04-05
  • 2011-05-03
  • 2011-05-12
  • 2011-06-16
  • 2011-06-21
  • 2011-06-23
  • 2011-06-29
  • 2011-06-30
  • 2011-07-11
  • 2011-07-18
  • 2011-07-20
  • 2011-07-21
  • 2011-07-28
  • 2011-08-03
  • 2011-08-05
  • 2011-08-15
  • 2011-08-17
  • 2011-08-29
  • 2011-08-30
  • 2011-09-19
  • 2011-10-03
  • 2011-10-06
  • 2011-10-27
  • 2011-11-01
  • 2011-11-08
  • 2011-11-08.2
  • 2011-11-14
  • 2011-11-17
  • 2011-12-05
  • 2011-12-16
  • 2012-01-12
  • 2012-01-13
  • 2012-02-07
  • 2012-03-01
  • 2012-04-02
  • 2012-04-03
  • 2012-04-18
  • 20120207
  • 7.1
  • 7.10
  • 7.11
  • 7.12
  • 7.13
  • 7.14
  • 7.15
  • 7.16
  • 7.16.1
  • 7.17
  • 7.18
  • 7.19
  • 7.2
  • 7.3
  • 7.3.1
  • 7.4
  • 7.5
  • 7.5.1
  • 7.6
  • 7.6.1
  • 7.7
  • 7.7.1
  • 7.7.2
  • 7.7.3
  • 7.8
  • 7.9
82 results

Target

Select target project
  • rklusman2/UNL-CMS
  • yzha1/UNL-CMS
  • pear/UNL-CMS
  • bbieber2/UNL-CMS
  • tsteiner2/UNL-CMS
  • erasmussen2/UNL-CMS
  • UNL-Information-Services/UNL-CMS
7 results
Select Git revision
  • develop
  • issue-424
  • issue-525
  • master
  • master-no-logins
  • svn-staging
  • svn-testing
  • svn-trunk
  • topics/add-contribution-info
  • 2010-11-11
  • 2010-12-15
  • 2011-01-11
  • 2011-01-18
  • 2011-01-26
  • 2011-02-10
  • 2011-02-23
  • 2011-03-09
  • 2011-03-15
  • 2011-03-30
  • 2011-04-05
  • 2011-05-03
  • 2011-05-12
  • 2011-06-16
  • 2011-06-21
  • 2011-06-23
  • 2011-06-29
  • 2011-06-30
  • 2011-07-11
  • 2011-07-18
  • 2011-07-20
  • 2011-07-21
  • 2011-07-28
  • 2011-08-03
  • 2011-08-05
  • 2011-08-15
  • 2011-08-17
  • 2011-08-29
  • 2011-08-30
  • 2011-09-19
  • 2011-10-03
  • 2011-10-06
  • 2011-10-27
  • 2011-11-01
  • 2011-11-08
  • 2011-11-08.2
  • 2011-11-14
  • 2011-11-17
  • 2011-12-05
  • 2011-12-16
  • 2012-01-12
  • 2012-01-13
  • 2012-02-07
  • 2012-03-01
  • 2012-04-02
  • 2012-04-03
  • 2012-04-18
  • 20120207
  • 7.1
  • 7.2
  • 7.3
  • 7.3.1
  • 7.4
  • 7.5
  • 7.5.1
  • 7.6
  • 7.6.1
66 results
Show changes
Showing
with 2158 additions and 980 deletions
<?php <?php
// $Id: schema.inc,v 1.39 2010/08/22 13:55:53 dries Exp $
/** /**
* @file * @file
* Generic Database schema code. * Generic Database schema code.
*/ */
require_once dirname(__FILE__) . '/query.inc';
/** /**
* @defgroup schemaapi Schema API * @defgroup schemaapi Schema API
* @{ * @{
* API to handle database schemas.
* *
* A Drupal schema definition is an array structure representing one or * A Drupal schema definition is an array structure representing one or
* more tables and their related keys and indexes. A schema is defined by * more tables and their related keys and indexes. A schema is defined by
...@@ -38,10 +40,16 @@ ...@@ -38,10 +40,16 @@
* description might contain "Always holds the largest (most * description might contain "Always holds the largest (most
* recent) {node_revision}.vid value for this nid." * recent) {node_revision}.vid value for this nid."
* - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int', * - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
* 'float', 'numeric', 'serial', 'date', 'datetime' or 'time'. Most types * 'float', 'numeric', or 'serial'. Most types just map to the according
* types just map to the according database engine specific datatypes. Use * database engine specific datatypes. Use 'serial' for auto incrementing
* 'serial' for auto incrementing fields. This will expand to 'INT * fields. This will expand to 'INT auto_increment' on MySQL.
* auto_increment' on MySQL. * - 'mysql_type', 'pgsql_type', 'sqlite_type', etc.: If you need to
* use a record type not included in the officially supported list
* of types above, you can specify a type for each database
* backend. In this case, you can leave out the type parameter,
* but be advised that your schema will fail to load on backends that
* do not have a type specified. A possible solution can be to
* use the "text" type as a fallback.
* - 'serialize': A boolean indicating whether the field will be stored as * - 'serialize': A boolean indicating whether the field will be stored as
* a serialized string. * a serialized string.
* - 'size': The data size: 'tiny', 'small', 'medium', 'normal', * - 'size': The data size: 'tiny', 'small', 'medium', 'normal',
...@@ -68,8 +76,13 @@ ...@@ -68,8 +76,13 @@
* the precision (total number of significant digits) and scale * the precision (total number of significant digits) and scale
* (decimal digits right of the decimal point). Both values are * (decimal digits right of the decimal point). Both values are
* mandatory. Ignored for other field types. * mandatory. Ignored for other field types.
* - 'binary': A boolean indicating that MySQL should force 'char',
* 'varchar' or 'text' fields to use case-sensitive binary collation.
* This has no effect on other database types for which case sensitivity
* is already the default behavior.
* All parameters apart from 'type' are optional except that type * All parameters apart from 'type' are optional except that type
* 'numeric' columns must specify 'precision' and 'scale'. * 'numeric' columns must specify 'precision' and 'scale', and type
* 'varchar' must specify the 'length' parameter.
* - 'primary key': An array of one or more key column specifiers (see below) * - 'primary key': An array of one or more key column specifiers (see below)
* that form the primary key. * that form the primary key.
* - 'unique keys': An associative array of unique keys ('keyname' => * - 'unique keys': An associative array of unique keys ('keyname' =>
...@@ -168,10 +181,33 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface { ...@@ -168,10 +181,33 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
*/ */
protected $defaultSchema = 'public'; protected $defaultSchema = 'public';
/**
* A unique identifier for this query object.
*/
protected $uniqueIdentifier;
public function __construct($connection) { public function __construct($connection) {
$this->uniqueIdentifier = uniqid('', TRUE);
$this->connection = $connection; $this->connection = $connection;
} }
/**
* Implements the magic __clone function.
*/
public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);
}
/**
* Implements QueryPlaceHolderInterface::uniqueIdentifier().
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* Implements QueryPlaceHolderInterface::nextPlaceholder().
*/
public function nextPlaceholder() { public function nextPlaceholder() {
return $this->placeholder++; return $this->placeholder++;
} }
......
<?php <?php
// $Id: select.inc,v 1.52 2010/10/05 01:42:24 dries Exp $
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
require_once dirname(__FILE__) . '/query.inc';
/** /**
* Interface for extendable query objects. * Interface for extendable query objects.
* *
...@@ -413,7 +414,7 @@ interface SelectQueryInterface extends QueryConditionInterface, QueryAlterableIn ...@@ -413,7 +414,7 @@ interface SelectQueryInterface extends QueryConditionInterface, QueryAlterableIn
* @param $start * @param $start
* The first record from the result set to return. If NULL, removes any * The first record from the result set to return. If NULL, removes any
* range directives that are set. * range directives that are set.
* @param $limit * @param $length
* The number of records to return from the result set. * The number of records to return from the result set.
* @return SelectQueryInterface * @return SelectQueryInterface
* The called object. * The called object.
...@@ -547,18 +548,32 @@ class SelectQueryExtender implements SelectQueryInterface { ...@@ -547,18 +548,32 @@ class SelectQueryExtender implements SelectQueryInterface {
*/ */
protected $connection; protected $connection;
/**
* A unique identifier for this query object.
*/
protected $uniqueIdentifier;
/** /**
* The placeholder counter. * The placeholder counter.
*/ */
protected $placeholder = 0; protected $placeholder = 0;
public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) { public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
$this->uniqueIdentifier = uniqid('', TRUE);
$this->query = $query; $this->query = $query;
$this->connection = $connection; $this->connection = $connection;
} }
/* Implementations of QueryPlaceholderInterface. */ /**
* Implements QueryPlaceholderInterface::uniqueIdentifier().
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* Implements QueryPlaceholderInterface::nextPlaceholder().
*/
public function nextPlaceholder() { public function nextPlaceholder() {
return $this->placeholder++; return $this->placeholder++;
} }
...@@ -575,11 +590,13 @@ class SelectQueryExtender implements SelectQueryInterface { ...@@ -575,11 +590,13 @@ class SelectQueryExtender implements SelectQueryInterface {
} }
public function hasAllTags() { public function hasAllTags() {
return call_user_func_array(array($this->query, 'hasAllTags'), func_get_args()); $args = func_get_args();
return call_user_func_array(array($this->query, 'hasAllTags'), $args);
} }
public function hasAnyTag() { public function hasAnyTag() {
return call_user_func_array(array($this->query, 'hasAnyTags'), func_get_args()); $args = func_get_args();
return call_user_func_array(array($this->query, 'hasAnyTags'), $args);
} }
public function addMetaData($key, $object) { public function addMetaData($key, $object) {
...@@ -611,23 +628,27 @@ class SelectQueryExtender implements SelectQueryInterface { ...@@ -611,23 +628,27 @@ class SelectQueryExtender implements SelectQueryInterface {
return $this; return $this;
} }
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) { public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, isset($queryPlaceholder) ? $queryPlaceholder : $this); return $this->query->compile($connection, $queryPlaceholder);
}
public function compiled() {
return $this->query->compiled();
} }
/* Implementations of QueryConditionInterface for the HAVING clause. */ /* Implementations of QueryConditionInterface for the HAVING clause. */
public function havingCondition($field, $value = NULL, $operator = '=') { public function havingCondition($field, $value = NULL, $operator = '=') {
$this->query->condition($field, $value, $operator, $num_args); $this->query->havingCondition($field, $value, $operator);
return $this; return $this;
} }
public function &havingConditions() { public function &havingConditions() {
return $this->having->conditions(); return $this->query->havingConditions();
} }
public function havingArguments() { public function havingArguments() {
return $this->having->arguments(); return $this->query->havingArguments();
} }
public function having($snippet, $args = array()) { public function having($snippet, $args = array()) {
...@@ -642,7 +663,9 @@ class SelectQueryExtender implements SelectQueryInterface { ...@@ -642,7 +663,9 @@ class SelectQueryExtender implements SelectQueryInterface {
/* Implementations of QueryExtendableInterface. */ /* Implementations of QueryExtendableInterface. */
public function extend($extender_name) { public function extend($extender_name) {
$class = $this->connection->getDriverClass($extender_name); // The extender can be anywhere so this needs to go to the registry, which
// is surely loaded by now.
$class = $this->connection->getDriverClass($extender_name, array(), TRUE);
return new $class($this, $this->connection); return new $class($this, $this->connection);
} }
...@@ -769,31 +792,7 @@ class SelectQueryExtender implements SelectQueryInterface { ...@@ -769,31 +792,7 @@ class SelectQueryExtender implements SelectQueryInterface {
} }
public function countQuery() { public function countQuery() {
// Create our new query object that we will mutate into a count query. return $this->query->countQuery();
$count = clone($this);
// Zero-out existing fields and expressions.
$fields =& $count->getFields();
$fields = array();
$expressions =& $count->getExpressions();
$expressions = array();
// Also remove 'all_fields' statements, which are expanded into tablename.*
// when the query is executed.
$tables = &$count->getTables();
foreach ($tables as $alias => &$table) {
unset($table['all_fields']);
}
// Ordering a count query is a waste of cycles, and breaks on some
// databases anyway.
$orders = &$count->getOrderBy();
$orders = array();
// COUNT() is an expression, so we add that back in.
$count->addExpression('COUNT(*)');
return $count;
} }
function isNull($field) { function isNull($field) {
...@@ -806,11 +805,23 @@ class SelectQueryExtender implements SelectQueryInterface { ...@@ -806,11 +805,23 @@ class SelectQueryExtender implements SelectQueryInterface {
return $this; return $this;
} }
public function exists(SelectQueryInterface $select) {
$this->query->exists($select);
return $this;
}
public function notExists(SelectQueryInterface $select) {
$this->query->notExists($select);
return $this;
}
public function __toString() { public function __toString() {
return (string) $this->query; return (string) $this->query;
} }
public function __clone() { public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);
// We need to deep-clone the query we're wrapping, which in turn may // We need to deep-clone the query we're wrapping, which in turn may
// deep-clone other objects. Exciting! // deep-clone other objects. Exciting!
$this->query = clone($this->query); $this->query = clone($this->query);
...@@ -972,11 +983,13 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -972,11 +983,13 @@ class SelectQuery extends Query implements SelectQueryInterface {
} }
public function hasAllTags() { public function hasAllTags() {
return !(boolean)array_diff(func_get_args(), array_keys($this->alterTags)); $args = func_get_args();
return !(boolean)array_diff($args, array_keys($this->alterTags));
} }
public function hasAnyTag() { public function hasAnyTag() {
return (boolean)array_intersect(func_get_args(), array_keys($this->alterTags)); $args = func_get_args();
return (boolean)array_intersect($args, array_keys($this->alterTags));
} }
public function addMetaData($key, $object) { public function addMetaData($key, $object) {
...@@ -1000,7 +1013,35 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1000,7 +1013,35 @@ class SelectQuery extends Query implements SelectQueryInterface {
} }
public function arguments() { public function arguments() {
return $this->where->arguments(); if (!$this->compiled()) {
return NULL;
}
$args = $this->where->arguments() + $this->having->arguments();
foreach ($this->tables as $table) {
if ($table['arguments']) {
$args += $table['arguments'];
}
// If this table is a subquery, grab its arguments recursively.
if ($table['table'] instanceof SelectQueryInterface) {
$args += $table['table']->arguments();
}
}
foreach ($this->expressions as $expression) {
if ($expression['arguments']) {
$args += $expression['arguments'];
}
}
// If there are any dependent queries to UNION,
// incorporate their arguments recursively.
foreach ($this->union as $union) {
$args += $union['query']->arguments();
}
return $args;
} }
public function where($snippet, $args = array()) { public function where($snippet, $args = array()) {
...@@ -1018,8 +1059,54 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1018,8 +1059,54 @@ class SelectQuery extends Query implements SelectQueryInterface {
return $this; return $this;
} }
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) { public function exists(SelectQueryInterface $select) {
return $this->where->compile($connection, isset($queryPlaceholder) ? $queryPlaceholder : $this); $this->where->exists($select);
return $this;
}
public function notExists(SelectQueryInterface $select) {
$this->where->notExists($select);
return $this;
}
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
$this->where->compile($connection, $queryPlaceholder);
$this->having->compile($connection, $queryPlaceholder);
foreach ($this->tables as $table) {
// If this table is a subquery, compile it recursively.
if ($table['table'] instanceof SelectQueryInterface) {
$table['table']->compile($connection, $queryPlaceholder);
}
}
// If there are any dependent queries to UNION, compile it recursively.
foreach ($this->union as $union) {
$union['query']->compile($connection, $queryPlaceholder);
}
}
public function compiled() {
if (!$this->where->compiled() || !$this->having->compiled()) {
return FALSE;
}
foreach ($this->tables as $table) {
// If this table is a subquery, check its status recursively.
if ($table['table'] instanceof SelectQueryInterface) {
if (!$table['table']->compiled()) {
return FALSE;
}
}
}
foreach ($this->union as $union) {
if (!$union['query']->compiled()) {
return FALSE;
}
}
return TRUE;
} }
/* Implementations of QueryConditionInterface for the HAVING clause. */ /* Implementations of QueryConditionInterface for the HAVING clause. */
...@@ -1066,6 +1153,16 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1066,6 +1153,16 @@ class SelectQuery extends Query implements SelectQueryInterface {
return $this; return $this;
} }
public function havingExists(SelectQueryInterface $select) {
$this->having->exists($select);
return $this;
}
public function havingNotExists(SelectQueryInterface $select) {
$this->having->notExists($select);
return $this;
}
public function forUpdate($set = TRUE) { public function forUpdate($set = TRUE) {
if (isset($set)) { if (isset($set)) {
$this->forUpdate = $set; $this->forUpdate = $set;
...@@ -1103,33 +1200,8 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1103,33 +1200,8 @@ class SelectQuery extends Query implements SelectQueryInterface {
if (!isset($queryPlaceholder)) { if (!isset($queryPlaceholder)) {
$queryPlaceholder = $this; $queryPlaceholder = $this;
} }
$this->where->compile($this->connection, $queryPlaceholder); $this->compile($this->connection, $queryPlaceholder);
$this->having->compile($this->connection, $queryPlaceholder); return $this->arguments();
$args = $this->where->arguments() + $this->having->arguments();
foreach ($this->tables as $table) {
if ($table['arguments']) {
$args += $table['arguments'];
}
// If this table is a subquery, grab its arguments recursively.
if ($table['table'] instanceof SelectQueryInterface) {
$args += $table['table']->getArguments($queryPlaceholder);
}
}
foreach ($this->expressions as $expression) {
if ($expression['arguments']) {
$args += $expression['arguments'];
}
}
// If there are any dependent queries to UNION,
// incorporate their arguments recursively.
foreach ($this->union as $union) {
$args += $union['query']->getArguments($queryPlaceholder);
}
return $args;
} }
/** /**
...@@ -1350,7 +1422,7 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1350,7 +1422,7 @@ class SelectQuery extends Query implements SelectQueryInterface {
} }
public function groupBy($field) { public function groupBy($field) {
$this->group[] = $field; $this->group[$field] = $field;
return $this; return $this;
} }
...@@ -1358,22 +1430,25 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1358,22 +1430,25 @@ class SelectQuery extends Query implements SelectQueryInterface {
// Create our new query object that we will mutate into a count query. // Create our new query object that we will mutate into a count query.
$count = clone($this); $count = clone($this);
$group_by = array_keys($count->getGroupBy()); $group_by = $count->getGroupBy();
$having = $count->havingConditions();
if (!$count->distinct) { if (!$count->distinct && !isset($having[0])) {
// When not executing a distinct query, we can zero-out existing fields // When not executing a distinct query, we can zero-out existing fields
// and expressions that are not used by a GROUP BY. Fields listed in // and expressions that are not used by a GROUP BY or HAVING. Fields
// the GROUP BY clause need to be present in the query. // listed in a GROUP BY or HAVING clause need to be present in the
// query.
$fields =& $count->getFields(); $fields =& $count->getFields();
foreach (array_keys($fields) as $field) { foreach (array_keys($fields) as $field) {
if (!empty($group_by[$field])) { if (empty($group_by[$field])) {
unset($fields[$field]); unset($fields[$field]);
} }
} }
$expressions =& $count->getExpressions(); $expressions =& $count->getExpressions();
foreach (array_keys($expressions) as $field) { foreach (array_keys($expressions) as $field) {
if (!empty($group_by[$field])) { if (empty($group_by[$field])) {
unset($fields[$field]); unset($expressions[$field]);
} }
} }
...@@ -1406,9 +1481,16 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1406,9 +1481,16 @@ class SelectQuery extends Query implements SelectQueryInterface {
} }
public function __toString() { public function __toString() {
// For convenience, we compile the query ourselves if the caller forgot
// to do it. This allows constructs like "(string) $query" to work. When
// the query will be executed, it will be recompiled using the proper
// placeholder generator anyway.
if (!$this->compiled()) {
$this->compile($this->connection, $this);
}
// Create a comments string to prepend to the query. // Create a sanitized comment string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; $comments = $this->connection->makeComment($this->comments);
// SELECT // SELECT
$query = $comments . 'SELECT '; $query = $comments . 'SELECT ';
...@@ -1464,14 +1546,6 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1464,14 +1546,6 @@ class SelectQuery extends Query implements SelectQueryInterface {
// WHERE // WHERE
if (count($this->where)) { if (count($this->where)) {
// The following line will not generate placeholders correctly if there
// is a subquery. Fortunately, it is also called from getArguments() first
// so it's not a problem in practice... unless you try to call __toString()
// before calling getArguments(). That is a problem that we will have to
// fix in Drupal 8, because it requires more refactoring than we are
// able to do in Drupal 7.
// @todo Move away from __toString() For SelectQuery compilation at least.
$this->where->compile($this->connection, $this);
// There is an implicit string cast on $this->condition. // There is an implicit string cast on $this->condition.
$query .= "\nWHERE " . $this->where; $query .= "\nWHERE " . $this->where;
} }
...@@ -1483,7 +1557,6 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1483,7 +1557,6 @@ class SelectQuery extends Query implements SelectQueryInterface {
// HAVING // HAVING
if (count($this->having)) { if (count($this->having)) {
$this->having->compile($this->connection, $this);
// There is an implicit string cast on $this->having. // There is an implicit string cast on $this->having.
$query .= "\nHAVING " . $this->having; $query .= "\nHAVING " . $this->having;
} }
...@@ -1536,5 +1609,5 @@ class SelectQuery extends Query implements SelectQueryInterface { ...@@ -1536,5 +1609,5 @@ class SelectQuery extends Query implements SelectQueryInterface {
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */
<?php <?php
// $Id: database.inc,v 1.39 2010/10/03 01:29:41 dries Exp $
/** /**
* @file * @file
...@@ -7,7 +6,7 @@ ...@@ -7,7 +6,7 @@
*/ */
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
...@@ -24,7 +23,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { ...@@ -24,7 +23,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
* Version of sqlite lower then 3.6.8 can't use savepoints. * Version of sqlite lower then 3.6.8 can't use savepoints.
* See http://www.sqlite.org/releaselog/3_6_8.html * See http://www.sqlite.org/releaselog/3_6_8.html
* *
* @var bool * @var boolean
*/ */
protected $savepointSupport = FALSE; protected $savepointSupport = FALSE;
...@@ -35,23 +34,63 @@ class DatabaseConnection_sqlite extends DatabaseConnection { ...@@ -35,23 +34,63 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
*/ */
protected $willRollback; protected $willRollback;
/**
* All databases attached to the current database. This is used to allow
* prefixes to be safely handled without locking the table
*
* @var array
*/
protected $attachedDatabases = array();
/**
* Whether or not a table has been dropped this request: the destructor will
* only try to get rid of unnecessary databases if there is potential of them
* being empty.
*
* This variable is set to public because DatabaseSchema_sqlite needs to
* access it. However, it should not be manually set.
*
* @var boolean
*/
var $tableDropped = FALSE;
public function __construct(array $connection_options = array()) { public function __construct(array $connection_options = array()) {
// We don't need a specific PDOStatement class here, we simulate it below. // We don't need a specific PDOStatement class here, we simulate it below.
$this->statementClass = NULL; $this->statementClass = NULL;
// This driver defaults to transaction support, except if explicitly passed FALSE. // This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE; $this->transactionSupport = $this->transactionalDDLSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
$this->connectionOptions = $connection_options; $this->connectionOptions = $connection_options;
parent::__construct('sqlite:' . $connection_options['database'], '', '', array( // Allow PDO options to be overridden.
// Force column names to lower case. $connection_options += array(
PDO::ATTR_CASE => PDO::CASE_LOWER, 'pdo' => array(),
);
$connection_options['pdo'] += array(
// Convert numeric values to strings when fetching. // Convert numeric values to strings when fetching.
PDO::ATTR_STRINGIFY_FETCHES => TRUE, PDO::ATTR_STRINGIFY_FETCHES => TRUE,
)); );
parent::__construct('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
// Attach one database for each registered prefix.
$prefixes = $this->prefixes;
foreach ($prefixes as $table => &$prefix) {
// Empty prefix means query the main database -- no need to attach anything.
if (!empty($prefix)) {
// Only attach the database once.
if (!isset($this->attachedDatabases[$prefix])) {
$this->attachedDatabases[$prefix] = $prefix;
$this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'] . '-' . $prefix, ':prefix' => $prefix));
}
$this->exec('PRAGMA encoding="UTF-8"'); // Add a ., so queries become prefix.table, which is proper syntax for
// querying an attached database.
$prefix .= '.';
}
}
// Regenerate the prefixes replacement table.
$this->setPrefix($prefixes);
// Detect support for SAVEPOINT. // Detect support for SAVEPOINT.
$version = $this->query('SELECT sqlite_version()')->fetchField(); $version = $this->query('SELECT sqlite_version()')->fetchField();
...@@ -67,6 +106,41 @@ class DatabaseConnection_sqlite extends DatabaseConnection { ...@@ -67,6 +106,41 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
$this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3); $this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
$this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3); $this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
$this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand')); $this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand'));
// Execute sqlite init_commands.
if (isset($connection_options['init_commands'])) {
$this->exec(implode('; ', $connection_options['init_commands']));
}
}
/**
* Destructor for the SQLite connection.
*
* We prune empty databases on destruct, but only if tables have been
* dropped. This is especially needed when running the test suite, which
* creates and destroy databases several times in a row.
*/
public function __destruct() {
if ($this->tableDropped && !empty($this->attachedDatabases)) {
foreach ($this->attachedDatabases as $prefix) {
// Check if the database is now empty, ignore the internal SQLite tables.
try {
$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.
unlink($this->connectionOptions['database'] . '-' . $prefix);
}
}
catch (Exception $e) {
// Ignore the exception and continue. There is nothing we can do here
// to report the error or fail safe.
}
}
}
} }
/** /**
...@@ -172,7 +246,9 @@ class DatabaseConnection_sqlite extends DatabaseConnection { ...@@ -172,7 +246,9 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
// Generate a new temporary table name and protect it from prefixing. // Generate a new temporary table name and protect it from prefixing.
// SQLite requires that temporary tables to be non-qualified. // SQLite requires that temporary tables to be non-qualified.
$tablename = $this->generateTemporaryTableName(); $tablename = $this->generateTemporaryTableName();
$this->prefixes[$tablename] = ''; $prefixes = $this->prefixes;
$prefixes[$tablename] = '';
$this->setPrefix($prefixes);
$this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options); $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options);
return $tablename; return $tablename;
...@@ -439,5 +515,5 @@ class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iter ...@@ -439,5 +515,5 @@ class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iter
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */
<?php <?php
// $Id: install.inc,v 1.2 2009/07/27 19:42:56 dries Exp $
/** /**
* @file * @file
...@@ -8,8 +7,45 @@ ...@@ -8,8 +7,45 @@
class DatabaseTasks_sqlite extends DatabaseTasks { class DatabaseTasks_sqlite extends DatabaseTasks {
protected $pdoDriver = 'sqlite'; protected $pdoDriver = 'sqlite';
public function name() { public function name() {
return 'SQLite'; return st('SQLite');
}
/**
* Minimum engine version.
*
* @todo: consider upping to 3.6.8 in Drupal 8 to get SAVEPOINT support.
*/
public function minimumVersion() {
return '3.3.7';
}
public function getFormOptions($database) {
$form = parent::getFormOptions($database);
// Remove the options that only apply to client/server style databases.
unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
// Make the text more accurate for SQLite.
$form['database']['#title'] = st('Database file');
$form['database']['#description'] = st('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name()));
$default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite';
$form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
return $form;
}
public function validateDatabaseSettings($database) {
// Perform standard validation.
$errors = parent::validateDatabaseSettings($database);
// Verify the database is writable.
$db_directory = new SplFileInfo(dirname($database['database']));
if (!$db_directory->isWritable()) {
$errors[$database['driver'] . '][database'] = st('The directory you specified is not writable by the web server.');
}
return $errors;
} }
} }
<?php <?php
// $Id: query.inc,v 1.13 2010/09/25 01:41:26 dries Exp $
/** /**
* @file * @file
...@@ -7,20 +6,10 @@ ...@@ -7,20 +6,10 @@
*/ */
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
/**
* SQLite specific query builder for SELECT statements.
*/
class SelectQuery_sqlite extends SelectQuery {
public function forUpdate($set = TRUE) {
// SQLite does not support FOR UPDATE so nothing to do.
return $this;
}
}
/** /**
* SQLite specific implementation of InsertQuery. * SQLite specific implementation of InsertQuery.
* *
...@@ -43,8 +32,8 @@ class InsertQuery_sqlite extends InsertQuery { ...@@ -43,8 +32,8 @@ class InsertQuery_sqlite extends InsertQuery {
} }
public function __toString() { public function __toString() {
// Create a comments string to prepend to the query. // Create a sanitized comment string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; $comments = $this->connection->makeComment($this->comments);
// Produce as many generic placeholders as necessary. // Produce as many generic placeholders as necessary.
$placeholders = array_fill(0, count($this->insertFields), '?'); $placeholders = array_fill(0, count($this->insertFields), '?');
...@@ -68,51 +57,30 @@ class InsertQuery_sqlite extends InsertQuery { ...@@ -68,51 +57,30 @@ class InsertQuery_sqlite extends InsertQuery {
* we don't select those rows. * we don't select those rows.
* *
* A query like this one: * A query like this one:
* UPDATE test SET name = 'newname' WHERE tid = 1 * UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1
* will become: * will become:
* UPDATE test SET name = 'newname' WHERE tid = 1 AND name <> 'newname' * UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1 AND (col1 <> 'newcol1' OR col2 <> 'newcol2')
*/ */
class UpdateQuery_sqlite extends UpdateQuery { class UpdateQuery_sqlite extends UpdateQuery {
/**
* Helper function that removes the fields that are already in a condition.
*
* @param $fields
* The fields.
* @param QueryConditionInterface $condition
* A database condition.
*/
protected function removeFieldsInCondition(&$fields, QueryConditionInterface $condition) {
foreach ($condition->conditions() as $child_condition) {
if ($child_condition['field'] instanceof QueryConditionInterface) {
$this->removeFieldsInCondition($fields, $child_condition['field']);
}
else {
unset($fields[$child_condition['field']]);
}
}
}
public function execute() { public function execute() {
if (!empty($this->queryOptions['sqlite_return_matched_rows'])) { if (!empty($this->queryOptions['sqlite_return_matched_rows'])) {
return parent::execute(); return parent::execute();
} }
// Get the fields used in the update query, and remove those that are already // Get the fields used in the update query.
// in the condition.
$fields = $this->expressionFields + $this->fields; $fields = $this->expressionFields + $this->fields;
$this->removeFieldsInCondition($fields, $this->condition);
// Add the inverse of the fields to the condition. // Add the inverse of the fields to the condition.
$condition = new DatabaseCondition('OR'); $condition = new DatabaseCondition('OR');
foreach ($fields as $field => $data) { foreach ($fields as $field => $data) {
if (is_array($data)) { if (is_array($data)) {
// The field is an expression. // The field is an expression.
$condition->condition($field, $data['expression'], '<>'); $condition->where($field . ' <> ' . $data['expression']);
$condition->isNull($field); $condition->isNull($field);
} }
elseif (!isset($data)) { elseif (!isset($data)) {
// The field will be set to NULL. // The field will be set to NULL.
$condition->isNull($field); $condition->isNotNull($field);
} }
else { else {
$condition->condition($field, $data, '<>'); $condition->condition($field, $data, '<>');
...@@ -159,13 +127,13 @@ class DeleteQuery_sqlite extends DeleteQuery { ...@@ -159,13 +127,13 @@ class DeleteQuery_sqlite extends DeleteQuery {
*/ */
class TruncateQuery_sqlite extends TruncateQuery { class TruncateQuery_sqlite extends TruncateQuery {
public function __toString() { public function __toString() {
// Create a comments string to prepend to the query. // Create a sanitized comment string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; $comments = $this->connection->makeComment($this->comments);
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
} }
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */
<?php <?php
// $Id: schema.inc,v 1.20 2010/10/22 15:18:56 webchick Exp $
/** /**
* @file * @file
...@@ -197,6 +196,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -197,6 +196,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
// $map does not use drupal_static as its value never changes. // $map does not use drupal_static as its value never changes.
static $map = array( static $map = array(
'varchar:normal' => 'VARCHAR', 'varchar:normal' => 'VARCHAR',
'char:normal' => 'CHAR',
'text:tiny' => 'TEXT', 'text:tiny' => 'TEXT',
'text:small' => 'TEXT', 'text:small' => 'TEXT',
...@@ -271,12 +271,12 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -271,12 +271,12 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
if (!$this->tableExists($table)) { if (!$this->tableExists($table)) {
return FALSE; return FALSE;
} }
$this->connection->tableDropped = TRUE;
$this->connection->query('DROP TABLE {' . $table . '}'); $this->connection->query('DROP TABLE {' . $table . '}');
return TRUE; return TRUE;
} }
public function addField($table, $field, $spec, $keys_new = array()) { public function addField($table, $field, $specification, $keys_new = array()) {
if (!$this->tableExists($table)) { if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table))); throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
} }
...@@ -284,10 +284,50 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -284,10 +284,50 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table))); throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
} }
// TODO: $keys_new is not supported yet. // SQLite doesn't have a full-featured ALTER TABLE statement. It only
$query = 'ALTER TABLE {' . $table . '} ADD '; // supports adding new fields to a table, in some simple cases. In most
$query .= $this->createFieldSql($field, $this->processField($spec)); // cases, we have to create a new table and copy the data over.
if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
// When we don't have to create new keys and we are not creating a
// NOT NULL column without a default value, we can use the quicker version.
$query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
$this->connection->query($query); $this->connection->query($query);
// Apply the initial value if set.
if (isset($specification['initial'])) {
$this->connection->update($table)
->fields(array($field => $specification['initial']))
->execute();
}
}
else {
// We cannot add the field directly. Use the slower table alteration
// method, starting from the old schema.
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
// Add the new field.
$new_schema['fields'][$field] = $specification;
// Build the mapping between the old fields and the new fields.
$mapping = array();
if (isset($specification['initial'])) {
// If we have a initial value, copy it over.
$mapping[$field] = array(
'expression' => ':newfieldinitial',
'arguments' => array(':newfieldinitial' => $specification['initial']),
);
}
else {
// Else use the default of the field.
$mapping[$field] = NULL;
}
// Add the new indexes.
$new_schema += $keys_new;
$this->alterTable($table, $old_schema, $new_schema, $mapping);
}
} }
/** /**
...@@ -303,7 +343,13 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -303,7 +343,13 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
* @param $new_schema * @param $new_schema
* The new schema array for the table. * The new schema array for the table.
* @param $mapping * @param $mapping
* An optional mapping between old fields => new fields. * An optional mapping between the fields of the old specification and the
* fields of the new specification. An associative array, whose keys are
* the fields of the new table, and values can take two possible forms:
* - a simple string, which is interpreted as the name of a field of the
* old table,
* - an associative array with two keys 'expression' and 'arguments',
* that will be used as an expression field.
*/ */
protected function alterTable($table, $old_schema, $new_schema, array $mapping = array()) { protected function alterTable($table, $old_schema, $new_schema, array $mapping = array()) {
$i = 0; $i = 0;
...@@ -313,22 +359,25 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -313,22 +359,25 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
$this->createTable($new_table, $new_schema); $this->createTable($new_table, $new_schema);
// Build a SQL query to migrate the data from the old table to the new.
$select = $this->connection->select($table);
// Complete the mapping. // Complete the mapping.
$old_keys = array_keys($old_schema['fields']); $possible_keys = array_keys($new_schema['fields']);
$mapping += array_combine($old_keys, $old_keys); $mapping += array_combine($possible_keys, $possible_keys);
$select = $this->connection->select($table); // Now add the fields.
$fields = &$select->getFields(); foreach ($mapping as $field_alias => $field_source) {
// Just ignore this field (ie. use it's default value).
// Map the fields. if (!isset($field_source)) {
foreach ($old_keys as $key) { continue;
if (isset($mapping[$key])) { }
// Don't use ->addField() here because it messes-up with the aliases.
$fields[$mapping[$key]] = array( if (is_array($field_source)) {
'field' => $key, $select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
'table' => $table, }
'alias' => $mapping[$key], else {
); $select->addField($table, $field_source, $field_alias);
} }
} }
...@@ -427,8 +476,6 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -427,8 +476,6 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
$old_schema = $this->introspectSchema($table); $old_schema = $this->introspectSchema($table);
$new_schema = $old_schema; $new_schema = $old_schema;
$mapping = array($field => NULL);
unset($new_schema['fields'][$field]); unset($new_schema['fields'][$field]);
foreach ($new_schema['indexes'] as $index => $fields) { foreach ($new_schema['indexes'] as $index => $fields) {
foreach ($fields as $key => $field_name) { foreach ($fields as $key => $field_name) {
...@@ -441,7 +488,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -441,7 +488,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
unset($new_schema['indexes'][$index]); unset($new_schema['indexes'][$index]);
} }
} }
$this->alterTable($table, $old_schema, $new_schema, $mapping); $this->alterTable($table, $old_schema, $new_schema);
return TRUE; return TRUE;
} }
...@@ -458,7 +505,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -458,7 +505,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
// Map the old field to the new field. // Map the old field to the new field.
if ($field != $field_new) { if ($field != $field_new) {
$mapping[$field] = $field_new; $mapping[$field_new] = $field;
} }
else { else {
$mapping = array(); $mapping = array();
...@@ -622,9 +669,14 @@ class DatabaseSchema_sqlite extends DatabaseSchema { ...@@ -622,9 +669,14 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
} }
public function findTables($table_expression) { public function findTables($table_expression) {
// Don't use {} around sqlite_master table. // Don't add the prefix, $table_expression already includes the prefix.
$result = db_query("SELECT name FROM sqlite_master WHERE name LIKE :table_name", array( $info = $this->getPrefixInfo($table_expression, FALSE);
':table_name' => $table_expression,
// Can't use query placeholders for the schema because the query would have
// to be :prefixsqlite_master, which does not work.
$result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
':type' => 'table',
':table_name' => $info['table'],
)); ));
return $result->fetchAllKeyed(0, 0); return $result->fetchAllKeyed(0, 0);
} }
......
<?php
/**
* @file
* Select builder for SQLite embedded database engine.
*/
/**
* @addtogroup database
* @{
*/
/**
* SQLite specific query builder for SELECT statements.
*/
class SelectQuery_sqlite extends SelectQuery {
public function forUpdate($set = TRUE) {
// SQLite does not support FOR UPDATE so nothing to do.
return $this;
}
}
/**
* @} End of "addtogroup database".
*/
<?php <?php
// $Id: date.inc,v 1.1 2009/10/13 21:34:14 dries Exp $
/** /**
* @file * @file
* Initialize the list of date formats and their locales. * Initializes the list of date formats and their locales.
*/ */
/** /**
* Implements hook_date_formats(). * Provides a default system list of date formats for system_date_formats().
*/ */
function system_default_date_formats() { function system_default_date_formats() {
$formats = array(); $formats = array();
......
<?php <?php
// $Id: entity.inc,v 1.16 2010/10/09 02:36:46 webchick Exp $
/** /**
* Interface for entity controller classes. * Interface for entity controller classes.
...@@ -24,8 +23,12 @@ interface DrupalEntityControllerInterface { ...@@ -24,8 +23,12 @@ interface DrupalEntityControllerInterface {
/** /**
* Resets the internal, static entity cache. * Resets the internal, static entity cache.
*
* @param $ids
* (optional) If specified, the cache is reset for the entities with the
* given ids only.
*/ */
public function resetCache(); public function resetCache(array $ids = NULL);
/** /**
* Loads one or more entities. * Loads one or more entities.
...@@ -36,7 +39,8 @@ interface DrupalEntityControllerInterface { ...@@ -36,7 +39,8 @@ interface DrupalEntityControllerInterface {
* An array of conditions in the form 'field' => $value. * An array of conditions in the form 'field' => $value.
* *
* @return * @return
* An array of entity objects indexed by their ids. * An array of entity objects indexed by their ids. When no results are
* found, an empty array is returned.
*/ */
public function load($ids = array(), $conditions = array()); public function load($ids = array(), $conditions = array());
} }
...@@ -139,9 +143,16 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface { ...@@ -139,9 +143,16 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
/** /**
* Implements DrupalEntityControllerInterface::resetCache(). * Implements DrupalEntityControllerInterface::resetCache().
*/ */
public function resetCache() { public function resetCache(array $ids = NULL) {
if (isset($ids)) {
foreach ($ids as $id) {
unset($this->entityCache[$id]);
}
}
else {
$this->entityCache = array(); $this->entityCache = array();
} }
}
/** /**
* Implements DrupalEntityControllerInterface::load(). * Implements DrupalEntityControllerInterface::load().
...@@ -285,6 +296,7 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface { ...@@ -285,6 +296,7 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
/** /**
* Attaches data to entities upon loading. * Attaches data to entities upon loading.
*
* This will attach fields, if the entity is fieldable. It calls * This will attach fields, if the entity is fieldable. It calls
* hook_entity_load() for modules which need to add data to all entities. * hook_entity_load() for modules which need to add data to all entities.
* It also calls hook_TYPE_load() on the loaded entities. For example * It also calls hook_TYPE_load() on the loaded entities. For example
...@@ -296,7 +308,7 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface { ...@@ -296,7 +308,7 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
* @param $queried_entities * @param $queried_entities
* Associative array of query results, keyed on the entity ID. * Associative array of query results, keyed on the entity ID.
* @param $revision_id * @param $revision_id
* ID of the revision that was loaded, or FALSE if teh most current revision * ID of the revision that was loaded, or FALSE if the most current revision
* was loaded. * was loaded.
*/ */
protected function attachLoad(&$queried_entities, $revision_id = FALSE) { protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
...@@ -398,19 +410,20 @@ class EntityFieldQueryException extends Exception {} ...@@ -398,19 +410,20 @@ class EntityFieldQueryException extends Exception {}
* direct access to the collected properties in order to handle alternate * direct access to the collected properties in order to handle alternate
* execution routines. We therefore use public properties for simplicity. Note * execution routines. We therefore use public properties for simplicity. Note
* that code that is simply creating and running a field query should still use * that code that is simply creating and running a field query should still use
* the appropriate methods add conditions on the query. * the appropriate methods to add conditions on the query.
* *
* Storage engines are not required to support every type of query. By default, * Storage engines are not required to support every type of query. By default,
* an EntityFieldQueryException will be raised if an unsupported condition is * an EntityFieldQueryException will be raised if an unsupported condition is
* specified or if the query has field conditions or sorts that are stored in * specified or if the query has field conditions or sorts that are stored in
* different field storage engines. However, this logic can be overridden in * different field storage engines. However, this logic can be overridden in
* hook_entity_query(). * hook_entity_query_alter().
* *
* Also note that this query does not automatically respect entity access * Also note that this query does not automatically respect entity access
* restrictions. Node access control is performed by the SQL storage engine but * restrictions. Node access control is performed by the SQL storage engine but
* other storage engines might not do this. * other storage engines might not do this.
*/ */
class EntityFieldQuery { class EntityFieldQuery {
/** /**
* Indicates that both deleted and non-deleted fields should be returned. * Indicates that both deleted and non-deleted fields should be returned.
* *
...@@ -418,6 +431,16 @@ class EntityFieldQuery { ...@@ -418,6 +431,16 @@ class EntityFieldQuery {
*/ */
const RETURN_ALL = NULL; const RETURN_ALL = NULL;
/**
* TRUE if the query has already been altered, FALSE if it hasn't.
*
* Used in alter hooks to check for cloned queries that have already been
* altered prior to the clone (for example, the pager count query).
*
* @var boolean
*/
public $altered = FALSE;
/** /**
* Associative array of entity-generic metadata conditions. * Associative array of entity-generic metadata conditions.
* *
...@@ -436,6 +459,21 @@ class EntityFieldQuery { ...@@ -436,6 +459,21 @@ class EntityFieldQuery {
*/ */
public $fieldConditions = array(); public $fieldConditions = array();
/**
* List of field meta conditions (language and delta).
*
* Field conditions operate on columns specified by hook_field_schema(),
* the meta conditions operate on columns added by the system: delta
* and language. These can not be mixed with the field conditions because
* field columns can have any name including delta and language.
*
* @var array
*
* @see EntityFieldQuery::fieldLanguageCondition()
* @see EntityFieldQuery::fieldDeltaCondition()
*/
public $fieldMetaConditions = array();
/** /**
* List of property conditions. * List of property conditions.
* *
...@@ -461,6 +499,15 @@ class EntityFieldQuery { ...@@ -461,6 +499,15 @@ class EntityFieldQuery {
*/ */
public $range = array(); public $range = array();
/**
* The query pager data.
*
* @var array
*
* @see EntityFieldQuery::pager()
*/
public $pager = array();
/** /**
* Query behavior for deleted data. * Query behavior for deleted data.
* *
...@@ -549,6 +596,8 @@ class EntityFieldQuery { ...@@ -549,6 +596,8 @@ class EntityFieldQuery {
* *
* 'bundle', 'revision_id' and 'entity_id' have no such restrictions. * 'bundle', 'revision_id' and 'entity_id' have no such restrictions.
* *
* Note: The "comment" entity type does not support bundle conditions.
*
* @param $name * @param $name
* 'entity_type', 'bundle', 'revision_id' or 'entity_id'. * 'entity_type', 'bundle', 'revision_id' or 'entity_id'.
* @param $value * @param $value
...@@ -557,18 +606,25 @@ class EntityFieldQuery { ...@@ -557,18 +606,25 @@ class EntityFieldQuery {
* dependent on $operator. * dependent on $operator.
* @param $operator * @param $operator
* Possible values: * Possible values:
* - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These * - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* operators expect $value to be a literal of the same type as the * operators expect $value to be a literal of the same type as the
* column. * column.
* - 'IN', 'NOT IN': These operators expect $value to be an array of * - 'IN', 'NOT IN': These operators expect $value to be an array of
* literals of the same type as the column. * literals of the same type as the column.
* - 'BETWEEN': This operator expects $value to be an array of two literals * - 'BETWEEN': This operator expects $value to be an array of two literals
* of the same type as the column. * of the same type as the column.
* The operator can be omitted, and will default to 'IN' if the value is an
* array, or to '=' otherwise.
* *
* @return EntityFieldQuery * @return EntityFieldQuery
* The called object. * The called object.
*/ */
public function entityCondition($name, $value, $operator = NULL) { public function entityCondition($name, $value, $operator = NULL) {
// The '!=' operator is deprecated in favour of the '<>' operator since the
// latter is ANSI SQL compatible.
if ($operator == '!=') {
$operator = '<>';
}
$this->entityConditions[$name] = array( $this->entityConditions[$name] = array(
'value' => $value, 'value' => $value,
'operator' => $operator, 'operator' => $operator,
...@@ -579,6 +635,91 @@ class EntityFieldQuery { ...@@ -579,6 +635,91 @@ class EntityFieldQuery {
/** /**
* Adds a condition on field values. * Adds a condition on field values.
* *
* Note that entities with empty field values will be excluded from the
* EntityFieldQuery results when using this method.
*
* @param $field
* Either a field name or a field array.
* @param $column
* The column that should hold the value to be matched.
* @param $value
* The value to test the column value against.
* @param $operator
* The operator to be used to test the given value.
* @param $delta_group
* An arbitrary identifier: conditions in the same group must have the same
* $delta_group.
* @param $language_group
* An arbitrary identifier: conditions in the same group must have the same
* $language_group.
*
* @return EntityFieldQuery
* The called object.
*
* @see EntityFieldQuery::addFieldCondition
* @see EntityFieldQuery::deleted
*/
public function fieldCondition($field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
return $this->addFieldCondition($this->fieldConditions, $field, $column, $value, $operator, $delta_group, $language_group);
}
/**
* Adds a condition on the field language column.
*
* @param $field
* Either a field name or a field array.
* @param $value
* The value to test the column value against.
* @param $operator
* The operator to be used to test the given value.
* @param $delta_group
* An arbitrary identifier: conditions in the same group must have the same
* $delta_group.
* @param $language_group
* An arbitrary identifier: conditions in the same group must have the same
* $language_group.
*
* @return EntityFieldQuery
* The called object.
*
* @see EntityFieldQuery::addFieldCondition
* @see EntityFieldQuery::deleted
*/
public function fieldLanguageCondition($field, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
return $this->addFieldCondition($this->fieldMetaConditions, $field, 'language', $value, $operator, $delta_group, $language_group);
}
/**
* Adds a condition on the field delta column.
*
* @param $field
* Either a field name or a field array.
* @param $value
* The value to test the column value against.
* @param $operator
* The operator to be used to test the given value.
* @param $delta_group
* An arbitrary identifier: conditions in the same group must have the same
* $delta_group.
* @param $language_group
* An arbitrary identifier: conditions in the same group must have the same
* $language_group.
*
* @return EntityFieldQuery
* The called object.
*
* @see EntityFieldQuery::addFieldCondition
* @see EntityFieldQuery::deleted
*/
public function fieldDeltaCondition($field, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
return $this->addFieldCondition($this->fieldMetaConditions, $field, 'delta', $value, $operator, $delta_group, $language_group);
}
/**
* Adds the given condition to the proper condition array.
*
* @param $conditions
* A reference to an array of conditions.
* @param $field * @param $field
* Either a field name or a field array. * Either a field name or a field array.
* @param $column * @param $column
...@@ -591,13 +732,15 @@ class EntityFieldQuery { ...@@ -591,13 +732,15 @@ class EntityFieldQuery {
* element in the array is dependent on $operator. * element in the array is dependent on $operator.
* @param $operator * @param $operator
* Possible values: * Possible values:
* - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These * - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* operators expect $value to be a literal of the same type as the * operators expect $value to be a literal of the same type as the
* column. * column.
* - 'IN', 'NOT IN': These operators expect $value to be an array of * - 'IN', 'NOT IN': These operators expect $value to be an array of
* literals of the same type as the column. * literals of the same type as the column.
* - 'BETWEEN': This operator expects $value to be an array of two literals * - 'BETWEEN': This operator expects $value to be an array of two literals
* of the same type as the column. * of the same type as the column.
* The operator can be omitted, and will default to 'IN' if the value is an
* array, or to '=' otherwise.
* @param $delta_group * @param $delta_group
* An arbitrary identifier: conditions in the same group must have the same * An arbitrary identifier: conditions in the same group must have the same
* $delta_group. For example, let's presume a multivalue field which has * $delta_group. For example, let's presume a multivalue field which has
...@@ -615,15 +758,24 @@ class EntityFieldQuery { ...@@ -615,15 +758,24 @@ class EntityFieldQuery {
* @return EntityFieldQuery * @return EntityFieldQuery
* The called object. * The called object.
*/ */
public function fieldCondition($field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) { protected function addFieldCondition(&$conditions, $field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
// The '!=' operator is deprecated in favour of the '<>' operator since the
// latter is ANSI SQL compatible.
if ($operator == '!=') {
$operator = '<>';
}
if (is_scalar($field)) { if (is_scalar($field)) {
$field = field_info_field($field); $field_definition = field_info_field($field);
if (empty($field_definition)) {
throw new EntityFieldQueryException(t('Unknown field: @field_name', array('@field_name' => $field)));
} }
// Ensure the same index is used for fieldConditions as for fields. $field = $field_definition;
}
// Ensure the same index is used for field conditions as for fields.
$index = count($this->fields); $index = count($this->fields);
$this->fields[$index] = $field; $this->fields[$index] = $field;
if (isset($column)) { if (isset($column)) {
$this->fieldConditions[$index] = array( $conditions[$index] = array(
'field' => $field, 'field' => $field,
'column' => $column, 'column' => $column,
'value' => $value, 'value' => $value,
...@@ -652,7 +804,7 @@ class EntityFieldQuery { ...@@ -652,7 +804,7 @@ class EntityFieldQuery {
* array is dependent on $operator. * array is dependent on $operator.
* @param $operator * @param $operator
* Possible values: * Possible values:
* - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These * - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* operators expect $value to be a literal of the same type as the * operators expect $value to be a literal of the same type as the
* column. * column.
* - 'IN', 'NOT IN': These operators expect $value to be an array of * - 'IN', 'NOT IN': These operators expect $value to be an array of
...@@ -666,6 +818,11 @@ class EntityFieldQuery { ...@@ -666,6 +818,11 @@ class EntityFieldQuery {
* The called object. * The called object.
*/ */
public function propertyCondition($column, $value, $operator = NULL) { public function propertyCondition($column, $value, $operator = NULL) {
// The '!=' operator is deprecated in favour of the '<>' operator since the
// latter is ANSI SQL compatible.
if ($operator == '!=') {
$operator = '<>';
}
$this->propertyConditions[] = array( $this->propertyConditions[] = array(
'column' => $column, 'column' => $column,
'value' => $value, 'value' => $value,
...@@ -680,6 +837,9 @@ class EntityFieldQuery { ...@@ -680,6 +837,9 @@ class EntityFieldQuery {
* If called multiple times, the query will order by each specified column in * If called multiple times, the query will order by each specified column in
* the order this method is called. * the order this method is called.
* *
* Note: The "comment" and "taxonomy_term" entity types don't support ordering
* by bundle. For "taxonomy_term", propertyOrderBy('vid') can be used instead.
*
* @param $name * @param $name
* 'entity_type', 'bundle', 'revision_id' or 'entity_id'. * 'entity_type', 'bundle', 'revision_id' or 'entity_id'.
* @param $direction * @param $direction
...@@ -701,7 +861,9 @@ class EntityFieldQuery { ...@@ -701,7 +861,9 @@ class EntityFieldQuery {
* Orders the result set by a given field column. * Orders the result set by a given field column.
* *
* If called multiple times, the query will order by each specified column in * If called multiple times, the query will order by each specified column in
* the order this method is called. * the order this method is called. Note that entities with empty field
* values will be excluded from the EntityFieldQuery results when using this
* method.
* *
* @param $field * @param $field
* Either a field name or a field array. * Either a field name or a field array.
...@@ -716,7 +878,11 @@ class EntityFieldQuery { ...@@ -716,7 +878,11 @@ class EntityFieldQuery {
*/ */
public function fieldOrderBy($field, $column, $direction = 'ASC') { public function fieldOrderBy($field, $column, $direction = 'ASC') {
if (is_scalar($field)) { if (is_scalar($field)) {
$field = field_info_field($field); $field_definition = field_info_field($field);
if (empty($field_definition)) {
throw new EntityFieldQueryException(t('Unknown field: @field_name', array('@field_name' => $field)));
}
$field = $field_definition;
} }
// Save the index used for the new field, for later use in field storage. // Save the index used for the new field, for later use in field storage.
$index = count($this->fields); $index = count($this->fields);
...@@ -791,6 +957,69 @@ class EntityFieldQuery { ...@@ -791,6 +957,69 @@ class EntityFieldQuery {
return $this; return $this;
} }
/**
* Enables a pager for the query.
*
* @param $limit
* An integer specifying the number of elements per page. If passed a false
* value (FALSE, 0, NULL), the pager is disabled.
* @param $element
* An optional integer to distinguish between multiple pagers on one page.
* If not provided, one is automatically calculated.
*
* @return EntityFieldQuery
* The called object.
*/
public function pager($limit = 10, $element = NULL) {
if (!isset($element)) {
$element = PagerDefault::$maxElement++;
}
elseif ($element >= PagerDefault::$maxElement) {
PagerDefault::$maxElement = $element + 1;
}
$this->pager = array(
'limit' => $limit,
'element' => $element,
);
return $this;
}
/**
* Enables sortable tables for this query.
*
* @param $headers
* An EFQ Header array based on which the order clause is added to the
* query.
*
* @return EntityFieldQuery
* The called object.
*/
public function tableSort(&$headers) {
// If 'field' is not initialized, the header columns aren't clickable
foreach ($headers as $key =>$header) {
if (is_array($header) && isset($header['specifier'])) {
$headers[$key]['field'] = '';
}
}
$order = tablesort_get_order($headers);
$direction = tablesort_get_sort($headers);
foreach ($headers as $header) {
if (is_array($header) && ($header['data'] == $order['name'])) {
if ($header['type'] == 'field') {
$this->fieldOrderBy($header['specifier']['field'], $header['specifier']['column'], $direction);
}
else {
$header['direction'] = $direction;
$this->order[] = $header;
}
}
}
return $this;
}
/** /**
* Filters on the data being deleted. * Filters on the data being deleted.
* *
...@@ -883,12 +1112,13 @@ class EntityFieldQuery { ...@@ -883,12 +1112,13 @@ class EntityFieldQuery {
* contains everything necessary for processing. * contains everything necessary for processing.
* *
* @return * @return
* Either a number if count() was called or an array of associative * Either a number if count() was called or an array of associative arrays
* arrays of stub entities. The outer array keys are entity types, and the * of stub entities. The outer array keys are entity types, and the inner
* inner array keys are the relevant ID. (In most this cases this will be * array keys are the relevant ID. (In most cases this will be the entity
* the entity ID. The only exception is when age=FIELD_LOAD_REVISION is used * ID. The only exception is when age=FIELD_LOAD_REVISION is used and field
* and field conditions or sorts are present -- in this case, the key will * conditions or sorts are present -- in this case, the key will be the
* be the revision ID.) The inner array values are always stub entities, as * revision ID.) The entity type will only exist in the outer array if
* results were found. The inner array values are always stub entities, as
* returned by entity_create_stub_entity(). To traverse the returned array: * returned by entity_create_stub_entity(). To traverse the returned array:
* @code * @code
* foreach ($query->execute() as $entity_type => $entities) { * foreach ($query->execute() as $entity_type => $entities) {
...@@ -898,12 +1128,18 @@ class EntityFieldQuery { ...@@ -898,12 +1128,18 @@ class EntityFieldQuery {
* the entities found: * the entities found:
* @code * @code
* $result = $query->execute(); * $result = $query->execute();
* if (!empty($result[$my_type])) {
* $entities = entity_load($my_type, array_keys($result[$my_type])); * $entities = entity_load($my_type, array_keys($result[$my_type]));
* }
* @endcode * @endcode
*/ */
public function execute() { public function execute() {
// Give a chance to other modules to alter the query. // Give a chance to other modules to alter the query.
drupal_alter('entity_query', $this); drupal_alter('entity_query', $this);
$this->altered = TRUE;
// Initialize the pager.
$this->initializePager();
// Execute the query using the correct callback. // Execute the query using the correct callback.
$result = call_user_func($this->queryCallback(), $this); $result = call_user_func($this->queryCallback(), $this);
...@@ -959,17 +1195,17 @@ class EntityFieldQuery { ...@@ -959,17 +1195,17 @@ class EntityFieldQuery {
throw new EntityFieldQueryException(t('For this query an entity type must be specified.')); throw new EntityFieldQueryException(t('For this query an entity type must be specified.'));
} }
$entity_type = $this->entityConditions['entity_type']['value']; $entity_type = $this->entityConditions['entity_type']['value'];
unset($this->entityConditions['entity_type']);
$entity_info = entity_get_info($entity_type); $entity_info = entity_get_info($entity_type);
if (empty($entity_info['base table'])) { if (empty($entity_info['base table'])) {
throw new EntityFieldQueryException(t('Entity %entity has no base table.', array('%entity' => $entity_type))); throw new EntityFieldQueryException(t('Entity %entity has no base table.', array('%entity' => $entity_type)));
} }
$base_table = $entity_info['base table']; $base_table = $entity_info['base table'];
$base_table_schema = drupal_get_schema($base_table);
$select_query = db_select($base_table); $select_query = db_select($base_table);
$select_query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type)); $select_query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type));
// Process the property conditions. // Process the property conditions.
foreach ($this->propertyConditions as $property_condition) { foreach ($this->propertyConditions as $property_condition) {
$this->addCondition($select_query, "$base_table." . $property_condition['column'], $property_condition); $this->addCondition($select_query, $base_table . '.' . $property_condition['column'], $property_condition);
} }
// Process the four possible entity condition. // Process the four possible entity condition.
// The id field is always present in entity keys. // The id field is always present in entity keys.
...@@ -977,7 +1213,7 @@ class EntityFieldQuery { ...@@ -977,7 +1213,7 @@ class EntityFieldQuery {
$id_map['entity_id'] = $sql_field; $id_map['entity_id'] = $sql_field;
$select_query->addField($base_table, $sql_field, 'entity_id'); $select_query->addField($base_table, $sql_field, 'entity_id');
if (isset($this->entityConditions['entity_id'])) { if (isset($this->entityConditions['entity_id'])) {
$this->addCondition($select_query, $sql_field, $this->entityConditions['entity_id']); $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['entity_id']);
} }
// If there is a revision key defined, use it. // If there is a revision key defined, use it.
...@@ -985,7 +1221,7 @@ class EntityFieldQuery { ...@@ -985,7 +1221,7 @@ class EntityFieldQuery {
$sql_field = $entity_info['entity keys']['revision']; $sql_field = $entity_info['entity keys']['revision'];
$select_query->addField($base_table, $sql_field, 'revision_id'); $select_query->addField($base_table, $sql_field, 'revision_id');
if (isset($this->entityConditions['revision_id'])) { if (isset($this->entityConditions['revision_id'])) {
$this->addCondition($select_query, $sql_field, $this->entityConditions['revision_id']); $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['revision_id']);
} }
} }
else { else {
...@@ -997,8 +1233,11 @@ class EntityFieldQuery { ...@@ -997,8 +1233,11 @@ class EntityFieldQuery {
// Handle bundles. // Handle bundles.
if (!empty($entity_info['entity keys']['bundle'])) { if (!empty($entity_info['entity keys']['bundle'])) {
$sql_field = $entity_info['entity keys']['bundle']; $sql_field = $entity_info['entity keys']['bundle'];
$select_query->addField($base_table, $sql_field, 'bundle');
$having = FALSE; $having = FALSE;
if (!empty($base_table_schema['fields'][$sql_field])) {
$select_query->addField($base_table, $sql_field, 'bundle');
}
} }
else { else {
$sql_field = 'bundle'; $sql_field = 'bundle';
...@@ -1007,7 +1246,13 @@ class EntityFieldQuery { ...@@ -1007,7 +1246,13 @@ class EntityFieldQuery {
} }
$id_map['bundle'] = $sql_field; $id_map['bundle'] = $sql_field;
if (isset($this->entityConditions['bundle'])) { if (isset($this->entityConditions['bundle'])) {
$this->addCondition($select_query, $sql_field, $this->entityConditions['bundle'], $having); if (!empty($entity_info['entity keys']['bundle'])) {
$this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['bundle'], $having);
}
else {
// This entity has no bundle, so invalidate the query.
$select_query->where('1 = 0');
}
} }
// Order the query. // Order the query.
...@@ -1020,13 +1265,30 @@ class EntityFieldQuery { ...@@ -1020,13 +1265,30 @@ class EntityFieldQuery {
$select_query->orderBy($id_map[$key], $order['direction']); $select_query->orderBy($id_map[$key], $order['direction']);
} }
elseif ($order['type'] == 'property') { elseif ($order['type'] == 'property') {
$select_query->orderBy("$base_table." . $order['specifier'], $order['direction']); $select_query->orderBy($base_table . '.' . $order['specifier'], $order['direction']);
} }
} }
return $this->finishQuery($select_query); return $this->finishQuery($select_query);
} }
/**
* Gets the total number of results and initializes a pager for the query.
*
* The pager can be disabled by either setting the pager limit to 0, or by
* setting this query to be a count query.
*/
function initializePager() {
if ($this->pager && !empty($this->pager['limit']) && !$this->count) {
$page = pager_find_page($this->pager['element']);
$count_query = clone $this;
$this->pager['total'] = $count_query->count()->execute();
$this->pager['start'] = $page * $this->pager['limit'];
pager_default_initialize($this->pager['total'], $this->pager['limit'], $this->pager['element']);
$this->range($this->pager['start'], $this->pager['limit']);
}
}
/** /**
* Finishes the query. * Finishes the query.
* *
...@@ -1057,7 +1319,8 @@ class EntityFieldQuery { ...@@ -1057,7 +1319,8 @@ class EntityFieldQuery {
} }
$return = array(); $return = array();
foreach ($select_query->execute() as $partial_entity) { foreach ($select_query->execute() as $partial_entity) {
$entity = entity_create_stub_entity($partial_entity->entity_type, array($partial_entity->entity_id, $partial_entity->revision_id, $partial_entity->bundle)); $bundle = isset($partial_entity->bundle) ? $partial_entity->bundle : NULL;
$entity = entity_create_stub_entity($partial_entity->entity_type, array($partial_entity->entity_id, $partial_entity->revision_id, $bundle));
$return[$partial_entity->entity_type][$partial_entity->$id_key] = $entity; $return[$partial_entity->entity_type][$partial_entity->$id_key] = $entity;
$this->ordered_results[] = $partial_entity; $this->ordered_results[] = $partial_entity;
} }
...@@ -1095,3 +1358,8 @@ class EntityFieldQuery { ...@@ -1095,3 +1358,8 @@ class EntityFieldQuery {
} }
} }
/**
* Defines an exception thrown when a malformed entity is passed.
*/
class EntityMalformedException extends Exception { }
<?php <?php
// $Id: errors.inc,v 1.9 2010/10/16 00:00:16 webchick Exp $
/** /**
* @file * @file
* Functions for error handling * Functions for error handling.
*/ */
/** /**
* Error reporting level: display no errors. * Maps PHP error constants to watchdog severity levels.
*/ *
define('ERROR_REPORTING_HIDE', 0);
/**
* Error reporting level: display errors and warnings.
*/
define('ERROR_REPORTING_DISPLAY_SOME', 1);
/**
* Error reporting level: display all messages.
*/
define('ERROR_REPORTING_DISPLAY_ALL', 2);
/**
* Map PHP error constants to watchdog severity levels.
* The error constants are documented at * The error constants are documented at
* http://php.net/manual/en/errorfunc.constants.php * http://php.net/manual/en/errorfunc.constants.php
*
* @ingroup logging_severity_levels
*/ */
function drupal_error_levels() { function drupal_error_levels() {
$types = array( $types = array(
...@@ -51,7 +38,7 @@ function drupal_error_levels() { ...@@ -51,7 +38,7 @@ function drupal_error_levels() {
} }
/** /**
* Custom PHP error handler. * Provides custom PHP error handling.
* *
* @param $error_level * @param $error_level
* The level of the error raised. * The level of the error raised.
...@@ -62,7 +49,8 @@ function drupal_error_levels() { ...@@ -62,7 +49,8 @@ function drupal_error_levels() {
* @param $line * @param $line
* The line number the error was raised at. * The line number the error was raised at.
* @param $context * @param $context
* An array that points to the active symbol table at the point the error occurred. * 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, $context) {
if ($error_level & error_reporting()) { if ($error_level & error_reporting()) {
...@@ -89,10 +77,11 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c ...@@ -89,10 +77,11 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
} }
/** /**
* Decode an exception, especially to retrive the correct caller. * Decodes an exception and retrieves the correct caller.
* *
* @param $exception * @param $exception
* The exception object that was thrown. * The exception object that was thrown.
*
* @return * @return
* An error in the format expected by _drupal_log_error(). * An error in the format expected by _drupal_log_error().
*/ */
...@@ -135,7 +124,7 @@ function _drupal_decode_exception($exception) { ...@@ -135,7 +124,7 @@ function _drupal_decode_exception($exception) {
} }
/** /**
* Render an error message for an exception without any possibility of a further exception occuring. * Renders an exception error message without further exceptions.
* *
* @param $exception * @param $exception
* The exception object that was thrown. * The exception object that was thrown.
...@@ -170,12 +159,12 @@ function error_displayable($error = NULL) { ...@@ -170,12 +159,12 @@ function error_displayable($error = NULL) {
} }
/** /**
* Log a PHP error or exception, display an error page in fatal cases. * Logs a PHP error or exception and displays an error page in fatal cases.
* *
* @param $error * @param $error
* An array with the following keys: %type, !message, %function, %file, %line. * An array with the following keys: %type, !message, %function, %file, %line
* All the parameters are plain-text, exception message, which needs to be * and severity_level. All the parameters are plain-text, with the exception
* a safe HTML string. * of !message, which needs to be a safe HTML string.
* @param $fatal * @param $fatal
* TRUE if the error is fatal. * TRUE if the error is fatal.
*/ */
...@@ -226,8 +215,10 @@ function _drupal_log_error($error, $fatal = FALSE) { ...@@ -226,8 +215,10 @@ function _drupal_log_error($error, $fatal = FALSE) {
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
if ($fatal) { if ($fatal) {
if (error_displayable($error)) {
// When called from JavaScript, simply output the error message. // When called from JavaScript, simply output the error message.
print t('%type: !message in %function (line %line of %file).', $error); print t('%type: !message in %function (line %line of %file).', $error);
}
exit; exit;
} }
} }
...@@ -262,6 +253,7 @@ function _drupal_log_error($error, $fatal = FALSE) { ...@@ -262,6 +253,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
* *
* @param $backtrace * @param $backtrace
* A standard PHP backtrace. * A standard PHP backtrace.
*
* @return * @return
* An associative array with keys 'file', 'line' and 'function'. * An associative array with keys 'file', 'line' and 'function'.
*/ */
......
<?php <?php
// $Id: file.inc,v 1.239 2010/10/21 12:09:41 dries Exp $
/** /**
* @file * @file
...@@ -71,11 +70,7 @@ define('FILE_EXISTS_ERROR', 2); ...@@ -71,11 +70,7 @@ define('FILE_EXISTS_ERROR', 2);
define('FILE_STATUS_PERMANENT', 1); define('FILE_STATUS_PERMANENT', 1);
/** /**
* Methods to manage a registry of stream wrappers. * Provides Drupal stream wrapper registry.
*/
/**
* Drupal stream wrapper registry.
* *
* A stream wrapper is an abstraction of a file system that allows Drupal to * A stream wrapper is an abstraction of a file system that allows Drupal to
* use the same set of methods to access both local files and remote resources. * use the same set of methods to access both local files and remote resources.
...@@ -94,7 +89,7 @@ define('FILE_STATUS_PERMANENT', 1); ...@@ -94,7 +89,7 @@ define('FILE_STATUS_PERMANENT', 1);
* wrappers that are appropriate for particular usage. For example, this returns * wrappers that are appropriate for particular usage. For example, this returns
* only stream wrappers that use local file storage: * only stream wrappers that use local file storage:
* @code * @code
* $local_stream_wrappers = file_get_stream_wrappers(STEAM_WRAPPERS_LOCAL); * $local_stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
* @endcode * @endcode
* *
* The $filter parameter can only filter to types containing a particular flag. * The $filter parameter can only filter to types containing a particular flag.
...@@ -104,7 +99,7 @@ define('FILE_STATUS_PERMANENT', 1); ...@@ -104,7 +99,7 @@ define('FILE_STATUS_PERMANENT', 1);
* array_diff_key() function can be used to help with this. For example, this * array_diff_key() function can be used to help with this. For example, this
* returns only stream wrappers that do not use local file storage: * returns only stream wrappers that do not use local file storage:
* @code * @code
* $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STEAM_WRAPPERS_LOCAL)); * $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
* @endcode * @endcode
* *
* @param $filter * @param $filter
...@@ -202,13 +197,12 @@ function file_stream_wrapper_get_class($scheme) { ...@@ -202,13 +197,12 @@ function file_stream_wrapper_get_class($scheme) {
* @see file_uri_target() * @see file_uri_target()
*/ */
function file_uri_scheme($uri) { function file_uri_scheme($uri) {
$data = explode('://', $uri, 2); $position = strpos($uri, '://');
return $position ? substr($uri, 0, $position) : FALSE;
return count($data) == 2 ? $data[0] : FALSE;
} }
/** /**
* Check that the scheme of a stream URI is valid. * Checks that the scheme of a stream URI is valid.
* *
* Confirms that there is a registered stream handler for the provided scheme * Confirms that there is a registered stream handler for the provided scheme
* and that it is callable. This is useful if you want to confirm a valid * and that it is callable. This is useful if you want to confirm a valid
...@@ -234,7 +228,7 @@ function file_stream_wrapper_valid_scheme($scheme) { ...@@ -234,7 +228,7 @@ function file_stream_wrapper_valid_scheme($scheme) {
/** /**
* Returns the part of an URI after the schema. * Returns the part of a URI after the schema.
* *
* @param $uri * @param $uri
* A stream, referenced as "scheme://target". * A stream, referenced as "scheme://target".
...@@ -254,7 +248,7 @@ function file_uri_target($uri) { ...@@ -254,7 +248,7 @@ function file_uri_target($uri) {
} }
/** /**
* Get the default file stream implementation. * Gets the default file stream implementation.
* *
* @return * @return
* 'public', 'private' or any other file scheme defined as the default. * 'public', 'private' or any other file scheme defined as the default.
...@@ -288,10 +282,6 @@ function file_stream_wrapper_uri_normalize($uri) { ...@@ -288,10 +282,6 @@ function file_stream_wrapper_uri_normalize($uri) {
$uri = $scheme . '://' . $target; $uri = $scheme . '://' . $target;
} }
} }
else {
// The default scheme is file://
$url = 'file://' . $uri;
}
return $uri; return $uri;
} }
...@@ -324,7 +314,7 @@ function file_stream_wrapper_get_instance_by_uri($uri) { ...@@ -324,7 +314,7 @@ function file_stream_wrapper_get_instance_by_uri($uri) {
} }
/** /**
* Returns a reference to the stream wrapper class responsible for a given scheme. * Returns a reference to the stream wrapper class responsible for a scheme.
* *
* This helper method returns a stream instance using a scheme. That is, the * This helper method returns a stream instance using a scheme. That is, the
* passed string does not contain a "://". For example, "public" is a scheme * passed string does not contain a "://". For example, "public" is a scheme
...@@ -359,7 +349,6 @@ function file_stream_wrapper_get_instance_by_scheme($scheme) { ...@@ -359,7 +349,6 @@ function file_stream_wrapper_get_instance_by_scheme($scheme) {
* Creates a web-accessible URL for a stream to an external or local file. * Creates a web-accessible URL for a stream to an external or local file.
* *
* Compatibility: normal paths and stream wrappers. * Compatibility: normal paths and stream wrappers.
* @see http://drupal.org/node/515192
* *
* There are two kinds of local files: * There are two kinds of local files:
* - "managed files", i.e. those stored by a Drupal-compatible stream wrapper. * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
...@@ -377,6 +366,8 @@ function file_stream_wrapper_get_instance_by_scheme($scheme) { ...@@ -377,6 +366,8 @@ function file_stream_wrapper_get_instance_by_scheme($scheme) {
* If the provided string already contains a preceding 'http', 'https', or * If the provided string already contains a preceding 'http', 'https', or
* '/', nothing is done and the same string is returned. If a stream wrapper * '/', nothing is done and the same string is returned. If a stream wrapper
* could not be found to generate an external URL, then FALSE is returned. * could not be found to generate an external URL, then FALSE is returned.
*
* @see http://drupal.org/node/515192
*/ */
function file_create_url($uri) { function file_create_url($uri) {
// Allow the URI to be altered, e.g. to serve a file from a CDN or static // Allow the URI to be altered, e.g. to serve a file from a CDN or static
...@@ -403,8 +394,8 @@ function file_create_url($uri) { ...@@ -403,8 +394,8 @@ function file_create_url($uri) {
} }
} }
elseif ($scheme == 'http' || $scheme == 'https') { elseif ($scheme == 'http' || $scheme == 'https') {
// Check for http so that we don't have to implement getExternalUrl() for // Check for HTTP so that we don't have to implement getExternalUrl() for
// the http wrapper. // the HTTP wrapper.
return $uri; return $uri;
} }
else { else {
...@@ -419,12 +410,12 @@ function file_create_url($uri) { ...@@ -419,12 +410,12 @@ function file_create_url($uri) {
} }
/** /**
* Check that the directory exists and is writable. * Checks that the directory exists and is writable.
* *
* Directories need to have execute permissions to be considered a directory by * Directories need to have execute permissions to be considered a directory by
* FTP servers, etc. * FTP servers, etc.
* *
* @param &$directory * @param $directory
* A string reference containing the name of a directory path or URI. A * A string reference containing the name of a directory path or URI. A
* trailing slash will be trimmed from a path. * trailing slash will be trimmed from a path.
* @param $options * @param $options
...@@ -461,7 +452,7 @@ function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) ...@@ -461,7 +452,7 @@ function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS)
} }
/** /**
* If missing, create a .htaccess file in each Drupal files directory. * Creates a .htaccess file in each Drupal files directory if it is missing.
*/ */
function file_ensure_htaccess() { function file_ensure_htaccess() {
file_create_htaccess('public://', FALSE); file_create_htaccess('public://', FALSE);
...@@ -472,7 +463,7 @@ function file_ensure_htaccess() { ...@@ -472,7 +463,7 @@ function file_ensure_htaccess() {
} }
/** /**
* Creates an .htaccess file in the given directory. * Creates a .htaccess file in the given directory.
* *
* @param $directory * @param $directory
* The directory. * The directory.
...@@ -519,28 +510,34 @@ function file_create_htaccess($directory, $private = TRUE) { ...@@ -519,28 +510,34 @@ function file_create_htaccess($directory, $private = TRUE) {
* @param $fids * @param $fids
* An array of file IDs. * An array of file IDs.
* @param $conditions * @param $conditions
* An array of conditions to match against the {file_managed} table. * (deprecated) An associative array of conditions on the {file_managed}
* These should be supplied in the form array('field_name' => * table, where the keys are the database fields and the values are the
* 'field_value'). * values those fields must have. Instead, it is preferable to use
* EntityFieldQuery to retrieve a list of entity IDs loadable by
* this function.
* *
* @return * @return
* An array of file objects, indexed by fid. * An array of file objects, indexed by fid.
* *
* @todo Remove $conditions in Drupal 8.
*
* @see hook_file_load() * @see hook_file_load()
* @see file_load() * @see file_load()
* @see entity_load()
* @see EntityFieldQuery
*/ */
function file_load_multiple($fids = array(), $conditions = array()) { function file_load_multiple($fids = array(), $conditions = array()) {
return entity_load('file', $fids, $conditions); return entity_load('file', $fids, $conditions);
} }
/** /**
* Load a file object from the database. * Loads a single file object from the database.
* *
* @param $fid * @param $fid
* A file ID. * A file ID.
* *
* @return * @return
* A file object. * An object representing the file, or FALSE if the file was not found.
* *
* @see hook_file_load() * @see hook_file_load()
* @see file_load_multiple() * @see file_load_multiple()
...@@ -551,7 +548,7 @@ function file_load($fid) { ...@@ -551,7 +548,7 @@ function file_load($fid) {
} }
/** /**
* Save a file object to the database. * Saves a file object to the database.
* *
* If the $file->fid is not set a new record will be added. * If the $file->fid is not set a new record will be added.
* *
...@@ -568,6 +565,14 @@ function file_save(stdClass $file) { ...@@ -568,6 +565,14 @@ function file_save(stdClass $file) {
$file->timestamp = REQUEST_TIME; $file->timestamp = REQUEST_TIME;
$file->filesize = filesize($file->uri); $file->filesize = filesize($file->uri);
// Load the stored entity, if any.
if (!empty($file->fid) && !isset($file->original)) {
$file->original = entity_load_unchanged('file', $file->fid);
}
module_invoke_all('file_presave', $file);
module_invoke_all('entity_presave', $file, 'file');
if (empty($file->fid)) { if (empty($file->fid)) {
drupal_write_record('file_managed', $file); drupal_write_record('file_managed', $file);
// Inform modules about the newly added file. // Inform modules about the newly added file.
...@@ -581,6 +586,7 @@ function file_save(stdClass $file) { ...@@ -581,6 +586,7 @@ function file_save(stdClass $file) {
module_invoke_all('entity_update', $file, 'file'); module_invoke_all('entity_update', $file, 'file');
} }
unset($file->original);
return $file; return $file;
} }
...@@ -592,7 +598,8 @@ function file_save(stdClass $file) { ...@@ -592,7 +598,8 @@ function file_save(stdClass $file) {
* *
* @return * @return
* A nested array with usage data. The first level is keyed by module name, * A nested array with usage data. The first level is keyed by module name,
* the second by object type, the third has 'id' and 'count' keys. * the second by object type and the third by the object id. The value
* of the third level contains the usage count.
* *
* @see file_usage_add() * @see file_usage_add()
* @see file_usage_delete() * @see file_usage_delete()
...@@ -605,7 +612,7 @@ function file_usage_list(stdClass $file) { ...@@ -605,7 +612,7 @@ function file_usage_list(stdClass $file) {
->execute(); ->execute();
$references = array(); $references = array();
foreach ($result as $usage) { foreach ($result as $usage) {
$references[$usage->module][$usage->type] = array('id' => $usage->id, 'count' => $usage->count); $references[$usage->module][$usage->type][$usage->id] = $usage->count;
} }
return $references; return $references;
} }
...@@ -724,8 +731,7 @@ function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $c ...@@ -724,8 +731,7 @@ function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $c
* A file object. * A file object.
* @param $destination * @param $destination
* A string containing the destination that $source should be copied to. * A string containing the destination that $source should be copied to.
* This must be a stream wrapper URI. If this value is omitted, Drupal's * This must be a stream wrapper URI.
* default files scheme will be used, usually "public://".
* @param $replace * @param $replace
* Replace behavior when the destination file already exists: * Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
...@@ -743,7 +749,12 @@ function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $c ...@@ -743,7 +749,12 @@ function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $c
*/ */
function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
if (!file_valid_uri($destination)) { if (!file_valid_uri($destination)) {
watchdog('file', 'File %file (%realpath) could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => drupal_realpath($source->uri), '%destination' => $destination)); if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
watchdog('file', 'File %file (%realpath) could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
}
else {
watchdog('file', 'File %file could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
}
drupal_set_message(t('The specified file %file could not be copied, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error'); drupal_set_message(t('The specified file %file could not be copied, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
return FALSE; return FALSE;
} }
...@@ -752,7 +763,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS ...@@ -752,7 +763,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
$file = clone $source; $file = clone $source;
$file->fid = NULL; $file->fid = NULL;
$file->uri = $uri; $file->uri = $uri;
$file->filename = basename($uri); $file->filename = drupal_basename($uri);
// If we are replacing an existing file re-use its database record. // If we are replacing an existing file re-use its database record.
if ($replace == FILE_EXISTS_REPLACE) { if ($replace == FILE_EXISTS_REPLACE) {
$existing_files = file_load_multiple(array(), array('uri' => $uri)); $existing_files = file_load_multiple(array(), array('uri' => $uri));
...@@ -765,7 +776,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS ...@@ -765,7 +776,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
// If we are renaming around an existing file (rather than a directory), // If we are renaming around an existing file (rather than a directory),
// use its basename for the filename. // use its basename for the filename.
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) { elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
$file->filename = basename($destination); $file->filename = drupal_basename($destination);
} }
$file = file_save($file); $file = file_save($file);
...@@ -779,7 +790,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS ...@@ -779,7 +790,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
} }
/** /**
* Determine whether the URI has a valid scheme for file API operations. * Determines whether the URI has a valid scheme for file API operations.
* *
* There must be a scheme and it must be a Drupal-provided scheme like * There must be a scheme and it must be a Drupal-provided scheme like
* 'public', 'private', 'temporary', or an extension provided with * 'public', 'private', 'temporary', or an extension provided with
...@@ -810,14 +821,17 @@ function file_valid_uri($uri) { ...@@ -810,14 +821,17 @@ function file_valid_uri($uri) {
* is reported. * is reported.
* - If file already exists in $destination either the call will error out, * - If file already exists in $destination either the call will error out,
* replace the file or rename the file based on the $replace parameter. * replace the file or rename the file based on the $replace parameter.
* - Provides a fallback using realpaths if the move fails using stream
* wrappers. This can occur because PHP's copy() function does not properly
* support streams if safe_mode or open_basedir are enabled. See
* https://bugs.php.net/bug.php?id=60456
* *
* @param $source * @param $source
* A string specifying the filepath or URI of the source file. * A string specifying the filepath or URI of the source file.
* @param $destination * @param $destination
* A URI containing the destination that $source should be copied to. The * A URI containing the destination that $source should be copied to. The
* URI may be a bare filepath (without a scheme) and in that case the default * URI may be a bare filepath (without a scheme). If this value is omitted,
* scheme (file://) will be used. If this value is omitted, Drupal's default * Drupal's default files scheme will be used, usually "public://".
* files scheme will be used, usually "public://".
* @param $replace * @param $replace
* Replace behavior when the destination file already exists: * Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. * - FILE_EXISTS_REPLACE - Replace the existing file.
...@@ -835,31 +849,35 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST ...@@ -835,31 +849,35 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
$original_destination = $destination; $original_destination = $destination;
// Assert that the source file actually exists. // Assert that the source file actually exists.
$source = drupal_realpath($source);
if (!file_exists($source)) { if (!file_exists($source)) {
// @todo Replace drupal_set_message() calls with exceptions instead. // @todo Replace drupal_set_message() calls with exceptions instead.
drupal_set_message(t('The specified file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error'); drupal_set_message(t('The specified file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
watchdog('file', 'File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => drupal_realpath($original_source))); if (($realpath = drupal_realpath($original_source)) !== FALSE) {
watchdog('file', 'File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
}
else {
watchdog('file', 'File %file could not be copied because it does not exist.', array('%file' => $original_source));
}
return FALSE; return FALSE;
} }
// Build a destination URI if necessary. // Build a destination URI if necessary.
if (!isset($destination)) { if (!isset($destination)) {
$destination = file_build_uri(basename($source)); $destination = file_build_uri(drupal_basename($source));
} }
// Prepare the destination directory. // Prepare the destination directory.
if (file_prepare_directory($destination)) { if (file_prepare_directory($destination)) {
// The destination is already a directory, so append the source basename. // The destination is already a directory, so append the source basename.
$destination = file_stream_wrapper_uri_normalize($destination . '/' . basename($source)); $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
} }
else { else {
// Perhaps $destination is a dir/file? // Perhaps $destination is a dir/file?
$dirname = drupal_dirname($destination); $dirname = drupal_dirname($destination);
if (!file_prepare_directory($dirname)) { if (!file_prepare_directory($dirname)) {
// The destination is not valid. // The destination is not valid.
watchdog('file', 'File %file could not be copied, because the destination directory %directory is not configured correctly.', array('%file' => $original_source, '%destination' => drupal_realpath($dirname))); watchdog('file', 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error'); drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
return FALSE; return FALSE;
} }
...@@ -869,12 +887,14 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST ...@@ -869,12 +887,14 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
$destination = file_destination($destination, $replace); $destination = file_destination($destination, $replace);
if ($destination === FALSE) { if ($destination === FALSE) {
drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error'); drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
watchdog('file', 'File %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $original_source, '%destination' => drupal_realpath($destination))); watchdog('file', 'File %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $original_source, '%directory' => $destination));
return FALSE; return FALSE;
} }
// Assert that the source and destination filenames are not the same. // Assert that the source and destination filenames are not the same.
if (drupal_realpath($source) == drupal_realpath($destination)) { $real_source = drupal_realpath($source);
$real_destination = drupal_realpath($destination);
if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error'); drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source)); watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
return FALSE; return FALSE;
...@@ -883,9 +903,13 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST ...@@ -883,9 +903,13 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
file_ensure_htaccess(); file_ensure_htaccess();
// Perform the copy operation. // Perform the copy operation.
if (!@copy($source, $destination)) { if (!@copy($source, $destination)) {
watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => drupal_realpath($destination)), WATCHDOG_ERROR); // If the copy failed and realpaths exist, retry the operation using them
// instead.
if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination), WATCHDOG_ERROR);
return FALSE; return FALSE;
} }
}
// Set the permissions on the new file. // Set the permissions on the new file.
drupal_chmod($destination); drupal_chmod($destination);
...@@ -894,7 +918,7 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST ...@@ -894,7 +918,7 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
} }
/** /**
* Given a relative path, construct a URI into Drupal's default files location. * Constructs a URI to Drupal's default files location given a relative path.
*/ */
function file_build_uri($path) { function file_build_uri($path) {
$uri = file_default_scheme() . '://' . $path; $uri = file_default_scheme() . '://' . $path;
...@@ -902,8 +926,7 @@ function file_build_uri($path) { ...@@ -902,8 +926,7 @@ function file_build_uri($path) {
} }
/** /**
* Determines the destination path for a file depending on how replacement of * Determines the destination path for a file.
* existing files should be handled.
* *
* @param $destination * @param $destination
* A string specifying the desired final URI or filepath. * A string specifying the desired final URI or filepath.
...@@ -926,7 +949,7 @@ function file_destination($destination, $replace) { ...@@ -926,7 +949,7 @@ function file_destination($destination, $replace) {
break; break;
case FILE_EXISTS_RENAME: case FILE_EXISTS_RENAME:
$basename = basename($destination); $basename = drupal_basename($destination);
$directory = drupal_dirname($destination); $directory = drupal_dirname($destination);
$destination = file_create_filename($basename, $directory); $destination = file_create_filename($basename, $directory);
break; break;
...@@ -940,7 +963,7 @@ function file_destination($destination, $replace) { ...@@ -940,7 +963,7 @@ function file_destination($destination, $replace) {
} }
/** /**
* Move a file to a new location and update the file's database entry. * Moves a file to a new location and update the file's database entry.
* *
* Moving a file is performed by copying the file to the new location and then * Moving a file is performed by copying the file to the new location and then
* deleting the original. * deleting the original.
...@@ -954,8 +977,7 @@ function file_destination($destination, $replace) { ...@@ -954,8 +977,7 @@ function file_destination($destination, $replace) {
* A file object. * A file object.
* @param $destination * @param $destination
* A string containing the destination that $source should be moved to. * A string containing the destination that $source should be moved to.
* This must be a stream wrapper URI. If this value is omitted, Drupal's * This must be a stream wrapper URI.
* default files scheme will be used, usually "public://".
* @param $replace * @param $replace
* Replace behavior when the destination file already exists: * Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
...@@ -975,7 +997,12 @@ function file_destination($destination, $replace) { ...@@ -975,7 +997,12 @@ function file_destination($destination, $replace) {
*/ */
function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
if (!file_valid_uri($destination)) { if (!file_valid_uri($destination)) {
watchdog('file', 'File %file (%realpath) could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => drupal_realpath($source->uri), '%destination' => $destination)); if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
watchdog('file', 'File %file (%realpath) could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
}
else {
watchdog('file', 'File %file could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
}
drupal_set_message(t('The specified file %file could not be moved, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error'); drupal_set_message(t('The specified file %file could not be moved, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
return FALSE; return FALSE;
} }
...@@ -997,7 +1024,7 @@ function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS ...@@ -997,7 +1024,7 @@ function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
// If we are renaming around an existing file (rather than a directory), // If we are renaming around an existing file (rather than a directory),
// use its basename for the filename. // use its basename for the filename.
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) { elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
$file->filename = basename($destination); $file->filename = drupal_basename($destination);
} }
$file = file_save($file); $file = file_save($file);
...@@ -1016,8 +1043,7 @@ function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS ...@@ -1016,8 +1043,7 @@ function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
} }
/** /**
* Move a file to a new location without calling any hooks or making any * Moves a file to a new location without database changes or hook invocation.
* changes to the database.
* *
* @param $source * @param $source
* A string specifying the filepath or URI of the original file. * A string specifying the filepath or URI of the original file.
...@@ -1046,7 +1072,7 @@ function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXIST ...@@ -1046,7 +1072,7 @@ function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXIST
} }
/** /**
* Modify a filename as needed for security purposes. * Modifies a filename as needed for security purposes.
* *
* Munging a file name prevents unknown file extensions from masking exploit * Munging a file name prevents unknown file extensions from masking exploit
* files. When web servers such as Apache decide how to process a URL request, * files. When web servers such as Apache decide how to process a URL request,
...@@ -1082,6 +1108,9 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) { ...@@ -1082,6 +1108,9 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
// Allow potentially insecure uploads for very savvy users and admin // Allow potentially insecure uploads for very savvy users and admin
if (!variable_get('allow_insecure_uploads', 0)) { if (!variable_get('allow_insecure_uploads', 0)) {
// Remove any null bytes. See http://php.net/manual/en/security.filesystem.nullbytes.php
$filename = str_replace(chr(0), '', $filename);
$whitelist = array_unique(explode(' ', trim($extensions))); $whitelist = array_unique(explode(' ', trim($extensions)));
// Split the filename up by periods. The first part becomes the basename // Split the filename up by periods. The first part becomes the basename
...@@ -1110,7 +1139,7 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) { ...@@ -1110,7 +1139,7 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
} }
/** /**
* Undo the effect of upload_munge_filename(). * Undoes the effect of file_munge_filename().
* *
* @param $filename * @param $filename
* String with the filename to be unmunged. * String with the filename to be unmunged.
...@@ -1123,7 +1152,7 @@ function file_unmunge_filename($filename) { ...@@ -1123,7 +1152,7 @@ function file_unmunge_filename($filename) {
} }
/** /**
* Create a full file path from a directory and filename. * Creates a full file path from a directory and filename.
* *
* If a file with the specified name already exists, an alternative will be * If a file with the specified name already exists, an alternative will be
* used. * used.
...@@ -1178,7 +1207,7 @@ function file_create_filename($basename, $directory) { ...@@ -1178,7 +1207,7 @@ function file_create_filename($basename, $directory) {
} }
/** /**
* Delete a file and its database record. * Deletes a file and its database record.
* *
* If the $force parameter is not TRUE, file_usage_list() will be called to * If the $force parameter is not TRUE, file_usage_list() will be called to
* determine if the file is being used by any modules. If the file is being * determine if the file is being used by any modules. If the file is being
...@@ -1201,7 +1230,12 @@ function file_create_filename($basename, $directory) { ...@@ -1201,7 +1230,12 @@ function file_create_filename($basename, $directory) {
*/ */
function file_delete(stdClass $file, $force = FALSE) { function file_delete(stdClass $file, $force = FALSE) {
if (!file_valid_uri($file->uri)) { if (!file_valid_uri($file->uri)) {
watchdog('file', 'File %file (%realpath) could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri, '%realpath' => drupal_realpath($file->uri))); if (($realpath = drupal_realpath($file->uri)) !== FALSE) {
watchdog('file', 'File %file (%realpath) could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri, '%realpath' => $realpath));
}
else {
watchdog('file', 'File %file could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri));
}
drupal_set_message(t('The specified file %file could not be deleted, because it is not a valid URI. More information is available in the system log.', array('%file' => $file->uri)), 'error'); drupal_set_message(t('The specified file %file could not be deleted, because it is not a valid URI. More information is available in the system log.', array('%file' => $file->uri)), 'error');
return FALSE; return FALSE;
} }
...@@ -1228,8 +1262,7 @@ function file_delete(stdClass $file, $force = FALSE) { ...@@ -1228,8 +1262,7 @@ function file_delete(stdClass $file, $force = FALSE) {
} }
/** /**
* Delete a file without calling any hooks or making any changes to the * Deletes a file without database changes or hook invocations.
* database.
* *
* This function should be used when the file to be deleted does not have an * This function should be used when the file to be deleted does not have an
* entry recorded in the files table. * entry recorded in the files table.
...@@ -1245,8 +1278,6 @@ function file_delete(stdClass $file, $force = FALSE) { ...@@ -1245,8 +1278,6 @@ function file_delete(stdClass $file, $force = FALSE) {
* @see file_unmanaged_delete_recursive() * @see file_unmanaged_delete_recursive()
*/ */
function file_unmanaged_delete($path) { function file_unmanaged_delete($path) {
// Resolve streamwrapper URI to local path.
$path = drupal_realpath($path);
if (is_dir($path)) { if (is_dir($path)) {
watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR); watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
return FALSE; return FALSE;
...@@ -1267,7 +1298,7 @@ function file_unmanaged_delete($path) { ...@@ -1267,7 +1298,7 @@ function file_unmanaged_delete($path) {
} }
/** /**
* Recursively delete all files and directories in the specified filepath. * Deletes all files and directories in the specified filepath recursively.
* *
* If the specified path is a directory then the function will call itself * If the specified path is a directory then the function will call itself
* recursively to process the contents. Once the contents have been removed the * recursively to process the contents. Once the contents have been removed the
...@@ -1288,8 +1319,6 @@ function file_unmanaged_delete($path) { ...@@ -1288,8 +1319,6 @@ function file_unmanaged_delete($path) {
* @see file_unmanaged_delete() * @see file_unmanaged_delete()
*/ */
function file_unmanaged_delete_recursive($path) { function file_unmanaged_delete_recursive($path) {
// Resolve streamwrapper URI to local path.
$path = drupal_realpath($path);
if (is_dir($path)) { if (is_dir($path)) {
$dir = dir($path); $dir = dir($path);
while (($entry = $dir->read()) !== FALSE) { while (($entry = $dir->read()) !== FALSE) {
...@@ -1307,7 +1336,7 @@ function file_unmanaged_delete_recursive($path) { ...@@ -1307,7 +1336,7 @@ function file_unmanaged_delete_recursive($path) {
} }
/** /**
* Determine total disk space used by a single user or the whole filesystem. * Determines total disk space used by a single user or the whole filesystem.
* *
* @param $uid * @param $uid
* Optional. A user id, specifying NULL returns the total space used by all * Optional. A user id, specifying NULL returns the total space used by all
...@@ -1414,7 +1443,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE, ...@@ -1414,7 +1443,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
$file = new stdClass(); $file = new stdClass();
$file->uid = $user->uid; $file->uid = $user->uid;
$file->status = 0; $file->status = 0;
$file->filename = trim(basename($_FILES['files']['name'][$source]), '.'); $file->filename = trim(drupal_basename($_FILES['files']['name'][$source]), '.');
$file->uri = $_FILES['files']['tmp_name'][$source]; $file->uri = $_FILES['files']['tmp_name'][$source];
$file->filemime = file_get_mimetype($file->filename); $file->filemime = file_get_mimetype($file->filename);
$file->filesize = $_FILES['files']['size'][$source]; $file->filesize = $_FILES['files']['size'][$source];
...@@ -1510,7 +1539,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE, ...@@ -1510,7 +1539,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
// directory. This overcomes open_basedir restrictions for future file // directory. This overcomes open_basedir restrictions for future file
// operations. // operations.
$file->uri = $file->destination; $file->uri = $file->destination;
if (!move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->uri)) { if (!drupal_move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->uri)) {
form_set_error($source, t('File upload error. Could not move uploaded file.')); form_set_error($source, t('File upload error. Could not move uploaded file.'));
watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri)); watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
return FALSE; return FALSE;
...@@ -1537,9 +1566,45 @@ function file_save_upload($source, $validators = array(), $destination = FALSE, ...@@ -1537,9 +1566,45 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
return FALSE; return FALSE;
} }
/**
* Moves an uploaded file to a new location.
*
* PHP's move_uploaded_file() does not properly support streams if safe_mode
* or open_basedir are enabled, so this function fills that gap.
*
* Compatibility: normal paths and stream wrappers.
*
* @param $filename
* The filename of the uploaded file.
* @param $uri
* A string containing the destination URI of the file.
*
* @return
* TRUE on success, or FALSE on failure.
*
* @see move_uploaded_file()
* @see http://drupal.org/node/515192
* @ingroup php_wrappers
*/
function drupal_move_uploaded_file($filename, $uri) {
$result = @move_uploaded_file($filename, $uri);
// PHP's move_uploaded_file() does not properly support streams if safe_mode
// or open_basedir are enabled so if the move failed, try finding a real path
// and retry the move operation.
if (!$result) {
if ($realpath = drupal_realpath($uri)) {
$result = move_uploaded_file($filename, $realpath);
}
else {
$result = move_uploaded_file($filename, $uri);
}
}
return $result;
}
/** /**
* Check that a file meets the criteria specified by the validators. * Checks that a file meets the criteria specified by the validators.
* *
* After executing the validator callbacks specified hook_file_validate() will * After executing the validator callbacks specified hook_file_validate() will
* also be called to allow other modules to report errors about the file. * also be called to allow other modules to report errors about the file.
...@@ -1574,10 +1639,11 @@ function file_validate(stdClass &$file, $validators = array()) { ...@@ -1574,10 +1639,11 @@ function file_validate(stdClass &$file, $validators = array()) {
} }
/** /**
* Check for files with names longer than we can store in the database. * Checks for files with names longer than we can store in the database.
* *
* @param $file * @param $file
* A Drupal file object. * A Drupal file object.
*
* @return * @return
* An array. If the file name is too long, it will contain an error message. * An array. If the file name is too long, it will contain an error message.
*/ */
...@@ -1594,7 +1660,7 @@ function file_validate_name_length(stdClass $file) { ...@@ -1594,7 +1660,7 @@ function file_validate_name_length(stdClass $file) {
} }
/** /**
* Check that the filename ends with an allowed extension. * Checks that the filename ends with an allowed extension.
* *
* @param $file * @param $file
* A Drupal file object. * A Drupal file object.
...@@ -1618,7 +1684,7 @@ function file_validate_extensions(stdClass $file, $extensions) { ...@@ -1618,7 +1684,7 @@ function file_validate_extensions(stdClass $file, $extensions) {
} }
/** /**
* Check that the file's size is below certain limits. * Checks that the file's size is below certain limits.
* *
* This check is not enforced for the user #1. * This check is not enforced for the user #1.
* *
...@@ -1657,7 +1723,7 @@ function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) { ...@@ -1657,7 +1723,7 @@ function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) {
} }
/** /**
* Check that the file is recognized by image_get_info() as an image. * Checks that the file is recognized by image_get_info() as an image.
* *
* @param $file * @param $file
* A Drupal file object. * A Drupal file object.
...@@ -1679,7 +1745,7 @@ function file_validate_is_image(stdClass $file) { ...@@ -1679,7 +1745,7 @@ function file_validate_is_image(stdClass $file) {
} }
/** /**
* Verify that image dimensions are within the specified maximum and minimum. * Verifies that image dimensions are within the specified maximum and minimum.
* *
* Non-image files will be ignored. If a image toolkit is available the image * Non-image files will be ignored. If a image toolkit is available the image
* will be scaled to fit within the desired maximum dimensions. * will be scaled to fit within the desired maximum dimensions.
...@@ -1737,14 +1803,14 @@ function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0, ...@@ -1737,14 +1803,14 @@ function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0,
} }
/** /**
* Save a string to the specified destination and create a database file entry. * Saves a file to the specified destination and creates a database entry.
* *
* @param $data * @param $data
* A string containing the contents of the file. * A string containing the contents of the file.
* @param $destination * @param $destination
* A string containing the destination URI. * A string containing the destination URI. This must be a stream wrapper URI.
* This must be a stream wrapper URI. If this value is omitted, Drupal's * If no value is provided, a randomized name will be generated and the file
* default files scheme will be used, usually "public://". * will be saved using Drupal's default files scheme, usually "public://".
* @param $replace * @param $replace
* Replace behavior when the destination file already exists: * Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
...@@ -1776,7 +1842,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM ...@@ -1776,7 +1842,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
$file = new stdClass(); $file = new stdClass();
$file->fid = NULL; $file->fid = NULL;
$file->uri = $uri; $file->uri = $uri;
$file->filename = basename($uri); $file->filename = drupal_basename($uri);
$file->filemime = file_get_mimetype($file->uri); $file->filemime = file_get_mimetype($file->uri);
$file->uid = $user->uid; $file->uid = $user->uid;
$file->status = FILE_STATUS_PERMANENT; $file->status = FILE_STATUS_PERMANENT;
...@@ -1792,7 +1858,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM ...@@ -1792,7 +1858,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
// If we are renaming around an existing file (rather than a directory), // If we are renaming around an existing file (rather than a directory),
// use its basename for the filename. // use its basename for the filename.
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) { elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
$file->filename = basename($destination); $file->filename = drupal_basename($destination);
} }
return file_save($file); return file_save($file);
...@@ -1801,7 +1867,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM ...@@ -1801,7 +1867,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
} }
/** /**
* Save a string to the specified destination without invoking file API. * Saves a string to the specified destination without invoking file API.
* *
* This function is identical to file_save_data() except the file will not be * This function is identical to file_save_data() except the file will not be
* saved to the {file_managed} table and none of the file_* hooks will be * saved to the {file_managed} table and none of the file_* hooks will be
...@@ -1810,10 +1876,10 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM ...@@ -1810,10 +1876,10 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
* @param $data * @param $data
* A string containing the contents of the file. * A string containing the contents of the file.
* @param $destination * @param $destination
* A string containing the destination location. * A string containing the destination location. This must be a stream wrapper
* This must be a stream wrapper URI. If no value is provided, a * URI. If no value is provided, a randomized name will be generated and the
* randomized name will be generated and the file is saved using Drupal's * file will be saved using Drupal's default files scheme, usually
* default files scheme, usually "public://". * "public://".
* @param $replace * @param $replace
* Replace behavior when the destination file already exists: * Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. * - FILE_EXISTS_REPLACE - Replace the existing file.
...@@ -1839,7 +1905,7 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX ...@@ -1839,7 +1905,7 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX
} }
/** /**
* Transfer file using HTTP to client. * Transfers a file to the client using HTTP.
* *
* Pipes a file through Drupal to the client. * Pipes a file through Drupal to the client.
* *
...@@ -1875,12 +1941,13 @@ function file_transfer($uri, $headers) { ...@@ -1875,12 +1941,13 @@ function file_transfer($uri, $headers) {
* Menu handler for private file transfers. * Menu handler for private file transfers.
* *
* Call modules that implement hook_file_download() to find out if a file is * Call modules that implement hook_file_download() to find out if a file is
* accessible and what headers it should be transferred with. If a module * accessible and what headers it should be transferred with. If one or more
* returns -1 drupal_access_denied() will be returned. If one or more modules * modules returned headers the download will start with the returned headers.
* returned headers the download will start with the returned headers. If no * If a module returns -1 drupal_access_denied() will be returned. If the file
* modules respond drupal_not_found() will be returned. * exists but no modules responded drupal_access_denied() will be returned.
* If the file does not exist drupal_not_found() will be returned.
* *
* @see hook_file_download() * @see system_menu()
*/ */
function file_download() { function file_download() {
// Merge remainder of arguments from GET['q'], into relative file path. // Merge remainder of arguments from GET['q'], into relative file path.
...@@ -1898,7 +1965,9 @@ function file_download() { ...@@ -1898,7 +1965,9 @@ function file_download() {
$function = $module . '_file_download'; $function = $module . '_file_download';
$result = $function($uri); $result = $function($uri);
if ($result == -1) { if ($result == -1) {
return drupal_access_denied(); // Throw away the headers received so far.
$headers = array();
break;
} }
if (isset($result) && is_array($result)) { if (isset($result) && is_array($result)) {
$headers = array_merge($headers, $result); $headers = array_merge($headers, $result);
...@@ -1907,8 +1976,12 @@ function file_download() { ...@@ -1907,8 +1976,12 @@ function file_download() {
if (count($headers)) { if (count($headers)) {
file_transfer($uri, $headers); file_transfer($uri, $headers);
} }
drupal_access_denied();
} }
return drupal_not_found(); else {
drupal_not_found();
}
drupal_exit();
} }
...@@ -1989,7 +2062,7 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) { ...@@ -1989,7 +2062,7 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
} }
/** /**
* Determine the maximum file upload size by querying the PHP settings. * Determines the maximum file upload size by querying the PHP settings.
* *
* @return * @return
* A file size limit in bytes based on the PHP upload_max_filesize and * A file size limit in bytes based on the PHP upload_max_filesize and
...@@ -2013,7 +2086,7 @@ function file_upload_max_size() { ...@@ -2013,7 +2086,7 @@ function file_upload_max_size() {
} }
/** /**
* Determine an Internet Media Type, or MIME type from a filename. * Determines an Internet Media Type or MIME type from a filename.
* *
* @param $uri * @param $uri
* A string containing the URI, path, or filename. * A string containing the URI, path, or filename.
...@@ -2042,7 +2115,7 @@ function file_get_mimetype($uri, $mapping = NULL) { ...@@ -2042,7 +2115,7 @@ function file_get_mimetype($uri, $mapping = NULL) {
} }
/** /**
* Set the permissions on a file or directory. * Sets the permissions on a file or directory.
* *
* This function will use the 'file_chmod_directory' and 'file_chmod_file' * This function will use the 'file_chmod_directory' and 'file_chmod_file'
* variables for the default modes for directories and uploaded/generated * variables for the default modes for directories and uploaded/generated
...@@ -2126,27 +2199,32 @@ function drupal_unlink($uri, $context = NULL) { ...@@ -2126,27 +2199,32 @@ function drupal_unlink($uri, $context = NULL) {
} }
/** /**
* Returns the absolute path of a file or directory * Returns the absolute local filesystem path of a stream URI.
*
* PHP's realpath() does not properly support streams, so this function
* fills that gap. If a stream wrapped URI is provided, it will be passed
* to the registered wrapper for handling. If the URI does not contain a
* scheme or the wrapper implementation does not implement realpath, then
* FALSE will be returned.
* *
* @see http://php.net/manual/en/function.realpath.php * This function was originally written to ease the conversion of 6.x code to
* use 7.x stream wrappers. However, it assumes that every URI may be resolved
* to an absolute local filesystem path, and this assumption fails when stream
* wrappers are used to support remote file storage. Remote stream wrappers
* may implement the realpath method by always returning FALSE. The use of
* drupal_realpath() is discouraged, and is slowly being removed from core
* functions where possible.
* *
* Compatibility: normal paths and stream wrappers. * Only use this function if you know that the stream wrapper in the URI uses
* @see http://drupal.org/node/515192 * the local file system, and you need to pass an absolute path to a function
* that is incompatible with stream URIs.
* *
* @param $uri * @param $uri
* A string containing the URI to verify. If this value is omitted, * A stream wrapper URI or a filesystem path, possibly including one or more
* Drupal's public files directory will be used [public://]. * symbolic links.
* *
* @return * @return
* The absolute pathname, or FALSE on failure. * The absolute local filesystem path (with no symbolic links), or FALSE on
* failure.
* *
* @see realpath() * @todo This function is deprecated, and should be removed wherever possible.
*
* @see DrupalStreamWrapperInterface::realpath()
* @see http://php.net/manual/function.realpath.php
* @ingroup php_wrappers * @ingroup php_wrappers
*/ */
function drupal_realpath($uri) { function drupal_realpath($uri) {
...@@ -2156,7 +2234,7 @@ function drupal_realpath($uri) { ...@@ -2156,7 +2234,7 @@ function drupal_realpath($uri) {
if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) { if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
return $wrapper->realpath(); return $wrapper->realpath();
} }
// Check that the uri has a value. There is a bug in PHP 5.2 on *BSD systems // Check that the URI has a value. There is a bug in PHP 5.2 on *BSD systems
// that makes realpath not return FALSE as expected when passing an empty // that makes realpath not return FALSE as expected when passing an empty
// variable. // variable.
// @todo Remove when Drupal drops support for PHP 5.2. // @todo Remove when Drupal drops support for PHP 5.2.
...@@ -2174,7 +2252,6 @@ function drupal_realpath($uri) { ...@@ -2174,7 +2252,6 @@ function drupal_realpath($uri) {
* PHP's dirname() as a fallback. * PHP's dirname() as a fallback.
* *
* Compatibility: normal paths and stream wrappers. * Compatibility: normal paths and stream wrappers.
* @see http://drupal.org/node/515192
* *
* @param $uri * @param $uri
* A URI or path. * A URI or path.
...@@ -2183,6 +2260,7 @@ function drupal_realpath($uri) { ...@@ -2183,6 +2260,7 @@ function drupal_realpath($uri) {
* A string containing the directory name. * A string containing the directory name.
* *
* @see dirname() * @see dirname()
* @see http://drupal.org/node/515192
* @ingroup php_wrappers * @ingroup php_wrappers
*/ */
function drupal_dirname($uri) { function drupal_dirname($uri) {
...@@ -2196,6 +2274,35 @@ function drupal_dirname($uri) { ...@@ -2196,6 +2274,35 @@ function drupal_dirname($uri) {
} }
} }
/**
* Gets the filename from a given path.
*
* PHP's basename() does not properly support streams or filenames beginning
* with a non-US-ASCII character.
*
* @see http://bugs.php.net/bug.php?id=37738
* @see basename()
*
* @ingroup php_wrappers
*/
function drupal_basename($uri, $suffix = NULL) {
$separators = '/';
if (DIRECTORY_SEPARATOR != '/') {
// For Windows OS add special separator.
$separators .= DIRECTORY_SEPARATOR;
}
// Remove right-most slashes when $uri points to directory.
$uri = rtrim($uri, $separators);
// Returns the trailing part of the $uri starting after one of the directory
// separators.
$filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : '';
// Cuts off a suffix from the filename.
if ($suffix) {
$filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename);
}
return $filename;
}
/** /**
* Creates a directory using Drupal's default mode. * Creates a directory using Drupal's default mode.
* *
...@@ -2203,7 +2310,6 @@ function drupal_dirname($uri) { ...@@ -2203,7 +2310,6 @@ function drupal_dirname($uri) {
* is not provided, this function will make sure that Drupal's is used. * is not provided, this function will make sure that Drupal's is used.
* *
* Compatibility: normal paths and stream wrappers. * Compatibility: normal paths and stream wrappers.
* @see http://drupal.org/node/515192
* *
* @param $uri * @param $uri
* A URI or pathname. * A URI or pathname.
...@@ -2218,6 +2324,7 @@ function drupal_dirname($uri) { ...@@ -2218,6 +2324,7 @@ function drupal_dirname($uri) {
* Boolean TRUE on success, or FALSE on failure. * Boolean TRUE on success, or FALSE on failure.
* *
* @see mkdir() * @see mkdir()
* @see http://drupal.org/node/515192
* @ingroup php_wrappers * @ingroup php_wrappers
*/ */
function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) { function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
...@@ -2234,7 +2341,7 @@ function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) { ...@@ -2234,7 +2341,7 @@ function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
} }
/** /**
* Remove a directory. * Removes a directory.
* *
* PHP's rmdir() is broken on Windows, as it can fail to remove a directory * PHP's rmdir() is broken on Windows, as it can fail to remove a directory
* when it has a read-only flag set. * when it has a read-only flag set.
...@@ -2271,7 +2378,6 @@ function drupal_rmdir($uri, $context = NULL) { ...@@ -2271,7 +2378,6 @@ function drupal_rmdir($uri, $context = NULL) {
* given a filepath. * given a filepath.
* *
* Compatibility: normal paths and stream wrappers. * Compatibility: normal paths and stream wrappers.
* @see http://drupal.org/node/515192
* *
* @param $directory * @param $directory
* The directory where the temporary filename will be created. * The directory where the temporary filename will be created.
...@@ -2283,6 +2389,7 @@ function drupal_rmdir($uri, $context = NULL) { ...@@ -2283,6 +2389,7 @@ function drupal_rmdir($uri, $context = NULL) {
* The new temporary filename, or FALSE on failure. * The new temporary filename, or FALSE on failure.
* *
* @see tempnam() * @see tempnam()
* @see http://drupal.org/node/515192
* @ingroup php_wrappers * @ingroup php_wrappers
*/ */
function drupal_tempnam($directory, $prefix) { function drupal_tempnam($directory, $prefix) {
...@@ -2292,7 +2399,7 @@ function drupal_tempnam($directory, $prefix) { ...@@ -2292,7 +2399,7 @@ function drupal_tempnam($directory, $prefix) {
$wrapper = file_stream_wrapper_get_instance_by_scheme($scheme); $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme);
if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) { if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) {
return $scheme . '://' . basename($filename); return $scheme . '://' . drupal_basename($filename);
} }
else { else {
return FALSE; return FALSE;
...@@ -2305,7 +2412,7 @@ function drupal_tempnam($directory, $prefix) { ...@@ -2305,7 +2412,7 @@ function drupal_tempnam($directory, $prefix) {
} }
/** /**
* Get the path of system-appropriate temporary directory. * Gets the path of system-appropriate temporary directory.
*/ */
function file_directory_temp() { function file_directory_temp() {
$temporary_directory = variable_get('file_temporary_path', NULL); $temporary_directory = variable_get('file_temporary_path', NULL);
...@@ -2322,11 +2429,9 @@ function file_directory_temp() { ...@@ -2322,11 +2429,9 @@ function file_directory_temp() {
if (substr(PHP_OS, 0, 3) == 'WIN') { if (substr(PHP_OS, 0, 3) == 'WIN') {
$directories[] = 'c:\\windows\\temp'; $directories[] = 'c:\\windows\\temp';
$directories[] = 'c:\\winnt\\temp'; $directories[] = 'c:\\winnt\\temp';
$path_delimiter = '\\';
} }
else { else {
$directories[] = '/tmp'; $directories[] = '/tmp';
$path_delimiter = '/';
} }
// PHP may be able to find an alternative tmp directory. // PHP may be able to find an alternative tmp directory.
// This function exists in PHP 5 >= 5.2.1, but Drupal // This function exists in PHP 5 >= 5.2.1, but Drupal
...@@ -2343,8 +2448,14 @@ function file_directory_temp() { ...@@ -2343,8 +2448,14 @@ function file_directory_temp() {
} }
if (empty($temporary_directory)) { if (empty($temporary_directory)) {
// If no directory has been found default to 'files/tmp' or 'files\\tmp'. // If no directory has been found default to 'files/tmp'.
$temporary_directory = variable_get('file_public_path', conf_path() . '/files') . $path_delimiter . 'tmp'; $temporary_directory = variable_get('file_public_path', conf_path() . '/files') . '/tmp';
// Windows accepts paths with either slash (/) or backslash (\), but will
// not accept a path which contains both a slash and a backslash. Since
// the 'file_public_path' variable may have either format, we sanitize
// everything to use slash which is supported on all platforms.
$temporary_directory = str_replace('\\', '/', $temporary_directory);
} }
// Save the path of the discovered directory. // Save the path of the discovered directory.
variable_set('file_temporary_path', $temporary_directory); variable_set('file_temporary_path', $temporary_directory);
...@@ -2358,26 +2469,17 @@ function file_directory_temp() { ...@@ -2358,26 +2469,17 @@ function file_directory_temp() {
* *
* @param $file * @param $file
* A file object. * A file object.
*
* @return * @return
* An associative array of headers, as expected by file_transfer(). * An associative array of headers, as expected by file_transfer().
*/ */
function file_get_content_headers($file) { function file_get_content_headers($file) {
$name = mime_header_encode($file->filename); $name = mime_header_encode($file->filename);
$type = mime_header_encode($file->filemime); $type = mime_header_encode($file->filemime);
// Serve images, text, and flash content for display rather than download.
$inline_types = variable_get('file_inline_types', array('^text/', '^image/', 'flash$'));
$disposition = 'attachment';
foreach ($inline_types as $inline_type) {
// Exclamation marks are used as delimiters to avoid escaping slashes.
if (preg_match('!' . $inline_type . '!', $file->filemime)) {
$disposition = 'inline';
}
}
return array( return array(
'Content-Type' => $type . '; name="' . $name . '"', 'Content-Type' => $type,
'Content-Length' => $file->filesize, 'Content-Length' => $file->filesize,
'Content-Disposition' => $disposition . '; filename="' . $name . '"',
'Cache-Control' => 'private', 'Cache-Control' => 'private',
); );
} }
......
<?php <?php
// $Id: file.mimetypes.inc,v 1.5 2010/07/16 02:40:48 dries Exp $
/** /**
* @file * @file
...@@ -374,6 +373,7 @@ function file_default_mimetype_mapping() { ...@@ -374,6 +373,7 @@ function file_default_mimetype_mapping() {
333 => 'video/vnd.mpegurl', 333 => 'video/vnd.mpegurl',
347 => 'video/x-flv', 347 => 'video/x-flv',
334 => 'video/x-la-asf', 334 => 'video/x-la-asf',
348 => 'video/x-m4v',
335 => 'video/x-mng', 335 => 'video/x-mng',
336 => 'video/x-ms-asf', 336 => 'video/x-ms-asf',
337 => 'video/x-ms-wm', 337 => 'video/x-ms-wm',
...@@ -408,7 +408,6 @@ function file_default_mimetype_mapping() { ...@@ -408,7 +408,6 @@ function file_default_mimetype_mapping() {
'doc' => 14, 'doc' => 14,
'bin' => 15, 'bin' => 15,
'oda' => 16, 'oda' => 16,
'ogg' => 17,
'ogx' => 17, 'ogx' => 17,
'pdf' => 18, 'pdf' => 18,
'key' => 19, 'key' => 19,
...@@ -630,6 +629,7 @@ function file_default_mimetype_mapping() { ...@@ -630,6 +629,7 @@ function file_default_mimetype_mapping() {
'm4a' => 188, 'm4a' => 188,
'mp3' => 188, 'mp3' => 188,
'mp2' => 188, 'mp2' => 188,
'ogg' => 189,
'oga' => 189, 'oga' => 189,
'spx' => 189, 'spx' => 189,
'sid' => 190, 'sid' => 190,
...@@ -853,6 +853,7 @@ function file_default_mimetype_mapping() { ...@@ -853,6 +853,7 @@ function file_default_mimetype_mapping() {
'f4a' => 346, 'f4a' => 346,
'f4b' => 346, 'f4b' => 346,
'flv' => 347, 'flv' => 347,
'm4v' => 348,
), ),
); );
} }
<?php <?php
// $Id: filetransfer.inc,v 1.9 2010/04/11 18:33:43 dries Exp $
/* /*
* Base FileTransfer class. * Base FileTransfer class.
...@@ -28,8 +27,15 @@ abstract class FileTransfer { ...@@ -28,8 +27,15 @@ abstract class FileTransfer {
* Classes that extend this class must override the factory() static method. * Classes that extend this class must override the factory() static method.
* *
* @param string $jail * @param string $jail
* The full path where all file operations performed by this object will
* be restricted to. This prevents the FileTransfer classes from being
* able to touch other parts of the filesystem.
* @param array $settings * @param array $settings
* @return object New instance of the appropriate FileTransfer subclass. * An array of connection settings for the FileTransfer subclass. If the
* getSettingsForm() method uses any nested settings, the same structure
* will be assumed here.
* @return object
* New instance of the appropriate FileTransfer subclass.
*/ */
static function factory($jail, $settings) { static function factory($jail, $settings) {
throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.'); throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
...@@ -205,10 +211,10 @@ abstract class FileTransfer { ...@@ -205,10 +211,10 @@ abstract class FileTransfer {
*/ */
protected function copyDirectoryJailed($source, $destination) { protected function copyDirectoryJailed($source, $destination) {
if ($this->isDirectory($destination)) { if ($this->isDirectory($destination)) {
$destination = $destination . '/' . basename($source); $destination = $destination . '/' . drupal_basename($source);
} }
$this->createDirectory($destination); $this->createDirectory($destination);
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) { foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
$relative_path = substr($filename, strlen($source)); $relative_path = substr($filename, strlen($source));
if ($file->isDir()) { if ($file->isDir()) {
$this->createDirectory($destination . $relative_path); $this->createDirectory($destination . $relative_path);
...@@ -296,7 +302,7 @@ abstract class FileTransfer { ...@@ -296,7 +302,7 @@ abstract class FileTransfer {
$chroot = ''; $chroot = '';
while (count($parts)) { while (count($parts)) {
$check = implode($parts, '/'); $check = implode($parts, '/');
if ($this->isFile($check . '/' . basename(__FILE__))) { if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
// Remove the trailing slash. // Remove the trailing slash.
return substr($chroot, 0, -1); return substr($chroot, 0, -1);
} }
...@@ -313,6 +319,42 @@ abstract class FileTransfer { ...@@ -313,6 +319,42 @@ abstract class FileTransfer {
$this->chroot = $this->findChroot(); $this->chroot = $this->findChroot();
$this->jail = $this->fixRemotePath($this->jail); $this->jail = $this->fixRemotePath($this->jail);
} }
/**
* Returns a form to collect connection settings credentials.
*
* Implementing classes can either extend this form with fields collecting the
* specific information they need, or override it entirely.
*/
public function getSettingsForm() {
$form['username'] = array(
'#type' => 'textfield',
'#title' => t('Username'),
);
$form['password'] = array(
'#type' => 'password',
'#title' => t('Password'),
'#description' => t('Your password is not saved in the database and is only used to establish a connection.'),
);
$form['advanced'] = array(
'#type' => 'fieldset',
'#title' => t('Advanced settings'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['advanced']['hostname'] = array(
'#type' => 'textfield',
'#title' => t('Host'),
'#default_value' => 'localhost',
'#description' => t('The connection will be created between your web server and the machine hosting the web server files. In the vast majority of cases, this will be the same machine, and "localhost" is correct.'),
);
$form['advanced']['port'] = array(
'#type' => 'textfield',
'#title' => t('Port'),
'#default_value' => NULL,
);
return $form;
}
} }
/** /**
...@@ -339,9 +381,47 @@ interface FileTransferChmodInterface { ...@@ -339,9 +381,47 @@ interface FileTransferChmodInterface {
* @param string $path * @param string $path
* Path to change permissions of. * Path to change permissions of.
* @param long $mode * @param long $mode
* @see http://php.net/chmod * The new file permission mode to be passed to chmod().
* @param boolean $recursive * @param boolean $recursive
* Pass TRUE to recursively chmod the entire directory specified in $path. * Pass TRUE to recursively chmod the entire directory specified in $path.
*/ */
function chmodJailed($path, $mode, $recursive); function chmodJailed($path, $mode, $recursive);
} }
/**
* Provides an interface for iterating recursively over filesystem directories.
*
* Manually skips '.' and '..' directories, since no existing method is
* available in PHP 5.2.
*
* @todo Depreciate in favor of RecursiveDirectoryIterator::SKIP_DOTS once PHP
* 5.3 or later is required.
*/
class SkipDotsRecursiveDirectoryIterator extends RecursiveDirectoryIterator {
/**
* Constructs a SkipDotsRecursiveDirectoryIterator
*
* @param $path
* The path of the directory to be iterated over.
*/
function __construct($path) {
parent::__construct($path);
$this->skipdots();
}
function rewind() {
parent::rewind();
$this->skipdots();
}
function next() {
parent::next();
$this->skipdots();
}
protected function skipdots() {
while ($this->isDot()) {
parent::next();
}
}
}
<?php <?php
// $Id: ftp.inc,v 1.14 2010/10/05 02:08:53 dries Exp $
/** /**
* Base class for FTP implementations. * Base class for FTP implementations.
...@@ -24,8 +23,10 @@ abstract class FileTransferFTP extends FileTransfer { ...@@ -24,8 +23,10 @@ abstract class FileTransferFTP extends FileTransfer {
* options. If the FTP PHP extension is available, use it. * options. If the FTP PHP extension is available, use it.
*/ */
static function factory($jail, $settings) { static function factory($jail, $settings) {
$settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname']; $username = empty($settings['username']) ? '' : $settings['username'];
$settings['port'] = empty($settings['port']) ? 21 : $settings['port']; $password = empty($settings['password']) ? '' : $settings['password'];
$hostname = empty($settings['advanced']['hostname']) ? 'localhost' : $settings['advanced']['hostname'];
$port = empty($settings['advanced']['port']) ? 21 : $settings['advanced']['port'];
if (function_exists('ftp_connect')) { if (function_exists('ftp_connect')) {
$class = 'FileTransferFTPExtension'; $class = 'FileTransferFTPExtension';
...@@ -34,7 +35,16 @@ abstract class FileTransferFTP extends FileTransfer { ...@@ -34,7 +35,16 @@ abstract class FileTransferFTP extends FileTransfer {
throw new FileTransferException('No FTP backend available.'); throw new FileTransferException('No FTP backend available.');
} }
return new $class($jail, $settings['username'], $settings['password'], $settings['hostname'], $settings['port']); return new $class($jail, $username, $password, $hostname, $port);
}
/**
* Returns the form to configure the FileTransfer class for FTP.
*/
public function getSettingsForm() {
$form = parent::getSettingsForm();
$form['advanced']['port']['#default_value'] = 21;
return $form;
} }
} }
......
<?php <?php
// $Id: local.inc,v 1.4 2010/08/17 22:05:22 dries Exp $
/** /**
* The local connection class for copying files as the httpd user. * The local connection class for copying files as the httpd user.
...@@ -31,7 +30,7 @@ class FileTransferLocal extends FileTransfer implements FileTransferChmodInterfa ...@@ -31,7 +30,7 @@ class FileTransferLocal extends FileTransfer implements FileTransferChmodInterfa
// Programmer error assertion, not something we expect users to see. // Programmer error assertion, not something we expect users to see.
throw new FileTransferException('removeDirectoryJailed() called with a path (%directory) that is not a directory.', NULL, array('%directory' => $directory)); throw new FileTransferException('removeDirectoryJailed() called with a path (%directory) that is not a directory.', NULL, array('%directory' => $directory));
} }
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory), RecursiveIteratorIterator::CHILD_FIRST) as $filename => $file) { foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($directory), RecursiveIteratorIterator::CHILD_FIRST) as $filename => $file) {
if ($file->isDir()) { if ($file->isDir()) {
if (@!drupal_rmdir($filename)) { if (@!drupal_rmdir($filename)) {
throw new FileTransferException('Cannot remove directory %directory.', NULL, array('%directory' => $filename)); throw new FileTransferException('Cannot remove directory %directory.', NULL, array('%directory' => $filename));
...@@ -64,7 +63,7 @@ class FileTransferLocal extends FileTransfer implements FileTransferChmodInterfa ...@@ -64,7 +63,7 @@ class FileTransferLocal extends FileTransfer implements FileTransferChmodInterfa
public function chmodJailed($path, $mode, $recursive) { public function chmodJailed($path, $mode, $recursive) {
if ($recursive && is_dir($path)) { if ($recursive && is_dir($path)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) { foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
if (@!chmod($filename, $mode)) { if (@!chmod($filename, $mode)) {
throw new FileTransferException('Cannot chmod %path.', NULL, array('%path' => $filename)); throw new FileTransferException('Cannot chmod %path.', NULL, array('%path' => $filename));
} }
......
<?php <?php
// $Id: ssh.inc,v 1.5 2010/01/30 07:59:24 dries Exp $
/** /**
* The SSH connection class for the update module. * The SSH connection class for the update module.
...@@ -17,7 +16,7 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface ...@@ -17,7 +16,7 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
function connect() { function connect() {
$this->connection = @ssh2_connect($this->hostname, $this->port); $this->connection = @ssh2_connect($this->hostname, $this->port);
if (!$this->connection) { if (!$this->connection) {
throw new FileTransferException('SSH Connection failed to @host:@port', NULL, array('@host' => $this->hostname, '@port' => 21)); throw new FileTransferException('SSH Connection failed to @host:@port', NULL, array('@host' => $this->hostname, '@port' => $this->port));
} }
if (!@ssh2_auth_password($this->connection, $this->username, $this->password)) { if (!@ssh2_auth_password($this->connection, $this->username, $this->password)) {
throw new FileTransferException('The supplied username/password combination was not accepted.'); throw new FileTransferException('The supplied username/password combination was not accepted.');
...@@ -25,9 +24,11 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface ...@@ -25,9 +24,11 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
} }
static function factory($jail, $settings) { static function factory($jail, $settings) {
$settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname']; $username = empty($settings['username']) ? '' : $settings['username'];
$settings['port'] = empty($settings['port']) ? 22 : $settings['port']; $password = empty($settings['password']) ? '' : $settings['password'];
return new FileTransferSSH($jail, $settings['username'], $settings['password'], $settings['hostname'], $settings['port']); $hostname = empty($settings['advanced']['hostname']) ? 'localhost' : $settings['advanced']['hostname'];
$port = empty($settings['advanced']['port']) ? 22 : $settings['advanced']['port'];
return new FileTransferSSH($jail, $username, $password, $hostname, $port);
} }
protected function copyFileJailed($source, $destination) { protected function copyFileJailed($source, $destination) {
...@@ -95,4 +96,13 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface ...@@ -95,4 +96,13 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
throw new FileTransferException('Cannot change permissions of @path.', NULL, array('@path' => $path)); throw new FileTransferException('Cannot change permissions of @path.', NULL, array('@path' => $path));
} }
} }
/**
* Returns the form to configure the FileTransfer class for SSH.
*/
public function getSettingsForm() {
$form = parent::getSettingsForm();
$form['advanced']['port']['#default_value'] = 22;
return $form;
}
} }
<?php <?php
// $Id: form.inc,v 1.506 2010/10/21 20:46:58 webchick Exp $ /**
* @file
* Functions for form and batch generation and processing.
*/
/** /**
* @defgroup forms Form builder functions * @defgroup forms Form builder functions
...@@ -17,7 +20,7 @@ ...@@ -17,7 +20,7 @@
* \@see user_pass_validate(). * \@see user_pass_validate().
* \@see user_pass_submit(). * \@see user_pass_submit().
* *
* @} End of "defgroup forms". * @}
*/ */
/** /**
...@@ -75,9 +78,9 @@ ...@@ -75,9 +78,9 @@
* the elements and properties of the form. For information on the array * the elements and properties of the form. For information on the array
* components and format, and more detailed explanations of the Form API * components and format, and more detailed explanations of the Form API
* workflow, see the * workflow, see the
* @link http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html Form API reference @endlink * @link forms_api_reference.html Form API reference @endlink
* and the * and the
* @link http://drupal.org/node/37775 Form API section of the handbook. @endlink * @link http://drupal.org/node/37775 Form API documentation section. @endlink
* In addition, there is a set of Form API tutorials in * In addition, there is a set of Form API tutorials in
* @link form_example_tutorial.inc the Form Example Tutorial @endlink which * @link form_example_tutorial.inc the Form Example Tutorial @endlink which
* provide basics all the way up through multistep forms. * provide basics all the way up through multistep forms.
...@@ -87,70 +90,15 @@ ...@@ -87,70 +90,15 @@
* passed by reference to most functions, so they use it to communicate with * passed by reference to most functions, so they use it to communicate with
* the form system and each other. * the form system and each other.
* *
* The $form_state keys are: * See drupal_build_form() for documentation of $form_state keys.
* - 'values': An associative array of values submitted to the form. The
* validation functions and submit functions use this array for nearly all
* their decision making. (Note that
* @link http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html/7#tree #tree @endlink
* determines whether the values are a flat array or an array whose structure
* parallels the $form array.)
* - 'rebuild': If the submit function sets $form_state['rebuild'] to TRUE,
* submission is not completed and instead the form is rebuilt using any
* information that the submit function has made available to the form builder
* function via $form_state. This is commonly used for wizard-style
* multi-step forms, add-more buttons, and the like. For further information
* see drupal_build_form().
* - 'redirect': a URL that will be used to redirect the form on submission.
* See drupal_redirect_form() for complete information.
* - 'storage': $form_state['storage'] is not a special key, and no specific
* support is provided for it in the Form API, but by tradition it was
* the location where application-specific data was stored for communication
* between the submit, validation, and form builder functions, especially
* in a multi-step-style form. Form implementations may use any key(s) within
* $form_state (other than the keys listed here and other reserved ones used
* by Form API internals) for this kind of storage. The recommended way to
* ensure that the chosen key doesn't conflict with ones used by the Form API
* or other modules is to use the module name as the key name or a prefix for
* the key name. For example, the Node module uses $form_state['node'] in node
* editing forms to store information about the node being edited, and this
* information stays available across successive clicks of the "Preview"
* button as well as when the "Save" button is finally clicked.
* - 'temporary': Since values for all non-reserved keys in $form_state persist
* throughout a multistep form sequence, the Form API provides the 'temporary'
* key for modules to use for communicating information across form-related
* functions during a single page request only. There is no use-case for this
* functionality in core.
* - 'triggering_element': (read-only) The form element that triggered
* submission. This is the same as the deprecated
* $form_state['clicked_button']. It is the element that caused submission,
* which may or may not be a button (in the case of AJAX forms.) This is
* often used to distinguish between various buttons in a submit handler,
* and is also used in AJAX handlers.
* - 'cache': The typical form workflow involves two page requests. During the
* first page request, a form is built and returned for the user to fill in.
* Then the user fills the form in and submits it, triggering a second page
* request in which the form must be built and processed. By default, $form
* and $form_state are built from scratch during each of these page requests.
* In some special use-cases, it is necessary or desired to persist the $form
* and $form_state variables from the initial page request to the one that
* processes the submission. A form builder function can set 'cache' to TRUE
* to do this. One example where this is needed is to handle AJAX submissions,
* so ajax_process_form() sets this for all forms that include an element with
* a #ajax property. (In AJAX, the handler has no way to build the form
* itself, so must rely on the cached version created on each page load, so
* it's a classic example of this use case.) Note that the persistence of
* $form and $form_state across successive submissions of a multi-step form
* happens automatically regardless of the value for 'cache'.
* - 'input': The array of values as they were submitted by the user. These are
* raw and unvalidated, so should not be used without a thorough understanding
* of security implications. In almost all cases, code should use the data in
* the 'values' array exclusively. The most common use of this key is for
* multi-step forms that need to clear some of the user input when setting
* 'rebuild'.
*/ */
/** /**
* Wrapper for drupal_build_form() for use when $form_state is not needed. * Returns a renderable form array for a given form ID.
*
* This function should be used instead of drupal_build_form() when $form_state
* is not needed (i.e., when initially rendering the form) and is often
* used as a menu callback.
* *
* @param $form_id * @param $form_id
* The unique string identifying the desired form. If a function with that * The unique string identifying the desired form. If a function with that
...@@ -158,7 +106,7 @@ ...@@ -158,7 +106,7 @@
* generate the same form (or very similar forms) using different $form_ids * generate the same form (or very similar forms) using different $form_ids
* can implement hook_forms(), which maps different $form_id values to the * can implement hook_forms(), which maps different $form_id values to the
* proper form constructor function. Examples may be found in node_forms(), * proper form constructor function. Examples may be found in node_forms(),
* search_forms(), and user_forms(). * and search_forms().
* @param ... * @param ...
* Any additional arguments are passed on to the functions called by * Any additional arguments are passed on to the functions called by
* drupal_get_form(), including the unique form constructor function. For * drupal_get_form(), including the unique form constructor function. For
...@@ -184,7 +132,7 @@ function drupal_get_form($form_id) { ...@@ -184,7 +132,7 @@ function drupal_get_form($form_id) {
} }
/** /**
* Build and process a form based on a form id. * Builds and process a form based on a form id.
* *
* The form may also be retrieved from the cache if the form was built in a * The form may also be retrieved from the cache if the form was built in a
* previous page-load. The form is then passed on for processing, validation * previous page-load. The form is then passed on for processing, validation
...@@ -196,8 +144,8 @@ function drupal_get_form($form_id) { ...@@ -196,8 +144,8 @@ function drupal_get_form($form_id) {
* generate the same form (or very similar forms) using different $form_ids * generate the same form (or very similar forms) using different $form_ids
* can implement hook_forms(), which maps different $form_id values to the * can implement hook_forms(), which maps different $form_id values to the
* proper form constructor function. Examples may be found in node_forms(), * proper form constructor function. Examples may be found in node_forms(),
* search_forms(), and user_forms(). * and search_forms().
* @param &$form_state * @param $form_state
* An array which stores information about the form. This is passed as a * An array which stores information about the form. This is passed as a
* reference so that the caller can use it to examine what in the form changed * reference so that the caller can use it to examine what in the form changed
* when the form submission process is complete. Furthermore, it may be used * when the form submission process is complete. Furthermore, it may be used
...@@ -205,22 +153,29 @@ function drupal_get_form($form_id) { ...@@ -205,22 +153,29 @@ function drupal_get_form($form_id) {
* persist across page requests when the 'cache' or 'rebuild' flag is set. * persist across page requests when the 'cache' or 'rebuild' flag is set.
* The following parameters may be set in $form_state to affect how the form * The following parameters may be set in $form_state to affect how the form
* is rendered: * is rendered:
* - build_info: A keyed array of build information that is necessary to * - build_info: Internal. An associative array of information stored by Form
* rebuild the form from cache when the original context may no longer be * API that is necessary to build and rebuild the form from cache when the
* available: * original context may no longer be available:
* - args: An array of arguments to pass to the form builder. * - args: A list of arguments to pass to the form constructor.
* - files: An optional array defining include files that need to be loaded * - files: An optional array defining include files that need to be loaded
* for building the form. Each array entry may be the path to a file or * for building the form. Each array entry may be the path to a file or
* another array containing values for the parameters 'type', 'module' and * another array containing values for the parameters 'type', 'module' and
* 'name' as needed by module_load_include(). The files listed here are * 'name' as needed by module_load_include(). The files listed here are
* automatically loaded by form_get_cache(). Defaults to the current menu * automatically loaded by form_get_cache(). By default the current menu
* router item's 'file' definition, if existent. * router item's 'file' definition is added, if any. Use
* form_load_include() to add include files from a form constructor.
* - form_id: Identification of the primary form being constructed and
* processed.
* - base_form_id: Identification for a base form, as declared in a
* hook_forms() implementation.
* - rebuild_info: Internal. Similar to 'build_info', but pertaining to
* drupal_rebuild_form().
* - rebuild: Normally, after the entire form processing is completed and * - rebuild: Normally, after the entire form processing is completed and
* submit handlers ran, a form is considered to be done and * submit handlers have run, a form is considered to be done and
* drupal_redirect_form() will redirect the user to a new page using a GET * drupal_redirect_form() will redirect the user to a new page using a GET
* request (so a browser refresh does not re-submit the form). However, if * request (so a browser refresh does not re-submit the form). However, if
* 'rebuild' has been set to TRUE, then a new copy of the form is * 'rebuild' has been set to TRUE, then a new copy of the form is
* immediately built and sent to the browser; instead of a redirect. This is * immediately built and sent to the browser, instead of a redirect. This is
* used for multi-step forms, such as wizards and confirmation forms. * used for multi-step forms, such as wizards and confirmation forms.
* Normally, $form_state['rebuild'] is set by a submit handler, since it is * Normally, $form_state['rebuild'] is set by a submit handler, since it is
* usually logic within a submit handler that determines whether a form is * usually logic within a submit handler that determines whether a form is
...@@ -228,32 +183,105 @@ function drupal_get_form($form_id) { ...@@ -228,32 +183,105 @@ function drupal_get_form($form_id) {
* set $form_state['rebuild'] to cause the form processing to bypass submit * set $form_state['rebuild'] to cause the form processing to bypass submit
* handlers and rebuild the form instead, even if there are no validation * handlers and rebuild the form instead, even if there are no validation
* errors. * errors.
* - input: An array of input that corresponds to $_POST or $_GET, depending * - redirect: Used to redirect the form on submission. It may either be a
* on the 'method' chosen (see below). * string containing the destination URL, or an array of arguments
* compatible with drupal_goto(). See drupal_redirect_form() for complete
* information.
* - no_redirect: If set to TRUE the form will NOT perform a drupal_goto(),
* even if 'redirect' is set.
* - method: The HTTP form method to use for finding the input for this form. * - method: The HTTP form method to use for finding the input for this form.
* May be 'post' or 'get'. Defaults to 'post'. Note that 'get' method * May be 'post' or 'get'. Defaults to 'post'. Note that 'get' method
* forms do not use form ids so are always considered to be submitted, which * forms do not use form ids so are always considered to be submitted, which
* can have unexpected effects. The 'get' method should only be used on * can have unexpected effects. The 'get' method should only be used on
* forms that do not change data, as that is exclusively the domain of post. * forms that do not change data, as that is exclusively the domain of
* - no_redirect: If set to TRUE the form will NOT perform a drupal_goto(), * 'post.'
* even if 'redirect' is set.
* - cache: If set to TRUE the original, unprocessed form structure will be * - cache: If set to TRUE the original, unprocessed form structure will be
* cached, which allows to rebuild the entire form from cache. * cached, which allows the entire form to be rebuilt from cache. A typical
* form workflow involves two page requests; first, a form is built and
* rendered for the user to fill in. Then, the user fills the form in and
* submits it, triggering a second page request in which the form must be
* built and processed. By default, $form and $form_state are built from
* scratch during each of these page requests. Often, it is necessary or
* desired to persist the $form and $form_state variables from the initial
* page request to the one that processes the submission. 'cache' can be set
* to TRUE to do this. A prominent example is an Ajax-enabled form, in which
* ajax_process_form() enables form caching for all forms that include an
* element with the #ajax property. (The Ajax handler has no way to build
* the form itself, so must rely on the cached version.) Note that the
* persistence of $form and $form_state happens automatically for
* (multi-step) forms having the 'rebuild' flag set, regardless of the value
* for 'cache'.
* - no_cache: If set to TRUE the form will NOT be cached, even if 'cache' is * - no_cache: If set to TRUE the form will NOT be cached, even if 'cache' is
* set. * set.
* - values: An associative array of values submitted to the form. The
* validation functions and submit functions use this array for nearly all
* their decision making. (Note that #tree determines whether the values are
* a flat array or an array whose structure parallels the $form array. See
* @link forms_api_reference.html Form API reference @endlink for more
* information.) These are raw and unvalidated, so should not be used
* without a thorough understanding of security implications. In almost all
* cases, code should use the data in the 'values' array exclusively. The
* most common use of this key is for multi-step forms that need to clear
* some of the user input when setting 'rebuild'. The values correspond to
* $_POST or $_GET, depending on the 'method' chosen.
* - always_process: If TRUE and the method is GET, a form_id is not * - always_process: If TRUE and the method is GET, a form_id is not
* necessary. This should only be used on RESTful GET forms that do NOT * necessary. This should only be used on RESTful GET forms that do NOT
* write data, as this could lead to security issues. It is useful so that * write data, as this could lead to security issues. It is useful so that
* searches do not need to have a form_id in their query arguments to * searches do not need to have a form_id in their query arguments to
* trigger the search. * trigger the search.
* - must_validate: Ordinarily, a form is only validated once but there are * - must_validate: Ordinarily, a form is only validated once, but there are
* times when a form is resubmitted internally and should be validated * times when a form is resubmitted internally and should be validated
* again. Setting this to TRUE will force that to happen. This is most * again. Setting this to TRUE will force that to happen. This is most
* likely to occur during AHAH or AJAX operations. * likely to occur during Ajax operations.
* - programmed: If TRUE, the form was submitted programmatically, usually
* invoked via drupal_form_submit(). Defaults to FALSE.
* - process_input: Boolean flag. TRUE signifies correct form submission.
* This is always TRUE for programmed forms coming from drupal_form_submit()
* (see 'programmed' key), or if the form_id coming from the $_POST data is
* set and matches the current form_id.
* - submitted: If TRUE, the form has been submitted. Defaults to FALSE.
* - executed: If TRUE, the form was submitted and has been processed and
* executed. Defaults to FALSE.
* - triggering_element: (read-only) The form element that triggered
* submission. This is the same as the deprecated
* $form_state['clicked_button']. It is the element that caused submission,
* which may or may not be a button (in the case of Ajax forms). This key is
* often used to distinguish between various buttons in a submit handler,
* and is also used in Ajax handlers.
* - clicked_button: Deprecated. Use triggering_element instead.
* - has_file_element: Internal. If TRUE, there is a file element and Form API
* will set the appropriate 'enctype' HTML attribute on the form.
* - groups: Internal. An array containing references to fieldsets to render
* them within vertical tabs.
* - storage: $form_state['storage'] is not a special key, and no specific
* support is provided for it in the Form API. By tradition it was
* the location where application-specific data was stored for communication
* between the submit, validation, and form builder functions, especially
* in a multi-step-style form. Form implementations may use any key(s)
* within $form_state (other than the keys listed here and other reserved
* ones used by Form API internals) for this kind of storage. The
* recommended way to ensure that the chosen key doesn't conflict with ones
* used by the Form API or other modules is to use the module name as the
* key name or a prefix for the key name. For example, the Node module uses
* $form_state['node'] in node editing forms to store information about the
* node being edited, and this information stays available across successive
* clicks of the "Preview" button as well as when the "Save" button is
* finally clicked.
* - buttons: A list containing copies of all submit and button elements in
* the form.
* - complete form: A reference to the $form variable containing the complete
* form structure. #process, #after_build, #element_validate, and other
* handlers being invoked on a form element may use this reference to access
* other information in the form the element is contained in.
* - temporary: An array holding temporary data accessible during the current * - temporary: An array holding temporary data accessible during the current
* page request only. It may be used to temporary save any data that doesn't * page request only. All $form_state properties that are not reserved keys
* need to or shouldn't be cached during the whole form workflow, e.g. data * (see form_state_keys_no_cache()) persist throughout a multistep form
* that needs to be accessed during the current form build process only. * sequence. Form API provides this key for modules to communicate
* information across form-related functions during a single page request.
* It may be used to temporarily save data that does not need to or should
* not be cached during the whole form workflow; e.g., data that needs to be
* accessed during the current form build process only. There is no use-case
* for this functionality in Drupal core.
* - wrapper_callback: Modules that wish to pre-populate certain forms with * - wrapper_callback: Modules that wish to pre-populate certain forms with
* common elements, such as back/next/save buttons in multi-step form * common elements, such as back/next/save buttons in multi-step form
* wizards, may define a form builder function name that returns a form * wizards, may define a form builder function name that returns a form
...@@ -262,11 +290,12 @@ function drupal_get_form($form_id) { ...@@ -262,11 +290,12 @@ function drupal_get_form($form_id) {
* hook_forms() or have to invoke drupal_build_form() (instead of * hook_forms() or have to invoke drupal_build_form() (instead of
* drupal_get_form()) on their own in a custom menu callback to prepare * drupal_get_form()) on their own in a custom menu callback to prepare
* $form_state accordingly. * $form_state accordingly.
* Further $form_state properties controlling the redirection behavior after * Information on how certain $form_state properties control redirection
* form submission may be found in drupal_redirect_form(). * behavior after form submission may be found in drupal_redirect_form().
* *
* @return * @return
* The rendered form or NULL, depending upon the $form_state flags that were set. * The rendered form. This function may also perform a redirect and hence may
* not return at all, depending upon the $form_state flags that were set.
* *
* @see drupal_redirect_form() * @see drupal_redirect_form()
*/ */
...@@ -356,14 +385,19 @@ function drupal_build_form($form_id, &$form_state) { ...@@ -356,14 +385,19 @@ function drupal_build_form($form_id, &$form_state) {
} }
/** /**
* Retrieve default values for the $form_state array. * Retrieves default values for the $form_state array.
*/ */
function form_state_defaults() { function form_state_defaults() {
return array( return array(
'rebuild' => FALSE, 'rebuild' => FALSE,
'rebuild_info' => array(), 'rebuild_info' => array(),
'redirect' => NULL, 'redirect' => NULL,
'build_info' => array('args' => array()), // @todo 'args' is usually set, so no other default 'build_info' keys are
// appended via += form_state_defaults().
'build_info' => array(
'args' => array(),
'files' => array(),
),
'temporary' => array(), 'temporary' => array(),
'submitted' => FALSE, 'submitted' => FALSE,
'executed' => FALSE, 'executed' => FALSE,
...@@ -386,9 +420,9 @@ function form_state_defaults() { ...@@ -386,9 +420,9 @@ function form_state_defaults() {
* function is called to generate a new $form, the next step in the form * function is called to generate a new $form, the next step in the form
* workflow, to be returned for rendering. * workflow, to be returned for rendering.
* *
* AJAX form submissions are almost always multi-step workflows, so that is one * Ajax form submissions are almost always multi-step workflows, so that is one
* common use-case during which form rebuilding occurs. See ajax_form_callback() * common use-case during which form rebuilding occurs. See ajax_form_callback()
* for more information about creating AJAX-enabled forms. * for more information about creating Ajax-enabled forms.
* *
* @param $form_id * @param $form_id
* The unique string identifying the desired form. If a function * The unique string identifying the desired form. If a function
...@@ -396,12 +430,12 @@ function form_state_defaults() { ...@@ -396,12 +430,12 @@ function form_state_defaults() {
* Modules that need to generate the same form (or very similar forms) * Modules that need to generate the same form (or very similar forms)
* using different $form_ids can implement hook_forms(), which maps * using different $form_ids can implement hook_forms(), which maps
* different $form_id values to the proper form constructor function. Examples * different $form_id values to the proper form constructor function. Examples
* may be found in node_forms(), search_forms(), and user_forms(). * may be found in node_forms() and search_forms().
* @param $form_state * @param $form_state
* A keyed array containing the current state of the form. * A keyed array containing the current state of the form.
* @param $old_form * @param $old_form
* (optional) A previously built $form. Used to retain the #build_id and * (optional) A previously built $form. Used to retain the #build_id and
* #action properties in AJAX callbacks and similar partial form rebuilds. The * #action properties in Ajax callbacks and similar partial form rebuilds. The
* only properties copied from $old_form are the ones which both exist in * only properties copied from $old_form are the ones which both exist in
* $old_form and for which $form_state['rebuild_info']['copy'][PROPERTY] is * $old_form and for which $form_state['rebuild_info']['copy'][PROPERTY] is
* TRUE. If $old_form is not passed, the entire $form is rebuilt freshly. * TRUE. If $old_form is not passed, the entire $form is rebuilt freshly.
...@@ -417,7 +451,7 @@ function form_state_defaults() { ...@@ -417,7 +451,7 @@ function form_state_defaults() {
function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
$form = drupal_retrieve_form($form_id, $form_state); $form = drupal_retrieve_form($form_id, $form_state);
// If only parts of the form will be returned to the browser (e.g. AJAX or // If only parts of the form will be returned to the browser (e.g., Ajax or
// RIA clients), re-use the old #build_id to not require client-side code to // RIA clients), re-use the old #build_id to not require client-side code to
// manually update the hidden 'build_id' input element. // manually update the hidden 'build_id' input element.
// Otherwise, a new #build_id is generated, to not clobber the previous // Otherwise, a new #build_id is generated, to not clobber the previous
...@@ -431,7 +465,7 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { ...@@ -431,7 +465,7 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
$form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand()); $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
} }
// #action defaults to request_uri(), but in case of AJAX and other partial // #action defaults to request_uri(), but in case of Ajax and other partial
// rebuilds, the form is submitted to an alternate URL, and the original // rebuilds, the form is submitted to an alternate URL, and the original
// #action needs to be retained. // #action needs to be retained.
if (isset($old_form['#action']) && !empty($form_state['rebuild_info']['copy']['#action'])) { if (isset($old_form['#action']) && !empty($form_state['rebuild_info']['copy']['#action'])) {
...@@ -457,7 +491,7 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { ...@@ -457,7 +491,7 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
} }
/** /**
* Fetch a form from cache. * Fetches a form from cache.
*/ */
function form_get_cache($form_build_id, &$form_state) { function form_get_cache($form_build_id, &$form_state) {
if ($cached = cache_get('form_' . $form_build_id, 'cache_form')) { if ($cached = cache_get('form_' . $form_build_id, 'cache_form')) {
...@@ -470,7 +504,7 @@ function form_get_cache($form_build_id, &$form_state) { ...@@ -470,7 +504,7 @@ function form_get_cache($form_build_id, &$form_state) {
$form_state = $cached->data + $form_state; $form_state = $cached->data + $form_state;
// If the original form is contained in include files, load the files. // If the original form is contained in include files, load the files.
// See drupal_build_form(). // @see form_load_include()
$form_state['build_info'] += array('files' => array()); $form_state['build_info'] += array('files' => array());
foreach ($form_state['build_info']['files'] as $file) { foreach ($form_state['build_info']['files'] as $file) {
if (is_array($file)) { if (is_array($file)) {
...@@ -488,7 +522,7 @@ function form_get_cache($form_build_id, &$form_state) { ...@@ -488,7 +522,7 @@ function form_get_cache($form_build_id, &$form_state) {
} }
/** /**
* Store a form in the cache. * Stores a form in the cache.
*/ */
function form_set_cache($form_build_id, $form, $form_state) { function form_set_cache($form_build_id, $form, $form_state) {
// 6 hours cache life time for forms should be plenty. // 6 hours cache life time for forms should be plenty.
...@@ -537,6 +571,55 @@ function form_state_keys_no_cache() { ...@@ -537,6 +571,55 @@ function form_state_keys_no_cache() {
); );
} }
/**
* Ensures an include file is loaded whenever the form is processed.
*
* Example:
* @code
* // Load node.admin.inc from Node module.
* form_load_include($form_state, 'inc', 'node', 'node.admin');
* @endcode
*
* Use this function instead of module_load_include() from inside a form
* constructor or any form processing logic as it ensures that the include file
* is loaded whenever the form is processed. In contrast to using
* module_load_include() directly, form_load_include() makes sure the include
* file is correctly loaded also if the form is cached.
*
* @param $form_state
* The current state of the form.
* @param $type
* The include file's type (file extension).
* @param $module
* The module to which the include file belongs.
* @param $name
* (optional) The base file name (without the $type extension). If omitted,
* $module is used; i.e., resulting in "$module.$type" by default.
*
* @return
* The filepath of the loaded include file, or FALSE if the include file was
* not found or has been loaded already.
*
* @see module_load_include()
*/
function form_load_include(&$form_state, $type, $module, $name = NULL) {
if (!isset($name)) {
$name = $module;
}
if (!isset($form_state['build_info']['files']["$module:$name.$type"])) {
// Only add successfully included files to the form state.
if ($result = module_load_include($type, $module, $name)) {
$form_state['build_info']['files']["$module:$name.$type"] = array(
'type' => $type,
'module' => $module,
'name' => $name,
);
return $result;
}
}
return FALSE;
}
/** /**
* Retrieves, populates, and processes a form. * Retrieves, populates, and processes a form.
* *
...@@ -553,7 +636,7 @@ function form_state_keys_no_cache() { ...@@ -553,7 +636,7 @@ function form_state_keys_no_cache() {
* Modules that need to generate the same form (or very similar forms) * Modules that need to generate the same form (or very similar forms)
* using different $form_ids can implement hook_forms(), which maps * using different $form_ids can implement hook_forms(), which maps
* different $form_id values to the proper form constructor function. Examples * different $form_id values to the proper form constructor function. Examples
* may be found in node_forms(), search_forms(), and user_forms(). * may be found in node_forms() and search_forms().
* @param $form_state * @param $form_state
* A keyed array containing the current state of the form. Most important is * A keyed array containing the current state of the form. Most important is
* the $form_state['values'] collection, a tree of data used to simulate the * the $form_state['values'] collection, a tree of data used to simulate the
...@@ -602,9 +685,14 @@ function drupal_form_submit($form_id, &$form_state) { ...@@ -602,9 +685,14 @@ function drupal_form_submit($form_id, &$form_state) {
// Merge in default values. // Merge in default values.
$form_state += form_state_defaults(); $form_state += form_state_defaults();
$form = drupal_retrieve_form($form_id, $form_state); // Populate $form_state['input'] with the submitted values before retrieving
// the form, to be consistent with what drupal_build_form() does for
// non-programmatic submissions (form builder functions may expect it to be
// there).
$form_state['input'] = $form_state['values']; $form_state['input'] = $form_state['values'];
$form_state['programmed'] = TRUE; $form_state['programmed'] = TRUE;
$form = drupal_retrieve_form($form_id, $form_state);
// Programmed forms are always submitted. // Programmed forms are always submitted.
$form_state['submitted'] = TRUE; $form_state['submitted'] = TRUE;
...@@ -633,14 +721,19 @@ function drupal_form_submit($form_id, &$form_state) { ...@@ -633,14 +721,19 @@ function drupal_form_submit($form_id, &$form_state) {
function drupal_retrieve_form($form_id, &$form_state) { function drupal_retrieve_form($form_id, &$form_state) {
$forms = &drupal_static(__FUNCTION__); $forms = &drupal_static(__FUNCTION__);
// Record the $form_id.
$form_state['build_info']['form_id'] = $form_id;
// Record the filepath of the include file containing the original form, so // Record the filepath of the include file containing the original form, so
// the form builder callbacks can be loaded when the form is being rebuilt // the form builder callbacks can be loaded when the form is being rebuilt
// from cache on a different path (such as 'system/ajax'). See // from cache on a different path (such as 'system/ajax'). See
// form_get_cache(). // form_get_cache().
// $menu_get_item() is not available at installation time. // $menu_get_item() is not available during installation.
if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) { if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) {
$item = menu_get_item(); $item = menu_get_item();
if (!empty($item['include_file'])) { if (!empty($item['include_file'])) {
// Do not use form_load_include() here, as the file is already loaded.
// Anyway, form_get_cache() is able to handle filepaths too.
$form_state['build_info']['files']['menu'] = $item['include_file']; $form_state['build_info']['files']['menu'] = $item['include_file'];
} }
} }
...@@ -757,7 +850,10 @@ function drupal_process_form($form_id, &$form, &$form_state) { ...@@ -757,7 +850,10 @@ function drupal_process_form($form_id, &$form, &$form_state) {
// cache when a form is processed, so scenarios that result in // cache when a form is processed, so scenarios that result in
// the form being built behind the scenes and again for the // the form being built behind the scenes and again for the
// browser don't increment all the element IDs needlessly. // browser don't increment all the element IDs needlessly.
if (!form_get_errors()) {
// In case of errors, do not break HTML IDs of other forms.
drupal_static_reset('drupal_html_id'); drupal_static_reset('drupal_html_id');
}
if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild']) { if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild']) {
// Execute form submit handlers. // Execute form submit handlers.
...@@ -816,13 +912,13 @@ function drupal_process_form($form_id, &$form, &$form_state) { ...@@ -816,13 +912,13 @@ function drupal_process_form($form_id, &$form, &$form_state) {
// yet complete. A new $form needs to be constructed based on the changes // yet complete. A new $form needs to be constructed based on the changes
// made to $form_state during this request. Normally, a submit handler sets // made to $form_state during this request. Normally, a submit handler sets
// $form_state['rebuild'] if a fully executed form requires another step. // $form_state['rebuild'] if a fully executed form requires another step.
// However, for forms that have not been fully executed (e.g., AJAX // However, for forms that have not been fully executed (e.g., Ajax
// submissions triggered by non-buttons), there is no submit handler to set // submissions triggered by non-buttons), there is no submit handler to set
// $form_state['rebuild']. It would not make sense to redisplay the // $form_state['rebuild']. It would not make sense to redisplay the
// identical form without an error for the user to correct, so we also // identical form without an error for the user to correct, so we also
// rebuild error-free non-executed forms, regardless of // rebuild error-free non-executed forms, regardless of
// $form_state['rebuild']. // $form_state['rebuild'].
// @todo D8: Simplify this logic; considering AJAX and non-HTML front-ends, // @todo D8: Simplify this logic; considering Ajax and non-HTML front-ends,
// along with element-level #submit properties, it makes no sense to have // along with element-level #submit properties, it makes no sense to have
// divergent form execution based on whether the triggering element has // divergent form execution based on whether the triggering element has
// #executes_submit_callback set to TRUE. // #executes_submit_callback set to TRUE.
...@@ -839,7 +935,7 @@ function drupal_process_form($form_id, &$form, &$form_state) { ...@@ -839,7 +935,7 @@ function drupal_process_form($form_id, &$form, &$form_state) {
// have set $form_state['cache'] to indicate that the form and form state // have set $form_state['cache'] to indicate that the form and form state
// shall be cached. But the form may only be cached if the 'no_cache' property // shall be cached. But the form may only be cached if the 'no_cache' property
// is not set to TRUE. Only cache $form as it was prior to form_builder(), // is not set to TRUE. Only cache $form as it was prior to form_builder(),
// because form_builder() must run for each request to accomodate new user // because form_builder() must run for each request to accommodate new user
// input. Rebuilt forms are not cached here, because drupal_rebuild_form() // input. Rebuilt forms are not cached here, because drupal_rebuild_form()
// already takes care of that. // already takes care of that.
if (!$form_state['rebuild'] && $form_state['cache'] && empty($form_state['no_cache'])) { if (!$form_state['rebuild'] && $form_state['cache'] && empty($form_state['no_cache'])) {
...@@ -848,9 +944,10 @@ function drupal_process_form($form_id, &$form, &$form_state) { ...@@ -848,9 +944,10 @@ function drupal_process_form($form_id, &$form, &$form_state) {
} }
/** /**
* Prepares a structured form array by adding required elements, * Prepares a structured form array.
* executing any hook_form_alter functions, and optionally inserting *
* a validation token to prevent tampering. * Adds required elements, executes any hook_form_alter functions, and
* optionally inserts a validation token to prevent tampering.
* *
* @param $form_id * @param $form_id
* A unique string identifying the form for validation, submission, * A unique string identifying the form for validation, submission,
...@@ -886,6 +983,10 @@ function drupal_prepare_form($form_id, &$form, &$form_state) { ...@@ -886,6 +983,10 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
'#value' => $form['#build_id'], '#value' => $form['#build_id'],
'#id' => $form['#build_id'], '#id' => $form['#build_id'],
'#name' => 'form_build_id', '#name' => 'form_build_id',
// Form processing and validation requires this value, so ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => array('form_build_id'),
); );
// Add a token, based on either #token or form_id, to any form displayed to // Add a token, based on either #token or form_id, to any form displayed to
...@@ -909,6 +1010,10 @@ function drupal_prepare_form($form_id, &$form, &$form_state) { ...@@ -909,6 +1010,10 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
'#id' => drupal_html_id('edit-' . $form_id . '-form-token'), '#id' => drupal_html_id('edit-' . $form_id . '-form-token'),
'#type' => 'token', '#type' => 'token',
'#default_value' => drupal_get_token($form['#token']), '#default_value' => drupal_get_token($form['#token']),
// Form processing and validation requires this value, so ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => array('form_token'),
); );
} }
} }
...@@ -918,6 +1023,10 @@ function drupal_prepare_form($form_id, &$form, &$form_state) { ...@@ -918,6 +1023,10 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
'#type' => 'hidden', '#type' => 'hidden',
'#value' => $form_id, '#value' => $form_id,
'#id' => drupal_html_id("edit-$form_id"), '#id' => drupal_html_id("edit-$form_id"),
// Form processing and validation requires this value, so ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => array('form_id'),
); );
} }
if (!isset($form['#id'])) { if (!isset($form['#id'])) {
...@@ -928,6 +1037,8 @@ function drupal_prepare_form($form_id, &$form, &$form_state) { ...@@ -928,6 +1037,8 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
$form += array('#tree' => FALSE, '#parents' => array()); $form += array('#tree' => FALSE, '#parents' => array());
if (!isset($form['#validate'])) { if (!isset($form['#validate'])) {
// Ensure that modules can rely on #validate being set.
$form['#validate'] = array();
// Check for a handler specific to $form_id. // Check for a handler specific to $form_id.
if (function_exists($form_id . '_validate')) { if (function_exists($form_id . '_validate')) {
$form['#validate'][] = $form_id . '_validate'; $form['#validate'][] = $form_id . '_validate';
...@@ -940,6 +1051,8 @@ function drupal_prepare_form($form_id, &$form, &$form_state) { ...@@ -940,6 +1051,8 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
} }
if (!isset($form['#submit'])) { if (!isset($form['#submit'])) {
// Ensure that modules can rely on #submit being set.
$form['#submit'] = array();
// Check for a handler specific to $form_id. // Check for a handler specific to $form_id.
if (function_exists($form_id . '_submit')) { if (function_exists($form_id . '_submit')) {
$form['#submit'][] = $form_id . '_submit'; $form['#submit'][] = $form_id . '_submit';
...@@ -974,8 +1087,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) { ...@@ -974,8 +1087,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
/** /**
* Validates user-submitted form data from the $form_state using * Validates user-submitted form data in the $form_state array.
* the validate functions defined in a structured form array.
* *
* @param $form_id * @param $form_id
* A unique string identifying the form for validation, submission, * A unique string identifying the form for validation, submission,
...@@ -992,7 +1104,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) { ...@@ -992,7 +1104,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
* A keyed array containing the current state of the form. The current * A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though * user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the * form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also * values for the sake of simplicity. Validation handlers can also use
* $form_state to pass information on to submit handlers. For example: * $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data; * $form_state['data_for_submission'] = $data;
* This technique is useful when validation requires file parsing, * This technique is useful when validation requires file parsing,
...@@ -1010,8 +1122,12 @@ function drupal_validate_form($form_id, &$form, &$form_state) { ...@@ -1010,8 +1122,12 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
// matches the current user's session. // matches the current user's session.
if (isset($form['#token'])) { if (isset($form['#token'])) {
if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) { if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) {
$path = current_path();
$query = drupal_get_query_parameters();
$url = url($path, array('query' => $query));
// Setting this error will cause the form to fail validation. // Setting this error will cause the form to fail validation.
form_set_error('form_token', t('This form is outdated. Reload the page and try again. Contact the site administrator if the problem persists.')); 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)));
} }
} }
...@@ -1031,10 +1147,29 @@ function drupal_validate_form($form_id, &$form, &$form_state) { ...@@ -1031,10 +1147,29 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
drupal_array_set_nested_value($values, $section, $value); drupal_array_set_nested_value($values, $section, $value);
} }
} }
// For convenience we always make the value of the pressed button available. // A button's #value does not require validation, so for convenience we
// allow the value of the clicked button to be retained in its normal
// $form_state['values'] locations, even if these locations are not included
// in #limit_validation_errors.
if (isset($form_state['triggering_element']['#button_type'])) { if (isset($form_state['triggering_element']['#button_type'])) {
$values[$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value']; $button_value = $form_state['triggering_element']['#value'];
drupal_array_set_nested_value($values, $form_state['triggering_element']['#parents'], $form_state['triggering_element']['#value']);
// Like all input controls, the button value may be in the location
// dictated by #parents. If it is, copy it to $values, but do not override
// what may already be in $values.
$parents = $form_state['triggering_element']['#parents'];
if (!drupal_array_nested_key_exists($values, $parents) && drupal_array_get_nested_value($form_state['values'], $parents) === $button_value) {
drupal_array_set_nested_value($values, $parents, $button_value);
}
// Additionally, form_builder() places the button value in
// $form_state['values'][BUTTON_NAME]. If it's still there, after
// validation handlers have run, copy it to $values, but do not override
// what may already be in $values.
$name = $form_state['triggering_element']['#name'];
if (!isset($values[$name]) && isset($form_state['values'][$name]) && $form_state['values'][$name] === $button_value) {
$values[$name] = $button_value;
}
} }
$form_state['values'] = $values; $form_state['values'] = $values;
} }
...@@ -1043,35 +1178,56 @@ function drupal_validate_form($form_id, &$form, &$form_state) { ...@@ -1043,35 +1178,56 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
/** /**
* Redirects the user to a URL after a form has been processed. * Redirects the user to a URL after a form has been processed.
* *
* After a form was executed, the data in $form_state controls whether the form * After a form is submitted and processed, normally the user should be
* is redirected. By default, we redirect to a new destination page. The path of * redirected to a new destination page. This function figures out what that
* the destination page can be set in $form_state['redirect']. If that is not * destination should be, based on the $form_state array and the 'destination'
* set, the user is redirected to the current page to display a fresh, * query string in the request URL, and redirects the user there.
* unpopulated copy of the form. *
* * Usually (for exceptions, see below) $form_state['redirect'] determines where
* There are several triggers that may prevent a redirection though: * to redirect the user. This can be set either to a string (the path to
* - If $form_state['redirect'] is FALSE, a form builder function or form * redirect to), or an array of arguments for drupal_goto(). If
* validation/submit handler does not want a user to be redirected, which * $form_state['redirect'] is missing, the user is usually (again, see below for
* means that drupal_goto() is not invoked. For most forms, the redirection * exceptions) redirected back to the page they came from, where they should see
* logic will be the same regardless of whether $form_state['redirect'] is * a fresh, unpopulated copy of the form.
* undefined or FALSE. However, in case it was not defined and the current *
* request contains a 'destination' query string, drupal_goto() will redirect * Here is an example of how to set up a form to redirect to the path 'node':
* to that given destination instead. Only setting $form_state['redirect'] to * @code
* FALSE will prevent any redirection. * $form_state['redirect'] = 'node';
* - If $form_state['no_redirect'] is TRUE, then the callback that originally * @endcode
* built the form explicitly disallows any redirection, regardless of the * And here is an example of how to redirect to 'node/123?foo=bar#baz':
* redirection value in $form_state['redirect']. For example, ajax_get_form() * @code
* defines $form_state['no_redirect'] when building a form in an AJAX * $form_state['redirect'] = array(
* callback to prevent any redirection. $form_state['no_redirect'] should NOT * 'node/123',
* be altered by form builder functions or form validation/submit handlers. * array(
* 'query' => array(
* 'foo' => 'bar',
* ),
* 'fragment' => 'baz',
* ),
* );
* @endcode
*
* There are several exceptions to the "usual" behavior described above:
* - If $form_state['programmed'] is TRUE, the form submission was usually * - If $form_state['programmed'] is TRUE, the form submission was usually
* invoked via drupal_form_submit(), so any redirection would break the script * invoked via drupal_form_submit(), so any redirection would break the script
* that invoked drupal_form_submit(). * that invoked drupal_form_submit() and no redirection is done.
* - If $form_state['rebuild'] is TRUE, the form needs to be rebuilt without * - If $form_state['rebuild'] is TRUE, the form is being rebuilt, and no
* redirection. * redirection is done.
* - If $form_state['no_redirect'] is TRUE, redirection is disabled. This is
* set, for instance, by ajax_get_form() to prevent redirection in Ajax
* callbacks. $form_state['no_redirect'] should never be set or altered by
* form builder functions or form validation/submit handlers.
* - If $form_state['redirect'] is set to FALSE, redirection is disabled.
* - If none of the above conditions has prevented redirection, then the
* redirect is accomplished by calling drupal_goto(), passing in the value of
* $form_state['redirect'] if it is set, or the current path if it is
* not. drupal_goto() preferentially uses the value of $_GET['destination']
* (the 'destination' URL query string) if it is present, so this will
* override any values set by $form_state['redirect']. Note that during
* installation, install_goto() is called in place of drupal_goto().
* *
* @param $form_state * @param $form_state
* A keyed array containing the current state of the form. * An associative array containing the current state of the form.
* *
* @see drupal_process_form() * @see drupal_process_form()
* @see drupal_build_form() * @see drupal_build_form()
...@@ -1103,14 +1259,16 @@ function drupal_redirect_form($form_state) { ...@@ -1103,14 +1259,16 @@ function drupal_redirect_form($form_state) {
$function($form_state['redirect']); $function($form_state['redirect']);
} }
} }
drupal_goto($_GET['q']); drupal_goto(current_path(), array('query' => drupal_get_query_parameters()));
} }
} }
/** /**
* Performs validation on form elements. First ensures required fields are * Performs validation on form elements.
* completed, #maxlength is not exceeded, and selected options were in the *
* list of options given to the user. Then calls user-defined validators. * First ensures required fields are completed, #maxlength is not exceeded, and
* selected options were in the list of options given to the user. Then calls
* user-defined validators.
* *
* @param $elements * @param $elements
* An associative array containing the structure of the form. * An associative array containing the structure of the form.
...@@ -1201,7 +1359,7 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) { ...@@ -1201,7 +1359,7 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
// If submit handlers won't run (due to the submission having been triggered // If submit handlers won't run (due to the submission having been triggered
// by an element whose #executes_submit_callback property isn't TRUE), then // by an element whose #executes_submit_callback property isn't TRUE), then
// it's safe to suppress all validation errors, and we do so by default, // it's safe to suppress all validation errors, and we do so by default,
// which is particularly useful during an AJAX submission triggered by a // which is particularly useful during an Ajax submission triggered by a
// non-button. An element can override this default by setting the // non-button. An element can override this default by setting the
// #limit_validation_errors property. For button element types, // #limit_validation_errors property. For button element types,
// #limit_validation_errors defaults to FALSE (via system_element_info()), // #limit_validation_errors defaults to FALSE (via system_element_info()),
...@@ -1262,9 +1420,10 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) { ...@@ -1262,9 +1420,10 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
} }
/** /**
* A helper function used to execute custom validation and submission * Executes custom validation and submission handlers for a given form.
* handlers for a given form. Button-specific handlers are checked *
* first. If none exist, the function falls back to form-level handlers. * Button-specific handlers are checked first. If none exist, the function
* falls back to form-level handlers.
* *
* @param $type * @param $type
* The type of handler to execute. 'validate' or 'submit' are the * The type of handler to execute. 'validate' or 'submit' are the
...@@ -1386,8 +1545,6 @@ function form_execute_handlers($type, &$form, &$form_state) { ...@@ -1386,8 +1545,6 @@ function form_execute_handlers($type, &$form, &$form_state) {
* doing anything with that data that requires it to be valid, PHP errors * doing anything with that data that requires it to be valid, PHP errors
* would be triggered if the input processing and validation steps were fully * would be triggered if the input processing and validation steps were fully
* skipped. * skipped.
* @see http://drupal.org/node/370537
* @see http://drupal.org/node/763376
* *
* @param $name * @param $name
* The name of the form element. If the #parents property of your form * The name of the form element. If the #parents property of your form
...@@ -1403,6 +1560,9 @@ function form_execute_handlers($type, &$form, &$form_state) { ...@@ -1403,6 +1560,9 @@ function form_execute_handlers($type, &$form, &$form_state) {
* @return * @return
* Return value is for internal use only. To get a list of errors, use * Return value is for internal use only. To get a list of errors, use
* form_get_errors() or form_get_error(). * form_get_errors() or form_get_error().
*
* @see http://drupal.org/node/370537
* @see http://drupal.org/node/763376
*/ */
function form_set_error($name = NULL, $message = '', $limit_validation_errors = NULL) { function form_set_error($name = NULL, $message = '', $limit_validation_errors = NULL) {
$form = &drupal_static(__FUNCTION__, array()); $form = &drupal_static(__FUNCTION__, array());
...@@ -1427,8 +1587,10 @@ function form_set_error($name = NULL, $message = '', $limit_validation_errors = ...@@ -1427,8 +1587,10 @@ function form_set_error($name = NULL, $message = '', $limit_validation_errors =
// reconstructed #parents begin with the same keys as the specified // reconstructed #parents begin with the same keys as the specified
// section, then the element's values are within the part of // section, then the element's values are within the part of
// $form_state['values'] that the clicked button requires to be valid, // $form_state['values'] that the clicked button requires to be valid,
// so errors for this element must be recorded. // so errors for this element must be recorded. As the exploded array
if (array_slice(explode('][', $name), 0, count($section)) === $section) { // will all be strings, we need to cast every value of the section
// array to string.
if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) {
$record = TRUE; $record = TRUE;
break; break;
} }
...@@ -1446,14 +1608,14 @@ function form_set_error($name = NULL, $message = '', $limit_validation_errors = ...@@ -1446,14 +1608,14 @@ function form_set_error($name = NULL, $message = '', $limit_validation_errors =
} }
/** /**
* Clear all errors against all form elements made by form_set_error(). * Clears all errors against all form elements made by form_set_error().
*/ */
function form_clear_error() { function form_clear_error() {
drupal_static_reset('form_set_error'); drupal_static_reset('form_set_error');
} }
/** /**
* Return an associative array of all errors. * Returns an associative array of all errors.
*/ */
function form_get_errors() { function form_get_errors() {
$form = form_set_error(); $form = form_set_error();
...@@ -1481,16 +1643,18 @@ function form_get_error($element) { ...@@ -1481,16 +1643,18 @@ function form_get_error($element) {
} }
/** /**
* Flag an element as having an error. * Flags an element as having an error.
*/ */
function form_error(&$element, $message = '') { function form_error(&$element, $message = '') {
form_set_error(implode('][', $element['#parents']), $message); form_set_error(implode('][', $element['#parents']), $message);
} }
/** /**
* Walk through the structured form array, adding any required properties to * Builds and processes all elements in the structured form array.
* each element and mapping the incoming input data to the proper elements. *
* Also, execute any #process handlers attached to a specific element. * Adds any required properties to each element, maps the incoming input data
* to the proper elements, and executes any #process handlers attached to a
* specific element.
* *
* This is one of the three primary functions that recursively iterates a form * This is one of the three primary functions that recursively iterates a form
* array. This one does it for completing the form building process. The other * array. This one does it for completing the form building process. The other
...@@ -1559,7 +1723,7 @@ function form_error(&$element, $message = '') { ...@@ -1559,7 +1723,7 @@ function form_error(&$element, $message = '') {
* This is most commonly implemented with a submit handler setting persistent * This is most commonly implemented with a submit handler setting persistent
* data within $form_state based on *validated* values in * data within $form_state based on *validated* values in
* $form_state['values'] and setting $form_state['rebuild']. The form building * $form_state['values'] and setting $form_state['rebuild']. The form building
* functions must then be implmented to use the $form_state data to rebuild * functions must then be implemented to use the $form_state data to rebuild
* the form with the structure appropriate for the new state. * the form with the structure appropriate for the new state.
* - Where user input must affect the rendering of the form without affecting * - Where user input must affect the rendering of the form without affecting
* its structure, the necessary conditional rendering logic should reside * its structure, the necessary conditional rendering logic should reside
...@@ -1618,6 +1782,9 @@ function form_builder($form_id, &$element, &$form_state) { ...@@ -1618,6 +1782,9 @@ function form_builder($form_id, &$element, &$form_state) {
else { else {
$form_state['process_input'] = FALSE; $form_state['process_input'] = FALSE;
} }
// All form elements should have an #array_parents property.
$element['#array_parents'] = array();
} }
if (!isset($element['#id'])) { if (!isset($element['#id'])) {
...@@ -1674,7 +1841,7 @@ function form_builder($form_id, &$element, &$form_state) { ...@@ -1674,7 +1841,7 @@ function form_builder($form_id, &$element, &$form_state) {
$element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key); $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key);
} }
// Ensure #array_parents follows the actual form structure. // Ensure #array_parents follows the actual form structure.
$array_parents = isset($element['#array_parents']) ? $element['#array_parents'] : array(); $array_parents = $element['#array_parents'];
$array_parents[] = $key; $array_parents[] = $key;
$element[$key]['#array_parents'] = $array_parents; $element[$key]['#array_parents'] = $array_parents;
...@@ -1759,8 +1926,7 @@ function form_builder($form_id, &$element, &$form_state) { ...@@ -1759,8 +1926,7 @@ function form_builder($form_id, &$element, &$form_state) {
} }
/** /**
* Populate the #value and #name properties of input elements so they * Adds the #name and #value properties of an input element before rendering.
* can be processed and rendered.
*/ */
function _form_builder_handle_input_element($form_id, &$element, &$form_state) { function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
if (!isset($element['#name'])) { if (!isset($element['#name'])) {
...@@ -1811,7 +1977,7 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { ...@@ -1811,7 +1977,7 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
// drupal_form_submit() must not be able to get around this. Forms that set // drupal_form_submit() must not be able to get around this. Forms that set
// #access=FALSE on an element usually allow access for some users, so forms // #access=FALSE on an element usually allow access for some users, so forms
// submitted with drupal_form_submit() may bypass access restriction and be // submitted with drupal_form_submit() may bypass access restriction and be
// treated as high-privelege users instead. // treated as high-privilege users instead.
$process_input = empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access']))); $process_input = empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])));
// Set the element's #value property. // Set the element's #value property.
...@@ -1872,12 +2038,12 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { ...@@ -1872,12 +2038,12 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
// form_state_values_clean(). Enforce the same input processing restrictions // form_state_values_clean(). Enforce the same input processing restrictions
// as above. // as above.
if ($process_input) { if ($process_input) {
// Detect if the element triggered the submission via AJAX. // Detect if the element triggered the submission via Ajax.
if (_form_element_triggered_scripted_submission($element, $form_state)) { if (_form_element_triggered_scripted_submission($element, $form_state)) {
$form_state['triggering_element'] = $element; $form_state['triggering_element'] = $element;
} }
// If the form was submitted by the browser rather than via AJAX, then it // If the form was submitted by the browser rather than via Ajax, then it
// can only have been triggered by a button, and we need to determine which // can only have been triggered by a button, and we need to determine which
// button within the constraints of how browsers provide this information. // button within the constraints of how browsers provide this information.
if (isset($element['#button_type'])) { if (isset($element['#button_type'])) {
...@@ -1899,10 +2065,10 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { ...@@ -1899,10 +2065,10 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
} }
/** /**
* Helper function to handle the convoluted logic of button click detection. * Detects if an element triggered the form submission via Ajax.
* *
* This detects button or non-button controls that trigger a form submission via * This detects button or non-button controls that trigger a form submission via
* AJAX or some other scriptable environment. These environments can set the * Ajax or some other scriptable environment. These environments can set the
* special input key '_triggering_element_name' to identify the triggering * special input key '_triggering_element_name' to identify the triggering
* element. If the name alone doesn't identify the element uniquely, the input * element. If the name alone doesn't identify the element uniquely, the input
* key '_triggering_element_value' may also be set to require a match on element * key '_triggering_element_value' may also be set to require a match on element
...@@ -1919,7 +2085,7 @@ function _form_element_triggered_scripted_submission($element, &$form_state) { ...@@ -1919,7 +2085,7 @@ function _form_element_triggered_scripted_submission($element, &$form_state) {
} }
/** /**
* Helper function to handle the convoluted logic of button click detection. * Determines if a given button triggered the form submission.
* *
* This detects button controls that trigger a form submission by being clicked * This detects button controls that trigger a form submission by being clicked
* and having the click processed by the browser rather than being captured by * and having the click processed by the browser rather than being captured by
...@@ -1927,7 +2093,7 @@ function _form_element_triggered_scripted_submission($element, &$form_state) { ...@@ -1927,7 +2093,7 @@ function _form_element_triggered_scripted_submission($element, &$form_state) {
* of the POST data, but with extra code to deal with the convoluted way in * of the POST data, but with extra code to deal with the convoluted way in
* which browsers submit data for image button clicks. * which browsers submit data for image button clicks.
* *
* This does not detect button clicks processed by AJAX (that is done in * This does not detect button clicks processed by Ajax (that is done in
* _form_element_triggered_scripted_submission()) and it does not detect form * _form_element_triggered_scripted_submission()) and it does not detect form
* submissions from Internet Explorer in response to an ENTER key pressed in a * submissions from Internet Explorer in response to an ENTER key pressed in a
* textfield (form_builder() has extra code for that). * textfield (form_builder() has extra code for that).
...@@ -1972,7 +2138,7 @@ function _form_button_was_clicked($element, &$form_state) { ...@@ -1972,7 +2138,7 @@ function _form_button_was_clicked($element, &$form_state) {
* - form_build_id * - form_build_id
* - op * - op
* *
* @param &$form_state * @param $form_state
* A keyed array containing the current state of the form, including * A keyed array containing the current state of the form, including
* submitted form values; altered by reference. * submitted form values; altered by reference.
*/ */
...@@ -2004,17 +2170,17 @@ function form_state_values_clean(&$form_state) { ...@@ -2004,17 +2170,17 @@ function form_state_values_clean(&$form_state) {
// $form_state['values']['foo']['bar'], which is the level where we can // $form_state['values']['foo']['bar'], which is the level where we can
// unset 'baz' (that is stored in $last_parent). // unset 'baz' (that is stored in $last_parent).
$parents = $button['#parents']; $parents = $button['#parents'];
$values = &$form_state['values'];
$last_parent = array_pop($parents); $last_parent = array_pop($parents);
foreach ($parents as $parent) { $key_exists = NULL;
$values = &$values[$parent]; $values = &drupal_array_get_nested_value($form_state['values'], $parents, $key_exists);
} if ($key_exists && is_array($values)) {
unset($values[$last_parent]); unset($values[$last_parent]);
} }
} }
}
/** /**
* Helper function to determine the value for an image button form element. * Determines the value for an image button form element.
* *
* @param $form * @param $form
* The form element whose value is being populated. * The form element whose value is being populated.
...@@ -2023,6 +2189,7 @@ function form_state_values_clean(&$form_state) { ...@@ -2023,6 +2189,7 @@ function form_state_values_clean(&$form_state) {
* the element's default value should be returned. * the element's default value should be returned.
* @param $form_state * @param $form_state
* A keyed array containing the current state of the form. * A keyed array containing the current state of the form.
*
* @return * @return
* The data that will appear in the $form_state['values'] collection * The data that will appear in the $form_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
...@@ -2061,35 +2228,55 @@ function form_type_image_button_value($form, $input, $form_state) { ...@@ -2061,35 +2228,55 @@ function form_type_image_button_value($form, $input, $form_state) {
} }
/** /**
* Helper function to determine the value for a checkbox form element. * Determines the value for a checkbox form element.
* *
* @param $form * @param $form
* The form element whose value is being populated. * The form element whose value is being populated.
* @param $input * @param $input
* The incoming input to populate the form element. If this is FALSE, * The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned. * the element's default value should be returned.
*
* @return * @return
* The data that will appear in the $element_state['values'] collection * The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
*/ */
function form_type_checkbox_value($element, $input = FALSE) { function form_type_checkbox_value($element, $input = FALSE) {
if ($input !== FALSE) { if ($input === FALSE) {
// Successful (checked) checkboxes are present with a value (possibly '0'). // Use #default_value as the default value of a checkbox, except change
// http://www.w3.org/TR/html401/interact/forms.html#successful-controls // NULL to 0, because _form_builder_handle_input_element() would otherwise
// For an unchecked checkbox, we return integer 0, so we can explicitly // replace NULL with empty string, but an empty string is a potentially
// test for a value different than string '0'. // valid value for a checked checkbox.
return isset($element['#default_value']) ? $element['#default_value'] : 0;
}
else {
// Checked checkboxes are submitted with a value (possibly '0' or ''):
// http://www.w3.org/TR/html401/interact/forms.html#successful-controls.
// For checked checkboxes, browsers submit the string version of
// #return_value, but we return the original #return_value. For unchecked
// checkboxes, browsers submit nothing at all, but
// _form_builder_handle_input_element() detects this, and calls this
// function with $input=NULL. Returning NULL from a value callback means to
// use the default value, which is not what is wanted when an unchecked
// checkbox is submitted, so we use integer 0 as the value indicating an
// unchecked checkbox. Therefore, modules must not use integer 0 as a
// #return_value, as doing so results in the checkbox always being treated
// as unchecked. The string '0' is allowed for #return_value. The most
// common use-case for setting #return_value to either 0 or '0' is for the
// first option within a 0-indexed array of checkboxes, and for this,
// form_process_checkboxes() uses the string rather than the integer.
return isset($input) ? $element['#return_value'] : 0; return isset($input) ? $element['#return_value'] : 0;
} }
} }
/** /**
* Helper function to determine the value for a checkboxes form element. * Determines the value for a checkboxes form element.
* *
* @param $element * @param $element
* The form element whose value is being populated. * The form element whose value is being populated.
* @param $input * @param $input
* The incoming input to populate the form element. If this is FALSE, * The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned. * the element's default value should be returned.
*
* @return * @return
* The data that will appear in the $element_state['values'] collection * The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
...@@ -2109,7 +2296,7 @@ function form_type_checkboxes_value($element, $input = FALSE) { ...@@ -2109,7 +2296,7 @@ function form_type_checkboxes_value($element, $input = FALSE) {
// NULL elements from the array before constructing the return value, to // NULL elements from the array before constructing the return value, to
// simulate the behavior of web browsers (which do not send unchecked // simulate the behavior of web browsers (which do not send unchecked
// checkboxes to the server at all). This will not affect non-programmatic // checkboxes to the server at all). This will not affect non-programmatic
// form submissions, since a checkbox can never legitimately be NULL. // form submissions, since all values in $_POST are strings.
foreach ($input as $key => $value) { foreach ($input as $key => $value) {
if (!isset($value)) { if (!isset($value)) {
unset($input[$key]); unset($input[$key]);
...@@ -2123,13 +2310,14 @@ function form_type_checkboxes_value($element, $input = FALSE) { ...@@ -2123,13 +2310,14 @@ function form_type_checkboxes_value($element, $input = FALSE) {
} }
/** /**
* Helper function to determine the value for a tableselect form element. * Determines the value for a tableselect form element.
* *
* @param $element * @param $element
* The form element whose value is being populated. * The form element whose value is being populated.
* @param $input * @param $input
* The incoming input to populate the form element. If this is FALSE, * The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned. * the element's default value should be returned.
*
* @return * @return
* The data that will appear in the $element_state['values'] collection * The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
...@@ -2158,14 +2346,53 @@ function form_type_tableselect_value($element, $input = FALSE) { ...@@ -2158,14 +2346,53 @@ function form_type_tableselect_value($element, $input = FALSE) {
} }
/** /**
* Helper function to determine the value for a password_confirm form * Form value callback: Determines the value for a #type radios form element.
* element. *
* @param $element
* The form element whose value is being populated.
* @param $input
* (optional) The incoming input to populate the form element. If FALSE, the
* element's default value is returned. Defaults to FALSE.
*
* @return
* The data that will appear in the $element_state['values'] collection for
* this element.
*/
function form_type_radios_value(&$element, $input = FALSE) {
if ($input !== FALSE) {
// When there's user input (including NULL), return it as the value.
// However, if NULL is submitted, _form_builder_handle_input_element() will
// apply the default value, and we want that validated against #options
// unless it's empty. (An empty #default_value, such as NULL or FALSE, can
// be used to indicate that no radio button is selected by default.)
if (!isset($input) && !empty($element['#default_value'])) {
$element['#needs_validation'] = TRUE;
}
return $input;
}
else {
// For default value handling, simply return #default_value. Additionally,
// for a NULL default value, set #has_garbage_value to prevent
// _form_builder_handle_input_element() converting the NULL to an empty
// string, so that code can distinguish between nothing selected and the
// selection of a radio button whose value is an empty string.
$value = isset($element['#default_value']) ? $element['#default_value'] : NULL;
if (!isset($value)) {
$element['#has_garbage_value'] = TRUE;
}
return $value;
}
}
/**
* Determines the value for a password_confirm form element.
* *
* @param $element * @param $element
* The form element whose value is being populated. * The form element whose value is being populated.
* @param $input * @param $input
* The incoming input to populate the form element. If this is FALSE, * The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned. * the element's default value should be returned.
*
* @return * @return
* The data that will appear in the $element_state['values'] collection * The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
...@@ -2178,13 +2405,14 @@ function form_type_password_confirm_value($element, $input = FALSE) { ...@@ -2178,13 +2405,14 @@ function form_type_password_confirm_value($element, $input = FALSE) {
} }
/** /**
* Helper function to determine the value for a select form element. * Determines the value for a select form element.
* *
* @param $element * @param $element
* The form element whose value is being populated. * The form element whose value is being populated.
* @param $input * @param $input
* The incoming input to populate the form element. If this is FALSE, * The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned. * the element's default value should be returned.
*
* @return * @return
* The data that will appear in the $element_state['values'] collection * The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
...@@ -2218,13 +2446,14 @@ function form_type_select_value($element, $input = FALSE) { ...@@ -2218,13 +2446,14 @@ function form_type_select_value($element, $input = FALSE) {
} }
/** /**
* Helper function to determine the value for a textfield form element. * Determines the value for a textfield form element.
* *
* @param $element * @param $element
* The form element whose value is being populated. * The form element whose value is being populated.
* @param $input * @param $input
* The incoming input to populate the form element. If this is FALSE, * The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned. * the element's default value should be returned.
*
* @return * @return
* The data that will appear in the $element_state['values'] collection * The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
...@@ -2238,13 +2467,14 @@ function form_type_textfield_value($element, $input = FALSE) { ...@@ -2238,13 +2467,14 @@ function form_type_textfield_value($element, $input = FALSE) {
} }
/** /**
* Helper function to determine the value for form's token value. * Determines the value for form's token value.
* *
* @param $element * @param $element
* The form element whose value is being populated. * The form element whose value is being populated.
* @param $input * @param $input
* The incoming input to populate the form element. If this is FALSE, * The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned. * the element's default value should be returned.
*
* @return * @return
* The data that will appear in the $element_state['values'] collection * The data that will appear in the $element_state['values'] collection
* for this element. Return nothing to use the default. * for this element. Return nothing to use the default.
...@@ -2256,11 +2486,13 @@ function form_type_token_value($element, $input = FALSE) { ...@@ -2256,11 +2486,13 @@ function form_type_token_value($element, $input = FALSE) {
} }
/** /**
* Change submitted form values during form validation. * Changes submitted form values during form validation.
* *
* Use this function to change the submitted value of a form element in a form * Use this function to change the submitted value of a form element in a form
* validation function, so that the changed value persists in $form_state * validation function, so that the changed value persists in $form_state
* through to the submission handlers. * through the remaining validation and submission handlers. It does not change
* the value in $element['#value'], only in $form_state['values'], which is
* where submitted values are always stored.
* *
* Note that form validation functions are specified in the '#validate' * Note that form validation functions are specified in the '#validate'
* component of the form array (the value of $form['#validate'] is an array of * component of the form array (the value of $form['#validate'] is an array of
...@@ -2283,7 +2515,7 @@ function form_type_token_value($element, $input = FALSE) { ...@@ -2283,7 +2515,7 @@ function form_type_token_value($element, $input = FALSE) {
* Form state array where the value change should be recorded. * Form state array where the value change should be recorded.
*/ */
function form_set_value($element, $value, &$form_state) { function form_set_value($element, $value, &$form_state) {
drupal_array_set_nested_value($form_state['values'], $element['#parents'], $value); drupal_array_set_nested_value($form_state['values'], $element['#parents'], $value, TRUE);
} }
/** /**
...@@ -2306,11 +2538,9 @@ function form_options_flatten($array) { ...@@ -2306,11 +2538,9 @@ function form_options_flatten($array) {
} }
/** /**
* Helper function for form_options_flatten(). * Iterates over an array and returns a flat array with duplicate keys removed.
* *
* Iterates over arrays which may share common values and produces a flat * This function also handles cases where objects are passed as array values.
* array that has removed duplicate keys. Also handles cases where objects
* are passed as array values.
*/ */
function _form_options_flatten($array) { function _form_options_flatten($array) {
$return = &drupal_static(__FUNCTION__); $return = &drupal_static(__FUNCTION__);
...@@ -2420,7 +2650,7 @@ function theme_select($variables) { ...@@ -2420,7 +2650,7 @@ function theme_select($variables) {
} }
/** /**
* Converts a select form element's options array into an HTML. * Converts a select form element's options array into HTML.
* *
* @param $element * @param $element
* An associative array containing the properties of the element. * An associative array containing the properties of the element.
...@@ -2428,6 +2658,7 @@ function theme_select($variables) { ...@@ -2428,6 +2658,7 @@ function theme_select($variables) {
* Mixed: Either an associative array of items to list as choices, or an * Mixed: Either an associative array of items to list as choices, or an
* object with an 'option' member that is an associative array. This * object with an 'option' member that is an associative array. This
* parameter is only used internally and should not be passed. * parameter is only used internally and should not be passed.
*
* @return * @return
* An HTML string of options for the select form element. * An HTML string of options for the select form element.
*/ */
...@@ -2464,8 +2695,7 @@ function form_select_options($element, $choices = NULL) { ...@@ -2464,8 +2695,7 @@ function form_select_options($element, $choices = NULL) {
} }
/** /**
* Traverses a select element's #option array looking for any values * Returns the indexes of a select element's options matching a given key.
* that hold the given key. Returns an array of indexes that match.
* *
* This function is useful if you need to modify the options that are * This function is useful if you need to modify the options that are
* already in a form element; for example, to remove choices which are * already in a form element; for example, to remove choices which are
...@@ -2491,6 +2721,7 @@ function form_select_options($element, $choices = NULL) { ...@@ -2491,6 +2721,7 @@ function form_select_options($element, $choices = NULL) {
* The select element to search. * The select element to search.
* @param $key * @param $key
* The key to look for. * The key to look for.
*
* @return * @return
* An array of indexes that match the given $key. Array will be * An array of indexes that match the given $key. Array will be
* empty if no elements were found. FALSE if optgroups were found. * empty if no elements were found. FALSE if optgroups were found.
...@@ -2550,6 +2781,9 @@ function theme_fieldset($variables) { ...@@ -2550,6 +2781,9 @@ function theme_fieldset($variables) {
/** /**
* Returns HTML for a radio button form element. * Returns HTML for a radio button form element.
* *
* Note: The input "name" attribute needs to be sanitized before output, which
* is currently done by passing all attributes to drupal_attributes().
*
* @param $variables * @param $variables
* An associative array containing: * An associative array containing:
* - element: An associative array containing the properties of the element. * - element: An associative array containing the properties of the element.
...@@ -2563,7 +2797,7 @@ function theme_radio($variables) { ...@@ -2563,7 +2797,7 @@ function theme_radio($variables) {
$element['#attributes']['type'] = 'radio'; $element['#attributes']['type'] = 'radio';
element_set_attributes($element, array('id', 'name', '#return_value' => 'value')); element_set_attributes($element, array('id', 'name', '#return_value' => 'value'));
if (isset($element['#return_value']) && check_plain($element['#value']) == $element['#return_value']) { if (isset($element['#return_value']) && $element['#value'] !== FALSE && $element['#value'] == $element['#return_value']) {
$element['#attributes']['checked'] = 'checked'; $element['#attributes']['checked'] = 'checked';
} }
_form_set_class($element, array('form-radio')); _form_set_class($element, array('form-radio'));
...@@ -2592,6 +2826,9 @@ function theme_radios($variables) { ...@@ -2592,6 +2826,9 @@ function theme_radios($variables) {
if (!empty($element['#attributes']['class'])) { if (!empty($element['#attributes']['class'])) {
$attributes['class'] .= ' ' . implode(' ', $element['#attributes']['class']); $attributes['class'] .= ' ' . implode(' ', $element['#attributes']['class']);
} }
if (isset($element['#attributes']['title'])) {
$attributes['title'] = $element['#attributes']['title'];
}
return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>'; return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
} }
...@@ -2624,7 +2861,7 @@ function form_process_password_confirm($element) { ...@@ -2624,7 +2861,7 @@ function form_process_password_confirm($element) {
} }
/** /**
* Validate password_confirm element. * Validates a password_confirm element.
*/ */
function password_confirm_validate($element, &$element_state) { function password_confirm_validate($element, &$element_state) {
$pass1 = trim($element['pass1']['#value']); $pass1 = trim($element['pass1']['#value']);
...@@ -2661,11 +2898,21 @@ function password_confirm_validate($element, &$element_state) { ...@@ -2661,11 +2898,21 @@ function password_confirm_validate($element, &$element_state) {
*/ */
function theme_date($variables) { function theme_date($variables) {
$element = $variables['element']; $element = $variables['element'];
return '<div class="container-inline">' . drupal_render_children($element) . '</div>';
$attributes = array();
if (isset($element['#id'])) {
$attributes['id'] = $element['#id'];
}
if (!empty($element['#attributes']['class'])) {
$attributes['class'] = (array) $element['#attributes']['class'];
}
$attributes['class'][] = 'container-inline';
return '<div' . drupal_attributes($attributes) . '>' . drupal_render_children($element) . '</div>';
} }
/** /**
* Roll out a single date element. * Expands a date element into year, month, and day select elements.
*/ */
function form_process_date($element) { function form_process_date($element) {
// Default to current date // Default to current date
...@@ -2721,11 +2968,11 @@ function form_process_date($element) { ...@@ -2721,11 +2968,11 @@ function form_process_date($element) {
} }
/** /**
* Validates the date type to stop dates like February 30, 2006. * Validates the date type to prevent invalid dates (e.g., February 30, 2006).
*/ */
function date_validate($form) { function date_validate($element) {
if (!checkdate($form['#value']['month'], $form['#value']['day'], $form['#value']['year'])) { if (!checkdate($element['#value']['month'], $element['#value']['day'], $element['#value']['year'])) {
form_error($form, t('The specified date is invalid.')); form_error($element, t('The specified date is invalid.'));
} }
} }
...@@ -2751,7 +2998,7 @@ function map_month($month) { ...@@ -2751,7 +2998,7 @@ function map_month($month) {
} }
/** /**
* If no default value is set for weight select boxes, use 0. * Sets the value for a weight element, with zero as a default.
*/ */
function weight_value(&$form) { function weight_value(&$form) {
if (isset($form['#default_value'])) { if (isset($form['#default_value'])) {
...@@ -2763,29 +3010,38 @@ function weight_value(&$form) { ...@@ -2763,29 +3010,38 @@ function weight_value(&$form) {
} }
/** /**
* Roll out a single radios element to a list of radios, * Expands a radios element into individual radio elements.
* using the options array as index.
*/ */
function form_process_radios($element) { function form_process_radios($element) {
if (count($element['#options']) > 0) { if (count($element['#options']) > 0) {
$weight = 0;
foreach ($element['#options'] as $key => $choice) { foreach ($element['#options'] as $key => $choice) {
if (!isset($element[$key])) { // Maintain order of options as defined in #options, in case the element
// defines custom option sub-elements, but does not define all option
// sub-elements.
$weight += 0.001;
$element += array($key => array());
// Generate the parents as the autogenerator does, so we will have a // Generate the parents as the autogenerator does, so we will have a
// unique id for each radio button. // unique id for each radio button.
$parents_for_id = array_merge($element['#parents'], array($key)); $parents_for_id = array_merge($element['#parents'], array($key));
$element[$key] = array( $element[$key] += array(
'#type' => 'radio', '#type' => 'radio',
'#title' => $choice, '#title' => $choice,
'#return_value' => check_plain($key), // The key is sanitized in drupal_attributes() during output from the
'#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL, // theme function.
'#return_value' => $key,
// Use default or FALSE. A value of FALSE means that the radio button is
// not 'checked'.
'#default_value' => isset($element['#default_value']) ? $element['#default_value'] : FALSE,
'#attributes' => $element['#attributes'], '#attributes' => $element['#attributes'],
'#parents' => $element['#parents'], '#parents' => $element['#parents'],
'#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
'#weight' => $weight,
); );
} }
} }
}
return $element; return $element;
} }
...@@ -2796,18 +3052,17 @@ function form_process_radios($element) { ...@@ -2796,18 +3052,17 @@ function form_process_radios($element) {
* An associative array containing: * An associative array containing:
* - element: An associative array containing the properties of the element. * - element: An associative array containing the properties of the element.
* Properties used: #title, #value, #return_value, #description, #required, * Properties used: #title, #value, #return_value, #description, #required,
* #attributes. * #attributes, #checked.
* *
* @ingroup themeable * @ingroup themeable
*/ */
function theme_checkbox($variables) { function theme_checkbox($variables) {
$element = $variables['element']; $element = $variables['element'];
$t = get_t();
$element['#attributes']['type'] = 'checkbox'; $element['#attributes']['type'] = 'checkbox';
element_set_attributes($element, array('id', 'name', '#return_value' => 'value')); element_set_attributes($element, array('id', 'name', '#return_value' => 'value'));
// Unchecked checkbox has #value of integer 0. // Unchecked checkbox has #value of integer 0.
if (isset($element['#return_value']) && isset($element['#value']) && $element['#value'] !== 0 && $element['#value'] == $element['#return_value']) { if (!empty($element['#checked'])) {
$element['#attributes']['checked'] = 'checked'; $element['#attributes']['checked'] = 'checked';
} }
_form_set_class($element, array('form-checkbox')); _form_set_class($element, array('form-checkbox'));
...@@ -2835,15 +3090,19 @@ function theme_checkboxes($variables) { ...@@ -2835,15 +3090,19 @@ function theme_checkboxes($variables) {
if (!empty($element['#attributes']['class'])) { if (!empty($element['#attributes']['class'])) {
$attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']); $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']);
} }
if (isset($element['#attributes']['title'])) {
$attributes['title'] = $element['#attributes']['title'];
}
return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>'; return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
} }
/** /**
* Add form_element theming to an element if title or description is set. * Adds form element theming to an element if its title or description is set.
* *
* This is used as a pre render function for checkboxes and radios. * This is used as a pre render function for checkboxes and radios.
*/ */
function form_pre_render_conditional_form_element($element) { function form_pre_render_conditional_form_element($element) {
$t = get_t();
// Set the element's title attribute to show #title as a tooltip, if needed. // Set the element's title attribute to show #title as a tooltip, if needed.
if (isset($element['#title']) && $element['#title_display'] == 'attribute') { if (isset($element['#title']) && $element['#title_display'] == 'attribute') {
$element['#attributes']['title'] = $element['#title']; $element['#attributes']['title'] = $element['#title'];
...@@ -2859,6 +3118,36 @@ function form_pre_render_conditional_form_element($element) { ...@@ -2859,6 +3118,36 @@ function form_pre_render_conditional_form_element($element) {
return $element; return $element;
} }
/**
* Sets the #checked property of a checkbox element.
*/
function form_process_checkbox($element, $form_state) {
$value = $element['#value'];
$return_value = $element['#return_value'];
// On form submission, the #value of an available and enabled checked
// checkbox is #return_value, and the #value of an available and enabled
// unchecked checkbox is integer 0. On not submitted forms, and for
// checkboxes with #access=FALSE or #disabled=TRUE, the #value is
// #default_value (integer 0 if #default_value is NULL). Most of the time,
// a string comparison of #value and #return_value is sufficient for
// determining the "checked" state, but a value of TRUE always means checked
// (even if #return_value is 'foo'), and a value of FALSE or integer 0 always
// means unchecked (even if #return_value is '' or '0').
if ($value === TRUE || $value === FALSE || $value === 0) {
$element['#checked'] = (bool) $value;
}
else {
// Compare as strings, so that 15 is not considered equal to '15foo', but 1
// is considered equal to '1'. This cast does not imply that either #value
// or #return_value is expected to be a string.
$element['#checked'] = ((string) $value === (string) $return_value);
}
return $element;
}
/**
* Processes a checkboxes form element.
*/
function form_process_checkboxes($element) { function form_process_checkboxes($element) {
$value = is_array($element['#value']) ? $element['#value'] : array(); $value = is_array($element['#value']) ? $element['#value'] : array();
$element['#tree'] = TRUE; $element['#tree'] = TRUE;
...@@ -2866,20 +3155,32 @@ function form_process_checkboxes($element) { ...@@ -2866,20 +3155,32 @@ function form_process_checkboxes($element) {
if (!isset($element['#default_value']) || $element['#default_value'] == 0) { if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
$element['#default_value'] = array(); $element['#default_value'] = array();
} }
$weight = 0;
foreach ($element['#options'] as $key => $choice) { foreach ($element['#options'] as $key => $choice) {
if (!isset($element[$key])) { // Integer 0 is not a valid #return_value, so use '0' instead.
$element[$key] = array( // @see form_type_checkbox_value().
// @todo For Drupal 8, cast all integer keys to strings for consistency
// with form_process_radios().
if ($key === 0) {
$key = '0';
}
// Maintain order of options as defined in #options, in case the element
// defines custom option sub-elements, but does not define all option
// sub-elements.
$weight += 0.001;
$element += array($key => array());
$element[$key] += array(
'#type' => 'checkbox', '#type' => 'checkbox',
'#processed' => TRUE,
'#title' => $choice, '#title' => $choice,
'#return_value' => $key, '#return_value' => $key,
'#default_value' => isset($value[$key]) ? $key : NULL, '#default_value' => isset($value[$key]) ? $key : NULL,
'#attributes' => $element['#attributes'], '#attributes' => $element['#attributes'],
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
'#weight' => $weight,
); );
} }
} }
}
return $element; return $element;
} }
...@@ -2908,6 +3209,7 @@ function form_process_actions($element, &$form_state) { ...@@ -2908,6 +3209,7 @@ function form_process_actions($element, &$form_state) {
* container. * container.
* @param $form_state * @param $form_state
* The $form_state array for the form this element belongs to. * The $form_state array for the form this element belongs to.
*
* @return * @return
* The processed element. * The processed element.
*/ */
...@@ -2920,7 +3222,11 @@ function form_process_container($element, &$form_state) { ...@@ -2920,7 +3222,11 @@ function form_process_container($element, &$form_state) {
} }
/** /**
* Returns HTML for a container for grouped form items. * Returns HTML to wrap child elements in a container.
*
* Used for grouped form items. Can also be used as a #theme_wrapper for any
* renderable element, to surround it with a <div> and add attributes such as
* classes or an HTML id.
* *
* @param $variables * @param $variables
* An associative array containing: * An associative array containing:
...@@ -2931,32 +3237,23 @@ function form_process_container($element, &$form_state) { ...@@ -2931,32 +3237,23 @@ function form_process_container($element, &$form_state) {
*/ */
function theme_container($variables) { function theme_container($variables) {
$element = $variables['element']; $element = $variables['element'];
// Special handling for form elements.
if (isset($element['#array_parents'])) {
// Assign an html ID.
if (!isset($element['#attributes']['id'])) { if (!isset($element['#attributes']['id'])) {
$element['#attributes']['id'] = $element['#id']; $element['#attributes']['id'] = $element['#id'];
} }
// Force the 'form-wrapper' class. // Add the 'form-wrapper' class.
$element['#attributes']['class'][] = 'form-wrapper'; $element['#attributes']['class'][] = 'form-wrapper';
}
return '<div' . drupal_attributes($element['#attributes']) . '>' . $element['#children'] . '</div>'; return '<div' . drupal_attributes($element['#attributes']) . '>' . $element['#children'] . '</div>';
} }
/** /**
* Returns HTML for a table with radio buttons or checkboxes. * Returns HTML for a table with radio buttons or checkboxes.
* *
* An example of per-row options:
* @code
* $options = array();
* $options[0]['title'] = "A red row"
* $options[0]['#attributes'] = array ('class' => array('red-row'));
* $options[1]['title'] = "A blue row"
* $options[1]['#attributes'] = array ('class' => array('blue-row'));
*
* $form['myselector'] = array (
* '#type' => 'tableselect',
* '#title' => 'My Selector'
* '#options' => $options,
* );
* @endcode
*
* @param $variables * @param $variables
* An associative array containing: * An associative array containing:
* - element: An associative array containing the properties and children of * - element: An associative array containing the properties and children of
...@@ -2964,7 +3261,35 @@ function theme_container($variables) { ...@@ -2964,7 +3261,35 @@ function theme_container($variables) {
* and #js_select. The #options property is an array of selection options; * and #js_select. The #options property is an array of selection options;
* each array element of #options is an array of properties. These * each array element of #options is an array of properties. These
* properties can include #attributes, which is added to the * properties can include #attributes, which is added to the
* table row's HTML attributes; see theme_table(). * table row's HTML attributes; see theme_table(). An example of per-row
* options:
* @code
* $options = array(
* array(
* 'title' => 'How to Learn Drupal',
* 'content_type' => 'Article',
* 'status' => 'published',
* '#attributes' => array('class' => array('article-row')),
* ),
* array(
* 'title' => 'Privacy Policy',
* 'content_type' => 'Page',
* 'status' => 'published',
* '#attributes' => array('class' => array('page-row')),
* ),
* );
* $header = array(
* 'title' => t('Title'),
* 'content_type' => t('Content type'),
* 'status' => t('Status'),
* );
* $form['table'] = array(
* '#type' => 'tableselect',
* '#header' => $header,
* '#options' => $options,
* '#empty' => t('No content available.'),
* );
* @endcode
* *
* @ingroup themeable * @ingroup themeable
*/ */
...@@ -3008,11 +3333,12 @@ function theme_tableselect($variables) { ...@@ -3008,11 +3333,12 @@ function theme_tableselect($variables) {
} }
/** /**
* Create the correct amount of checkbox or radio elements to populate the table. * Creates checkbox or radio elements to populate a tableselect table.
* *
* @param $element * @param $element
* An associative array containing the properties and children of the * An associative array containing the properties and children of the
* tableselect element. * tableselect element.
*
* @return * @return
* The processed element. * The processed element.
*/ */
...@@ -3022,7 +3348,7 @@ function form_process_tableselect($element) { ...@@ -3022,7 +3348,7 @@ function form_process_tableselect($element) {
$value = is_array($element['#value']) ? $element['#value'] : array(); $value = is_array($element['#value']) ? $element['#value'] : array();
} }
else { else {
// Advanced selection behaviour make no sense for radios. // Advanced selection behavior makes no sense for radios.
$element['#js_select'] = FALSE; $element['#js_select'] = FALSE;
} }
...@@ -3106,6 +3432,9 @@ function form_process_tableselect($element) { ...@@ -3106,6 +3432,9 @@ function form_process_tableselect($element) {
* different character, 'replace_pattern' needs to be set accordingly. * different character, 'replace_pattern' needs to be set accordingly.
* - error: (optional) A custom form error message string to show, if the * - error: (optional) A custom form error message string to show, if the
* machine name contains disallowed characters. * machine name contains disallowed characters.
* - standalone: (optional) Whether the live preview should stay in its own
* form element rather than in the suffix of the source element. Defaults
* to FALSE.
* - #maxlength: (optional) Should be set to the maximum allowed length of the * - #maxlength: (optional) Should be set to the maximum allowed length of the
* machine name. Defaults to 64. * machine name. Defaults to 64.
* - #disabled: (optional) Should be set to TRUE in case an existing machine * - #disabled: (optional) Should be set to TRUE in case an existing machine
...@@ -3117,6 +3446,9 @@ function form_process_machine_name($element, &$form_state) { ...@@ -3117,6 +3446,9 @@ function form_process_machine_name($element, &$form_state) {
'#title' => t('Machine-readable name'), '#title' => t('Machine-readable name'),
'#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'), '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
'#machine_name' => array(), '#machine_name' => array(),
'#field_prefix' => '',
'#field_suffix' => '',
'#suffix' => '',
); );
// A form element that only wants to set one #machine_name property (usually // A form element that only wants to set one #machine_name property (usually
// 'source' only) would leave all other properties undefined, if the defaults // 'source' only) would leave all other properties undefined, if the defaults
...@@ -3127,15 +3459,25 @@ function form_process_machine_name($element, &$form_state) { ...@@ -3127,15 +3459,25 @@ function form_process_machine_name($element, &$form_state) {
'label' => t('Machine name'), 'label' => t('Machine name'),
'replace_pattern' => '[^a-z0-9_]+', 'replace_pattern' => '[^a-z0-9_]+',
'replace' => '_', 'replace' => '_',
'standalone' => FALSE,
'field_prefix' => $element['#field_prefix'],
'field_suffix' => $element['#field_suffix'],
); );
// By default, machine names are restricted to Latin alphanumeric characters.
// So, default to LTR directionality.
if (!isset($element['#attributes'])) {
$element['#attributes'] = array();
}
$element['#attributes'] += array('dir' => 'ltr');
// The source element defaults to array('name'), but may have been overidden. // The source element defaults to array('name'), but may have been overidden.
if (empty($element['#machine_name']['source'])) { if (empty($element['#machine_name']['source'])) {
return $element; return $element;
} }
// Retrieve the form element containing the human-readable name from the // Retrieve the form element containing the human-readable name from the
// complete form in $form_state. By reference, because we need to append // complete form in $form_state. By reference, because we may need to append
// a #field_suffix that will hold the live preview. // a #field_suffix that will hold the live preview.
$key_exists = NULL; $key_exists = NULL;
$source = drupal_array_get_nested_value($form_state['complete form'], $element['#machine_name']['source'], $key_exists); $source = drupal_array_get_nested_value($form_state['complete form'], $element['#machine_name']['source'], $key_exists);
...@@ -3143,16 +3485,21 @@ function form_process_machine_name($element, &$form_state) { ...@@ -3143,16 +3485,21 @@ function form_process_machine_name($element, &$form_state) {
return $element; return $element;
} }
$suffix_id = $source['#id'] . '-machine-name-suffix';
$element['#machine_name']['suffix'] = '#' . $suffix_id;
if ($element['#machine_name']['standalone']) {
$element['#suffix'] .= ' <small id="' . $suffix_id . '">&nbsp;</small>';
}
else {
// Append a field suffix to the source form element, which will contain // Append a field suffix to the source form element, which will contain
// the live preview of the machine name. // the live preview of the machine name.
$suffix_id = $source['#id'] . '-machine-name-suffix';
$source += array('#field_suffix' => ''); $source += array('#field_suffix' => '');
$source['#field_suffix'] .= ' <small id="' . $suffix_id . '">&nbsp;</small>'; $source['#field_suffix'] .= ' <small id="' . $suffix_id . '">&nbsp;</small>';
$parents = array_merge($element['#machine_name']['source'], array('#field_suffix')); $parents = array_merge($element['#machine_name']['source'], array('#field_suffix'));
drupal_array_set_nested_value($form_state['complete form'], $parents, $source['#field_suffix']); drupal_array_set_nested_value($form_state['complete form'], $parents, $source['#field_suffix']);
}
$element['#machine_name']['suffix'] = '#' . $suffix_id;
$js_settings = array( $js_settings = array(
'type' => 'setting', 'type' => 'setting',
...@@ -3169,7 +3516,7 @@ function form_process_machine_name($element, &$form_state) { ...@@ -3169,7 +3516,7 @@ function form_process_machine_name($element, &$form_state) {
} }
/** /**
* Form element validation handler for #type 'machine_name'. * Form element validation handler for machine_name elements.
* *
* Note that #maxlength is validated by _form_validate() already. * Note that #maxlength is validated by _form_validate() already.
*/ */
...@@ -3200,22 +3547,22 @@ function form_validate_machine_name(&$element, &$form_state) { ...@@ -3200,22 +3547,22 @@ function form_validate_machine_name(&$element, &$form_state) {
// Verify that the machine name is unique. // Verify that the machine name is unique.
if ($element['#default_value'] !== $element['#value']) { if ($element['#default_value'] !== $element['#value']) {
$function = $element['#machine_name']['exists']; $function = $element['#machine_name']['exists'];
if ($function($element['#value'])) { if ($function($element['#value'], $element, $form_state)) {
form_error($element, t('The machine-readable name is already in use. It must be unique.')); form_error($element, t('The machine-readable name is already in use. It must be unique.'));
} }
} }
} }
/** /**
* Adds fieldsets to the specified group or adds group members to this * Arranges fieldsets into groups.
* fieldset.
* *
* @param &$element * @param $element
* An associative array containing the properties and children of the * An associative array containing the properties and children of the
* fieldset. Note that $element must be taken by reference here, so processed * fieldset. Note that $element must be taken by reference here, so processed
* child elements are taken over into $form_state. * child elements are taken over into $form_state.
* @param $form_state * @param $form_state
* The $form_state array for the form this fieldset belongs to. * The $form_state array for the form this fieldset belongs to.
*
* @return * @return
* The processed element. * The processed element.
*/ */
...@@ -3235,7 +3582,7 @@ function form_process_fieldset(&$element, &$form_state) { ...@@ -3235,7 +3582,7 @@ function form_process_fieldset(&$element, &$form_state) {
} }
// Contains form element summary functionalities. // Contains form element summary functionalities.
$element['#attached']['js']['misc/form.js'] = array('group' => JS_LIBRARY, 'weight' => 1); $element['#attached']['library'][] = array('system', 'drupal.form');
// The .form-wrapper class is required for #states to treat fieldsets like // The .form-wrapper class is required for #states to treat fieldsets like
// containers. // containers.
...@@ -3245,7 +3592,7 @@ function form_process_fieldset(&$element, &$form_state) { ...@@ -3245,7 +3592,7 @@ function form_process_fieldset(&$element, &$form_state) {
// Collapsible fieldsets // Collapsible fieldsets
if (!empty($element['#collapsible'])) { if (!empty($element['#collapsible'])) {
$element['#attached']['js'][] = 'misc/collapse.js'; $element['#attached']['library'][] = array('system', 'drupal.collapse');
$element['#attributes']['class'][] = 'collapsible'; $element['#attributes']['class'][] = 'collapsible';
if (!empty($element['#collapsed'])) { if (!empty($element['#collapsed'])) {
$element['#attributes']['class'][] = 'collapsed'; $element['#attributes']['class'][] = 'collapsed';
...@@ -3319,6 +3666,7 @@ function form_pre_render_fieldset($element) { ...@@ -3319,6 +3666,7 @@ function form_pre_render_fieldset($element) {
* fieldset. * fieldset.
* @param $form_state * @param $form_state
* The $form_state array for the form this vertical tab widget belongs to. * The $form_state array for the form this vertical tab widget belongs to.
*
* @return * @return
* The processed element. * The processed element.
*/ */
...@@ -3353,15 +3701,15 @@ function form_process_vertical_tabs($element, &$form_state) { ...@@ -3353,15 +3701,15 @@ function form_process_vertical_tabs($element, &$form_state) {
* *
* @param $variables * @param $variables
* An associative array containing: * An associative array containing:
* - element: An associative array containing the properties and children of the * - element: An associative array containing the properties and children of
* fieldset. Properties used: #children. * the fieldset. Properties used: #children.
* *
* @ingroup themeable * @ingroup themeable
*/ */
function theme_vertical_tabs($variables) { function theme_vertical_tabs($variables) {
$element = $variables['element']; $element = $variables['element'];
// Add required JavaScript and Stylesheet. // Add required JavaScript and Stylesheet.
drupal_add_library('system', 'vertical-tabs'); drupal_add_library('system', 'drupal.vertical-tabs');
$output = '<h2 class="element-invisible">' . t('Vertical Tabs') . '</h2>'; $output = '<h2 class="element-invisible">' . t('Vertical Tabs') . '</h2>';
$output .= '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>'; $output .= '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
...@@ -3470,7 +3818,7 @@ function theme_textfield($variables) { ...@@ -3470,7 +3818,7 @@ function theme_textfield($variables) {
$extra = ''; $extra = '';
if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) { if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) {
drupal_add_js('misc/autocomplete.js'); drupal_add_library('system', 'drupal.autocomplete');
$element['#attributes']['class'][] = 'form-autocomplete'; $element['#attributes']['class'][] = 'form-autocomplete';
$attributes = array(); $attributes = array();
...@@ -3532,7 +3880,7 @@ function theme_textarea($variables) { ...@@ -3532,7 +3880,7 @@ function theme_textarea($variables) {
// Add resizable behavior. // Add resizable behavior.
if (!empty($element['#resizable'])) { if (!empty($element['#resizable'])) {
drupal_add_js('misc/textarea.js'); drupal_add_library('system', 'drupal.textarea');
$wrapper_attributes['class'][] = 'resizable'; $wrapper_attributes['class'][] = 'resizable';
} }
...@@ -3556,23 +3904,37 @@ function theme_textarea($variables) { ...@@ -3556,23 +3904,37 @@ function theme_textarea($variables) {
function theme_password($variables) { function theme_password($variables) {
$element = $variables['element']; $element = $variables['element'];
$element['#attributes']['type'] = 'password'; $element['#attributes']['type'] = 'password';
element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength')); element_set_attributes($element, array('id', 'name', 'size', 'maxlength'));
_form_set_class($element, array('form-text')); _form_set_class($element, array('form-text'));
return '<input' . drupal_attributes($element['#attributes']) . ' />'; return '<input' . drupal_attributes($element['#attributes']) . ' />';
} }
/** /**
* Expand weight elements into selects. * Expands a weight element into a select element.
*/ */
function form_process_weight($element) { function form_process_weight($element) {
$element['#is_weight'] = TRUE;
// If the number of options is small enough, use a select field.
$max_elements = variable_get('drupal_weight_select_max', DRUPAL_WEIGHT_SELECT_MAX);
if ($element['#delta'] <= $max_elements) {
$element['#type'] = 'select';
for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) {
$weights[$n] = $n; $weights[$n] = $n;
} }
$element['#options'] = $weights; $element['#options'] = $weights;
$element['#type'] = 'select';
$element['#is_weight'] = TRUE;
$element += element_info('select'); $element += element_info('select');
}
// Otherwise, use a text field.
else {
$element['#type'] = 'textfield';
// Use a field big enough to fit most weights.
$element['#size'] = 10;
$element['#element_validate'] = array('element_validate_integer');
$element += element_info('textfield');
}
return $element; return $element;
} }
...@@ -3647,8 +4009,6 @@ function theme_file($variables) { ...@@ -3647,8 +4009,6 @@ function theme_file($variables) {
*/ */
function theme_form_element($variables) { function theme_form_element($variables) {
$element = &$variables['element']; $element = &$variables['element'];
// This is also used in the installer, pre-database setup.
$t = get_t();
// This function is invoked as theme wrapper, but the rendered form element // This function is invoked as theme wrapper, but the rendered form element
// may not necessarily have been processed by form_builder(). // may not necessarily have been processed by form_builder().
...@@ -3733,7 +4093,8 @@ function theme_form_required_marker($variables) { ...@@ -3733,7 +4093,8 @@ function theme_form_required_marker($variables) {
* *
* Form element labels include the #title and a #required marker. The label is * Form element labels include the #title and a #required marker. The label is
* associated with the element itself by the element #id. Labels may appear * associated with the element itself by the element #id. Labels may appear
* before or after elements, depending on theme_form_element() and #title_display. * before or after elements, depending on theme_form_element() and
* #title_display.
* *
* This function will not be called for elements with no labels, depending on * This function will not be called for elements with no labels, depending on
* #title_display. For elements that have an empty #title and are not required, * #title_display. For elements that have an empty #title and are not required,
...@@ -3756,7 +4117,7 @@ function theme_form_element_label($variables) { ...@@ -3756,7 +4117,7 @@ function theme_form_element_label($variables) {
$t = get_t(); $t = get_t();
// If title and required marker are both empty, output no label. // If title and required marker are both empty, output no label.
if (empty($element['#title']) && empty($element['#required'])) { if ((!isset($element['#title']) || $element['#title'] === '') && empty($element['#required'])) {
return ''; return '';
} }
...@@ -3788,7 +4149,7 @@ function theme_form_element_label($variables) { ...@@ -3788,7 +4149,7 @@ function theme_form_element_label($variables) {
* *
* Adds 'required' and 'error' classes as needed. * Adds 'required' and 'error' classes as needed.
* *
* @param &$element * @param $element
* The form element. * The form element.
* @param $name * @param $name
* Array of new class names to be added. * Array of new class names to be added.
...@@ -3806,11 +4167,41 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3806,11 +4167,41 @@ function _form_set_class(&$element, $class = array()) {
if (!empty($element['#required'])) { if (!empty($element['#required'])) {
$element['#attributes']['class'][] = 'required'; $element['#attributes']['class'][] = 'required';
} }
if (isset($element['#parents']) && form_get_error($element)) { if (isset($element['#parents']) && form_get_error($element) !== NULL && !empty($element['#validated'])) {
$element['#attributes']['class'][] = 'error'; $element['#attributes']['class'][] = 'error';
} }
} }
/**
* Form element validation handler for integer elements.
*/
function element_validate_integer($element, &$form_state) {
$value = $element['#value'];
if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) {
form_error($element, t('%name must be an integer.', array('%name' => $element['#title'])));
}
}
/**
* Form element validation handler for integer elements that must be positive.
*/
function element_validate_integer_positive($element, &$form_state) {
$value = $element['#value'];
if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value <= 0)) {
form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title'])));
}
}
/**
* Form element validation handler for number elements.
*/
function element_validate_number($element, &$form_state) {
$value = $element['#value'];
if ($value != '' && !is_numeric($value)) {
form_error($element, t('%name must be a number.', array('%name' => $element['#title'])));
}
}
/** /**
* @} End of "defgroup form_api". * @} End of "defgroup form_api".
*/ */
...@@ -3818,6 +4209,8 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3818,6 +4209,8 @@ function _form_set_class(&$element, $class = array()) {
/** /**
* @defgroup batch Batch operations * @defgroup batch Batch operations
* @{ * @{
* Creates and processes batch operations.
*
* Functions allowing forms processing to be spread out over several page * Functions allowing forms processing to be spread out over several page
* requests, thus ensuring that the processing does not get interrupted * requests, thus ensuring that the processing does not get interrupted
* because of a PHP timeout, while allowing the user to receive feedback * because of a PHP timeout, while allowing the user to receive feedback
...@@ -3839,14 +4232,17 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3839,14 +4232,17 @@ function _form_set_class(&$element, $class = array()) {
* 'file' => 'path_to_file_containing_myfunctions', * 'file' => 'path_to_file_containing_myfunctions',
* ); * );
* batch_set($batch); * batch_set($batch);
* // only needed if not inside a form _submit handler : * // Only needed if not inside a form _submit handler.
* batch_process(); * // Setting redirect in batch_process.
* batch_process('node/1');
* @endcode * @endcode
* *
* Note: if the batch 'title', 'init_message', 'progress_message', or * Note: if the batch 'title', 'init_message', 'progress_message', or
* 'error_message' could contain any user input, it is the responsibility of * 'error_message' could contain any user input, it is the responsibility of
* the code calling batch_set() to sanitize them first with a function like * the code calling batch_set() to sanitize them first with a function like
* check_plain() or filter_xss(). * check_plain() or filter_xss(). Furthermore, if the batch operation
* returns any user input in the 'results' or 'message' keys of $context,
* it must also sanitize them first.
* *
* Sample batch operations: * Sample batch operations:
* @code * @code
...@@ -3869,8 +4265,8 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3869,8 +4265,8 @@ function _form_set_class(&$element, $class = array()) {
* // and the batch processing can continue to the next operation. * // and the batch processing can continue to the next operation.
* *
* $node = node_load(array('uid' => $uid, 'type' => $type)); * $node = node_load(array('uid' => $uid, 'type' => $type));
* $context['results'][] = $node->nid . ' : ' . $node->title; * $context['results'][] = $node->nid . ' : ' . check_plain($node->title);
* $context['message'] = $node->title; * $context['message'] = check_plain($node->title);
* } * }
* *
* // More advanced example: multi-step operation - load all nodes, five by five * // More advanced example: multi-step operation - load all nodes, five by five
...@@ -3889,10 +4285,10 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3889,10 +4285,10 @@ function _form_set_class(&$element, $class = array()) {
* ->execute(); * ->execute();
* foreach ($result as $row) { * foreach ($result as $row) {
* $node = node_load($row->nid, NULL, TRUE); * $node = node_load($row->nid, NULL, TRUE);
* $context['results'][] = $node->nid . ' : ' . $node->title; * $context['results'][] = $node->nid . ' : ' . check_plain($node->title);
* $context['sandbox']['progress']++; * $context['sandbox']['progress']++;
* $context['sandbox']['current_node'] = $node->nid; * $context['sandbox']['current_node'] = $node->nid;
* $context['message'] = $node->title; * $context['message'] = check_plain($node->title);
* } * }
* if ($context['sandbox']['progress'] != $context['sandbox']['max']) { * if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
* $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; * $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
...@@ -3903,6 +4299,8 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3903,6 +4299,8 @@ function _form_set_class(&$element, $class = array()) {
* Sample 'finished' callback: * Sample 'finished' callback:
* @code * @code
* function batch_test_finished($success, $results, $operations) { * function batch_test_finished($success, $results, $operations) {
* // The 'success' parameter means no fatal PHP errors were detected. All
* // other error management should be handled using 'results'.
* if ($success) { * if ($success) {
* $message = format_plural(count($results), 'One post processed.', '@count posts processed.'); * $message = format_plural(count($results), 'One post processed.', '@count posts processed.');
* } * }
...@@ -3920,13 +4318,24 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3920,13 +4318,24 @@ function _form_set_class(&$element, $class = array()) {
*/ */
/** /**
* Opens a new batch. * Adds a new batch.
* *
* @param $batch * Batch operations are added as new batch sets. Batch sets are used to spread
* An array defining the batch. The following keys can be used -- only * processing (primarily, but not exclusively, forms processing) over several
* 'operations' is required, and batch_init() provides default values for * page requests. This helps to ensure that the processing is not interrupted
* the messages. * due to PHP timeouts, while users are still able to receive feedback on the
* - 'operations': Array of function calls to be performed. * progress of the ongoing operations. Combining related operations into
* distinct batch sets provides clean code independence for each batch set,
* ensuring that two or more batches, submitted independently, can be processed
* without mutual interference. Each batch set may specify its own set of
* operations and results, produce its own UI messages, and trigger its own
* 'finished' callback. Batch sets are processed sequentially, with the progress
* bar starting afresh for each new set.
*
* @param $batch_definition
* An associative array defining the batch, with the following elements (all
* are optional except as noted):
* - operations: (required) Array of function calls to be performed.
* Example: * Example:
* @code * @code
* array( * array(
...@@ -3934,35 +4343,26 @@ function _form_set_class(&$element, $class = array()) { ...@@ -3934,35 +4343,26 @@ function _form_set_class(&$element, $class = array()) {
* array('my_function_2', array($arg2_1, $arg2_2)), * array('my_function_2', array($arg2_1, $arg2_2)),
* ) * )
* @endcode * @endcode
* - 'title': Title for the progress page. Only safe strings should be passed. * - title: A safe, translated string to use as the title for the progress
* Defaults to t('Processing'). * page. Defaults to t('Processing').
* - 'init_message': Message displayed while the processing is initialized. * - init_message: Message displayed while the processing is initialized.
* Defaults to t('Initializing.'). * Defaults to t('Initializing.').
* - 'progress_message': Message displayed while processing the batch. * - progress_message: Message displayed while processing the batch. Available
* Available placeholders are @current, @remaining, @total, @percentage, * placeholders are @current, @remaining, @total, @percentage, @estimate and
* @estimate and @elapsed. Defaults to t('Completed @current of @total.'). * @elapsed. Defaults to t('Completed @current of @total.').
* - 'error_message': Message displayed if an error occurred while processing * - error_message: Message displayed if an error occurred while processing
* the batch. Defaults to t('An error has occurred.'). * the batch. Defaults to t('An error has occurred.').
* - 'finished': Name of a function to be executed after the batch has * - finished: Name of a function to be executed after the batch has
* completed. This should be used to perform any result massaging that * completed. This should be used to perform any result massaging that may
* may be needed, and possibly save data in $_SESSION for display after * be needed, and possibly save data in $_SESSION for display after final
* final page redirection. * page redirection.
* - 'file': Path to the file containing the definitions of the * - file: Path to the file containing the definitions of the 'operations' and
* 'operations' and 'finished' functions, for instance if they don't * 'finished' functions, for instance if they don't reside in the main
* reside in the main .module file. The path should be relative to * .module file. The path should be relative to base_path(), and thus should
* base_path(), and thus should be built using drupal_get_path(). * be built using drupal_get_path().
* - 'css': Array of paths to CSS files to be used on the progress page. * - css: Array of paths to CSS files to be used on the progress page.
* - 'url_options': options passed to url() when constructing redirect * - url_options: options passed to url() when constructing redirect URLs for
* URLs for the batch. * the batch.
*
* Operations are added as new batch sets. Batch sets are used to ensure
* clean code independence, ensuring that several batches submitted by
* different parts of the code (core / contrib modules) can be processed
* correctly while not interfering or having to cope with each other. Each
* batch set gets to specify his own UI messages, operates on its own set
* of operations and results, and triggers its own 'finished' callback.
* Batch sets are processed sequentially, with the progress bar starting
* fresh for every new set.
*/ */
function batch_set($batch_definition) { function batch_set($batch_definition) {
if ($batch_definition) { if ($batch_definition) {
...@@ -3977,7 +4377,7 @@ function batch_set($batch_definition) { ...@@ -3977,7 +4377,7 @@ function batch_set($batch_definition) {
} }
// Base and default properties for the batch set. // Base and default properties for the batch set.
// Use get_t() to allow batches at install time. // Use get_t() to allow batches during installation.
$t = get_t(); $t = get_t();
$init = array( $init = array(
'sandbox' => array(), 'sandbox' => array(),
...@@ -4142,6 +4542,7 @@ function &batch_get() { ...@@ -4142,6 +4542,7 @@ function &batch_get() {
* The batch array. * The batch array.
* @param $set_id * @param $set_id
* The id of the set to process. * The id of the set to process.
*
* @return * @return
* The name and class of the queue are added by reference to the batch set. * The name and class of the queue are added by reference to the batch set.
*/ */
...@@ -4171,6 +4572,7 @@ function _batch_populate_queue(&$batch, $set_id) { ...@@ -4171,6 +4572,7 @@ function _batch_populate_queue(&$batch, $set_id) {
* *
* @param $batch_set * @param $batch_set
* The batch set. * The batch set.
*
* @return * @return
* The queue object. * The queue object.
*/ */
......
<?php <?php
// $Id: graph.inc,v 1.4 2010/04/22 19:21:12 dries Exp $
/** /**
* @file * @file
* Directed acyclic graph functions. * Directed acyclic graph manipulation.
*/ */
/** /**
* Perform a depth first sort on a directed acyclic graph. * Performs a depth-first search and sort on a directed acyclic graph.
* *
* @param $graph * @param $graph
* A three dimensional associated array, with the first keys being the names * A three dimensional associated array, with the first keys being the names
...@@ -33,7 +32,7 @@ ...@@ -33,7 +32,7 @@
* @endcode * @endcode
* *
* @return * @return
* The passed in $graph with more secondary keys filled in: * The passed-in $graph with more secondary keys filled in:
* - 'paths': Contains a list of vertices than can be reached on a path from * - 'paths': Contains a list of vertices than can be reached on a path from
* this vertex. * this vertex.
* - 'reverse_paths': Contains a list of vertices that has a path from them * - 'reverse_paths': Contains a list of vertices that has a path from them
...@@ -53,7 +52,7 @@ function drupal_depth_first_search(&$graph) { ...@@ -53,7 +52,7 @@ function drupal_depth_first_search(&$graph) {
// The components of the graph. // The components of the graph.
'components' => array(), 'components' => array(),
); );
// Perform the actual sort. // Perform the actual search.
foreach ($graph as $start => $data) { foreach ($graph as $start => $data) {
_drupal_depth_first_search($graph, $state, $start); _drupal_depth_first_search($graph, $state, $start);
} }
...@@ -73,17 +72,17 @@ function drupal_depth_first_search(&$graph) { ...@@ -73,17 +72,17 @@ function drupal_depth_first_search(&$graph) {
} }
/** /**
* Helper function to perform a depth first sort. * Performs a depth-first search on a graph.
* *
* @param &$graph * @param $graph
* A three dimensional associated graph array. * A three dimensional associated graph array.
* @param &$state * @param $state
* An associative array. The key 'last_visit_order' stores a list of the * An associative array. The key 'last_visit_order' stores a list of the
* vertices visited. The key components stores list of vertices belonging * vertices visited. The key components stores list of vertices belonging
* to the same the component. * to the same the component.
* @param $start * @param $start
* An arbitrary vertex where we started traversing the graph. * An arbitrary vertex where we started traversing the graph.
* @param &$component * @param $component
* The component of the last vertex. * The component of the last vertex.
* *
* @see drupal_depth_first_search() * @see drupal_depth_first_search()
...@@ -144,4 +143,3 @@ function _drupal_depth_first_search(&$graph, &$state, $start, &$component = NULL ...@@ -144,4 +143,3 @@ function _drupal_depth_first_search(&$graph, &$state, $start, &$component = NULL
// topological order if the graph is acyclic. // topological order if the graph is acyclic.
$state['last_visit_order'][] = $start; $state['last_visit_order'][] = $start;
} }
<?php <?php
// $Id: image.inc,v 1.40 2010/07/16 02:39:38 dries Exp $
/** /**
* @file * @file
...@@ -9,6 +8,8 @@ ...@@ -9,6 +8,8 @@
/** /**
* @defgroup image Image toolkits * @defgroup image Image toolkits
* @{ * @{
* Functions for image file manipulations.
*
* Drupal's image toolkits provide an abstraction layer for common image file * Drupal's image toolkits provide an abstraction layer for common image file
* manipulations like scaling, cropping, and rotating. The abstraction frees * manipulations like scaling, cropping, and rotating. The abstraction frees
* module authors from the need to support multiple image libraries, and it * module authors from the need to support multiple image libraries, and it
...@@ -33,7 +34,7 @@ ...@@ -33,7 +34,7 @@
*/ */
/** /**
* Return a list of available toolkits. * Gets a list of available toolkits.
* *
* @return * @return
* An array with the toolkit names as keys and the descriptions as values. * An array with the toolkit names as keys and the descriptions as values.
...@@ -54,7 +55,7 @@ function image_get_available_toolkits() { ...@@ -54,7 +55,7 @@ function image_get_available_toolkits() {
} }
/** /**
* Retrieve the name of the currently used toolkit. * Gets the name of the currently used toolkit.
* *
* @return * @return
* String containing the name of the selected toolkit, or FALSE on error. * String containing the name of the selected toolkit, or FALSE on error.
...@@ -100,7 +101,7 @@ function image_toolkit_invoke($method, stdClass $image, array $params = array()) ...@@ -100,7 +101,7 @@ function image_toolkit_invoke($method, stdClass $image, array $params = array())
} }
/** /**
* Get details about an image. * Gets details about an image.
* *
* Drupal supports GIF, JPG and PNG file formats when used with the GD * Drupal supports GIF, JPG and PNG file formats when used with the GD
* toolkit, and may support others, depending on which toolkits are * toolkit, and may support others, depending on which toolkits are
...@@ -122,7 +123,7 @@ function image_toolkit_invoke($method, stdClass $image, array $params = array()) ...@@ -122,7 +123,7 @@ function image_toolkit_invoke($method, stdClass $image, array $params = array())
*/ */
function image_get_info($filepath, $toolkit = FALSE) { function image_get_info($filepath, $toolkit = FALSE) {
$details = FALSE; $details = FALSE;
if (!is_file($filepath)) { if (!is_file($filepath) && !is_uploaded_file($filepath)) {
return $details; return $details;
} }
...@@ -159,7 +160,7 @@ function image_get_info($filepath, $toolkit = FALSE) { ...@@ -159,7 +160,7 @@ function image_get_info($filepath, $toolkit = FALSE) {
* The target height, in pixels. * The target height, in pixels.
* *
* @return * @return
* TRUE or FALSE, based on success. * TRUE on success, FALSE on failure.
* *
* @see image_load() * @see image_load()
* @see image_resize() * @see image_resize()
...@@ -177,59 +178,90 @@ function image_scale_and_crop(stdClass $image, $width, $height) { ...@@ -177,59 +178,90 @@ function image_scale_and_crop(stdClass $image, $width, $height) {
} }
/** /**
* Scales an image to the given width and height while maintaining aspect ratio. * Scales image dimensions while maintaining aspect ratio.
*
* The resulting dimensions can be smaller for one or both target dimensions.
*
* @param $dimensions
* Dimensions to be modified - an array with components width and height, in
* pixels.
* @param $width
* The target width, in pixels. If this value is NULL then the scaling will be
* based only on the height value.
* @param $height
* The target height, in pixels. If this value is NULL then the scaling will
* be based only on the width value.
* @param $upscale
* Boolean indicating that images smaller than the target dimensions will be
* scaled up. This generally results in a low quality image.
*
* @return
* TRUE if $dimensions was modified, FALSE otherwise.
*
* @see image_scale()
*/
function image_dimensions_scale(array &$dimensions, $width = NULL, $height = NULL, $upscale = FALSE) {
$aspect = $dimensions['height'] / $dimensions['width'];
// Calculate one of the dimensions from the other target dimension,
// ensuring the same aspect ratio as the source dimensions. If one of the
// target dimensions is missing, that is the one that is calculated. If both
// are specified then the dimension calculated is the one that would not be
// calculated to be bigger than its target.
if (($width && !$height) || ($width && $height && $aspect < $height / $width)) {
$height = (int) round($width * $aspect);
}
else {
$width = (int) round($height / $aspect);
}
// Don't upscale if the option isn't enabled.
if (!$upscale && ($width >= $dimensions['width'] || $height >= $dimensions['height'])) {
return FALSE;
}
$dimensions['width'] = $width;
$dimensions['height'] = $height;
return TRUE;
}
/**
* Scales an image while maintaining aspect ratio.
* *
* The resulting image can be smaller for one or both target dimensions. * The resulting image can be smaller for one or both target dimensions.
* *
* @param $image * @param $image
* An image object returned by image_load(). * An image object returned by image_load().
* @param $width * @param $width
* The target width, in pixels. This value is omitted then the scaling will * The target width, in pixels. If this value is NULL then the scaling will
* based only on the height value. * be based only on the height value.
* @param $height * @param $height
* The target height, in pixels. This value is omitted then the scaling will * The target height, in pixels. If this value is NULL then the scaling will
* based only on the width value. * be based only on the width value.
* @param $upscale * @param $upscale
* Boolean indicating that files smaller than the dimensions will be scaled * Boolean indicating that files smaller than the dimensions will be scaled
* up. This generally results in a low quality image. * up. This generally results in a low quality image.
* *
* @return * @return
* TRUE or FALSE, based on success. * TRUE on success, FALSE on failure.
* *
* @see image_dimensions_scale()
* @see image_load() * @see image_load()
* @see image_scale_and_crop() * @see image_scale_and_crop()
*/ */
function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale = FALSE) { function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale = FALSE) {
$aspect = $image->info['height'] / $image->info['width']; $dimensions = $image->info;
if ($upscale) { // Scale the dimensions - if they don't change then just return success.
// Set width/height according to aspect ratio if either is empty. if (!image_dimensions_scale($dimensions, $width, $height, $upscale)) {
$width = !empty($width) ? $width : $height / $aspect;
$height = !empty($height) ? $height : $width / $aspect;
}
else {
// Set impossibly large values if the width and height aren't set.
$width = !empty($width) ? $width : 9999999;
$height = !empty($height) ? $height : 9999999;
// Don't scale up.
if (round($width) >= $image->info['width'] && round($height) >= $image->info['height']) {
return TRUE; return TRUE;
} }
}
if ($aspect < $height / $width) {
$height = $width * $aspect;
}
else {
$width = $height / $aspect;
}
return image_resize($image, $width, $height); return image_resize($image, $dimensions['width'], $dimensions['height']);
} }
/** /**
* Resize an image to the given dimensions (ignoring aspect ratio). * Resizes an image to the given dimensions (ignoring aspect ratio).
* *
* @param $image * @param $image
* An image object returned by image_load(). * An image object returned by image_load().
...@@ -239,7 +271,7 @@ function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale = ...@@ -239,7 +271,7 @@ function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale =
* The target height, in pixels. * The target height, in pixels.
* *
* @return * @return
* TRUE or FALSE, based on success. * TRUE on success, FALSE on failure.
* *
* @see image_load() * @see image_load()
* @see image_gd_resize() * @see image_gd_resize()
...@@ -252,7 +284,7 @@ function image_resize(stdClass $image, $width, $height) { ...@@ -252,7 +284,7 @@ function image_resize(stdClass $image, $width, $height) {
} }
/** /**
* Rotate an image by the given number of degrees. * Rotates an image by the given number of degrees.
* *
* @param $image * @param $image
* An image object returned by image_load(). * An image object returned by image_load().
...@@ -266,7 +298,7 @@ function image_resize(stdClass $image, $width, $height) { ...@@ -266,7 +298,7 @@ function image_resize(stdClass $image, $width, $height) {
* be white. * be white.
* *
* @return * @return
* TRUE or FALSE, based on success. * TRUE on success, FALSE on failure.
* *
* @see image_load() * @see image_load()
* @see image_gd_rotate() * @see image_gd_rotate()
...@@ -276,7 +308,7 @@ function image_rotate(stdClass $image, $degrees, $background = NULL) { ...@@ -276,7 +308,7 @@ function image_rotate(stdClass $image, $degrees, $background = NULL) {
} }
/** /**
* Crop an image to the rectangle specified by the given rectangle. * Crops an image to a rectangle specified by the given dimensions.
* *
* @param $image * @param $image
* An image object returned by image_load(). * An image object returned by image_load().
...@@ -290,7 +322,7 @@ function image_rotate(stdClass $image, $degrees, $background = NULL) { ...@@ -290,7 +322,7 @@ function image_rotate(stdClass $image, $degrees, $background = NULL) {
* The target height, in pixels. * The target height, in pixels.
* *
* @return * @return
* TRUE or FALSE, based on success. * TRUE on success, FALSE on failure.
* *
* @see image_load() * @see image_load()
* @see image_scale_and_crop() * @see image_scale_and_crop()
...@@ -308,13 +340,13 @@ function image_crop(stdClass $image, $x, $y, $width, $height) { ...@@ -308,13 +340,13 @@ function image_crop(stdClass $image, $x, $y, $width, $height) {
} }
/** /**
* Convert an image to grayscale. * Converts an image to grayscale.
* *
* @param $image * @param $image
* An image object returned by image_load(). * An image object returned by image_load().
* *
* @return * @return
* TRUE or FALSE, based on success. * TRUE on success, FALSE on failure.
* *
* @see image_load() * @see image_load()
* @see image_gd_desaturate() * @see image_gd_desaturate()
...@@ -323,9 +355,8 @@ function image_desaturate(stdClass $image) { ...@@ -323,9 +355,8 @@ function image_desaturate(stdClass $image) {
return image_toolkit_invoke('desaturate', $image); return image_toolkit_invoke('desaturate', $image);
} }
/** /**
* Load an image file and return an image object. * Loads an image file and returns an image object.
* *
* Any changes to the file are not saved until image_save() is called. * Any changes to the file are not saved until image_save() is called.
* *
...@@ -368,7 +399,7 @@ function image_load($file, $toolkit = FALSE) { ...@@ -368,7 +399,7 @@ function image_load($file, $toolkit = FALSE) {
} }
/** /**
* Close the image and save the changes to a file. * Closes the image and saves the changes to a file.
* *
* @param $image * @param $image
* An image object returned by image_load(). The object's 'info' property * An image object returned by image_load(). The object's 'info' property
...@@ -378,7 +409,7 @@ function image_load($file, $toolkit = FALSE) { ...@@ -378,7 +409,7 @@ function image_load($file, $toolkit = FALSE) {
* original image file will be overwritten. * original image file will be overwritten.
* *
* @return * @return
* TRUE or FALSE, based on success. * TRUE on success, FALSE on failure.
* *
* @see image_load() * @see image_load()
* @see image_gd_save() * @see image_gd_save()
......
<?php <?php
// $Id: install.core.inc,v 1.40 2010/10/15 03:31:28 webchick Exp $
/** /**
* @file * @file
...@@ -7,8 +6,7 @@ ...@@ -7,8 +6,7 @@
*/ */
/** /**
* Global flag to indicate that a task should not be run during the current * Do not run the task during the current installation request.
* installation request.
* *
* This can be used to skip running an installation task when certain * This can be used to skip running an installation task when certain
* conditions are met, even though the task may still show on the list of * conditions are met, even though the task may still show on the list of
...@@ -21,8 +19,7 @@ ...@@ -21,8 +19,7 @@
define('INSTALL_TASK_SKIP', 1); define('INSTALL_TASK_SKIP', 1);
/** /**
* Global flag to indicate that a task should be run on each installation * Run the task on each installation request until the database is set up.
* request that reaches it.
* *
* This is primarily used by the Drupal installer for bootstrap-related tasks. * This is primarily used by the Drupal installer for bootstrap-related tasks.
*/ */
...@@ -201,7 +198,7 @@ function install_state_defaults() { ...@@ -201,7 +198,7 @@ function install_state_defaults() {
} }
/** /**
* Begin an installation request, modifying the installation state as needed. * Begins an installation request, modifying the installation state as needed.
* *
* This function performs commands that must run at the beginning of every page * This function performs commands that must run at the beginning of every page
* request. It throws an exception if the installation should not proceed. * request. It throws an exception if the installation should not proceed.
...@@ -289,7 +286,6 @@ function install_begin_request(&$install_state) { ...@@ -289,7 +286,6 @@ function install_begin_request(&$install_state) {
// Initialize the database system. Note that the connection // Initialize the database system. Note that the connection
// won't be initialized until it is actually requested. // won't be initialized until it is actually requested.
require_once DRUPAL_ROOT . '/includes/database/database.inc'; require_once DRUPAL_ROOT . '/includes/database/database.inc';
spl_autoload_register('db_autoload');
// Verify the last completed task in the database, if there is one. // Verify the last completed task in the database, if there is one.
$task = install_verify_completed_task(); $task = install_verify_completed_task();
...@@ -297,12 +293,11 @@ function install_begin_request(&$install_state) { ...@@ -297,12 +293,11 @@ function install_begin_request(&$install_state) {
else { else {
$task = NULL; $task = NULL;
// Since previous versions of Drupal stored database connection information // Do not install over a configured settings.php. Check the 'db_url'
// in the 'db_url' variable, we should never let an installation proceed if // variable in addition to 'databases', since previous versions of Drupal
// this variable is defined and the settings file was not verified above // used that (and we do not want to allow installations on an existing site
// (otherwise we risk installing over an existing site whose settings file // whose settings file has not yet been updated).
// has not yet been updated). if (!empty($GLOBALS['databases']) || !empty($GLOBALS['db_url'])) {
if (!empty($GLOBALS['db_url'])) {
throw new Exception(install_already_done_error()); throw new Exception(install_already_done_error());
} }
} }
...@@ -527,7 +522,7 @@ function install_tasks($install_state) { ...@@ -527,7 +522,7 @@ function install_tasks($install_state) {
$needs_translations = count($install_state['locales']) > 1 && !empty($install_state['parameters']['locale']) && $install_state['parameters']['locale'] != 'en'; $needs_translations = count($install_state['locales']) > 1 && !empty($install_state['parameters']['locale']) && $install_state['parameters']['locale'] != 'en';
// Start with the core installation tasks that run before handing control // Start with the core installation tasks that run before handing control
// to the install profile. // to the installation profile.
$tasks = array( $tasks = array(
'install_select_profile' => array( 'install_select_profile' => array(
'display_name' => st('Choose profile'), 'display_name' => st('Choose profile'),
...@@ -572,6 +567,12 @@ function install_tasks($install_state) { ...@@ -572,6 +567,12 @@ function install_tasks($install_state) {
// Now add any tasks defined by the installation profile. // Now add any tasks defined by the installation profile.
if (!empty($install_state['parameters']['profile'])) { if (!empty($install_state['parameters']['profile'])) {
// Load the profile install file, because it is not always loaded when
// hook_install_tasks() is invoked (e.g. batch processing).
$profile_install_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.install';
if (file_exists($profile_install_file)) {
include_once $profile_install_file;
}
$function = $install_state['parameters']['profile'] . '_install_tasks'; $function = $install_state['parameters']['profile'] . '_install_tasks';
if (function_exists($function)) { if (function_exists($function)) {
$result = $function($install_state); $result = $function($install_state);
...@@ -597,7 +598,7 @@ function install_tasks($install_state) { ...@@ -597,7 +598,7 @@ function install_tasks($install_state) {
// Allow the installation profile to modify the full list of tasks. // Allow the installation profile to modify the full list of tasks.
if (!empty($install_state['parameters']['profile'])) { if (!empty($install_state['parameters']['profile'])) {
$profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile'; $profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile';
if (is_file($profile_file)) { if (file_exists($profile_file)) {
include_once $profile_file; include_once $profile_file;
$function = $install_state['parameters']['profile'] . '_install_tasks_alter'; $function = $install_state['parameters']['profile'] . '_install_tasks_alter';
if (function_exists($function)) { if (function_exists($function)) {
...@@ -705,15 +706,17 @@ function install_display_output($output, $install_state) { ...@@ -705,15 +706,17 @@ function install_display_output($output, $install_state) {
} }
/** /**
* Installation task; verify the requirements for installing Drupal. * Verifies the requirements for installing Drupal.
* *
* @param $install_state * @param $install_state
* An array of information about the current installation state. * An array of information about the current installation state.
* *
* @return * @return
* A themed status report, or an exception if there are requirement errors. * A themed status report, or an exception if there are requirement errors.
* Otherwise, no output is returned, so that the next task can be run * If there are only requirement warnings, a themed status report is shown
* in the same page request. * initially, but the user is allowed to bypass it by providing 'continue=1'
* in the URL. Otherwise, no output is returned, so that the next task can be
* run in the same page request.
*/ */
function install_verify_requirements(&$install_state) { function install_verify_requirements(&$install_state) {
// Check the installation requirements for Drupal and this profile. // Check the installation requirements for Drupal and this profile.
...@@ -725,25 +728,33 @@ function install_verify_requirements(&$install_state) { ...@@ -725,25 +728,33 @@ function install_verify_requirements(&$install_state) {
// Check the severity of the requirements reported. // Check the severity of the requirements reported.
$severity = drupal_requirements_severity($requirements); $severity = drupal_requirements_severity($requirements);
if ($severity == REQUIREMENT_ERROR) { // If there are errors, always display them. If there are only warnings, skip
// them if the user has provided a URL parameter acknowledging the warnings
// and indicating a desire to continue anyway. See drupal_requirements_url().
if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($install_state['parameters']['continue']))) {
if ($install_state['interactive']) { if ($install_state['interactive']) {
drupal_set_title(st('Requirements problem')); drupal_set_title(st('Requirements problem'));
$status_report = theme('status_report', array('requirements' => $requirements)); $status_report = theme('status_report', array('requirements' => $requirements));
$status_report .= st('Check the error messages and <a href="!url">proceed with the installation</a>.', array('!url' => check_url(request_uri()))); $status_report .= st('Check the error messages and <a href="!url">proceed with the installation</a>.', array('!url' => check_url(drupal_requirements_url($severity))));
return $status_report; return $status_report;
} }
else { else {
// Throw an exception showing all unmet requirements. // Throw an exception showing any unmet requirements.
$failures = array(); $failures = array();
foreach ($requirements as $requirement) { foreach ($requirements as $requirement) {
// Skip warnings altogether for non-interactive installations; these
// proceed in a single request so there is no good opportunity (and no
// good method) to warn the user anyway.
if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) {
$failures[] = $requirement['title'] . ': ' . $requirement['value'] . "\n\n" . $requirement['description']; $failures[] = $requirement['title'] . ': ' . $requirement['value'] . "\n\n" . $requirement['description'];
} }
} }
if (!empty($failures)) {
throw new Exception(implode("\n\n", $failures)); throw new Exception(implode("\n\n", $failures));
} }
} }
} }
}
/** /**
* Installation task; install the Drupal system module. * Installation task; install the Drupal system module.
...@@ -763,7 +774,7 @@ function install_system_module(&$install_state) { ...@@ -763,7 +774,7 @@ function install_system_module(&$install_state) {
// variable_set() can be used now that system.module is installed. // variable_set() can be used now that system.module is installed.
$modules = $install_state['profile_info']['dependencies']; $modules = $install_state['profile_info']['dependencies'];
// The install profile is also a module, which needs to be installed // The installation profile is also a module, which needs to be installed
// after all the dependencies have been installed. // after all the dependencies have been installed.
$modules[] = drupal_get_profile(); $modules[] = drupal_get_profile();
...@@ -772,7 +783,7 @@ function install_system_module(&$install_state) { ...@@ -772,7 +783,7 @@ function install_system_module(&$install_state) {
} }
/** /**
* Verify and return the last installation task that was completed. * Verifies and returns the last installation task that was completed.
* *
* @return * @return
* The last completed task, if there is one. An exception is thrown if Drupal * The last completed task, if there is one. An exception is thrown if Drupal
...@@ -816,7 +827,7 @@ function install_verify_settings() { ...@@ -816,7 +827,7 @@ function install_verify_settings() {
} }
/** /**
* Verify PDO library. * Verifies the PDO library.
*/ */
function install_verify_pdo() { function install_verify_pdo() {
// PDO was moved to PHP core in 5.2.0, but the old extension (targeting 5.0 // PDO was moved to PHP core in 5.2.0, but the old extension (targeting 5.0
...@@ -828,15 +839,14 @@ function install_verify_pdo() { ...@@ -828,15 +839,14 @@ function install_verify_pdo() {
} }
/** /**
* Installation task; define a form to configure and rewrite settings.php. * Form constructor for a form to configure and rewrite settings.php.
* *
* @param $form_state
* An associative array containing the current state of the form.
* @param $install_state * @param $install_state
* An array of information about the current installation state. * An array of information about the current installation state.
* *
* @return * @see install_settings_form_validate()
* The form API definition for the database configuration form. * @see install_settings_form_submit()
* @ingroup forms
*/ */
function install_settings_form($form, &$form_state, &$install_state) { function install_settings_form($form, &$form_state, &$install_state) {
global $databases; global $databases;
...@@ -850,14 +860,13 @@ function install_settings_form($form, &$form_state, &$install_state) { ...@@ -850,14 +860,13 @@ function install_settings_form($form, &$form_state, &$install_state) {
drupal_set_title(st('Database configuration')); drupal_set_title(st('Database configuration'));
$drivers = drupal_detect_database_types(); $drivers = drupal_get_database_types();
$drivers_keys = array_keys($drivers); $drivers_keys = array_keys($drivers);
$form['driver'] = array( $form['driver'] = array(
'#type' => 'radios', '#type' => 'radios',
'#title' => st('Database type'), '#title' => st('Database type'),
'#required' => TRUE, '#required' => TRUE,
'#options' => $drivers,
'#default_value' => !empty($database['driver']) ? $database['driver'] : current($drivers_keys), '#default_value' => !empty($database['driver']) ? $database['driver'] : current($drivers_keys),
'#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_distribution_name())), '#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_distribution_name())),
); );
...@@ -866,96 +875,56 @@ function install_settings_form($form, &$form_state, &$install_state) { ...@@ -866,96 +875,56 @@ function install_settings_form($form, &$form_state, &$install_state) {
$form['driver']['#description'] .= ' ' . st('Your PHP configuration only supports a single database type, so it has been automatically selected.'); $form['driver']['#description'] .= ' ' . st('Your PHP configuration only supports a single database type, so it has been automatically selected.');
} }
// Database name. // Add driver specific configuration options.
$form['database'] = array( foreach ($drivers as $key => $driver) {
'#type' => 'textfield', $form['driver']['#options'][$key] = $driver->name();
'#title' => st('Database name'),
'#default_value' => empty($database['database']) ? '' : $database['database'],
'#size' => 45,
'#required' => TRUE,
'#description' => st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_distribution_name())),
);
// Database username.
$form['username'] = array(
'#type' => 'textfield',
'#title' => st('Database username'),
'#default_value' => empty($database['username']) ? '' : $database['username'],
'#size' => 45,
);
// Database password.
$form['password'] = array(
'#type' => 'password',
'#title' => st('Database password'),
'#default_value' => empty($database['password']) ? '' : $database['password'],
'#size' => 45,
);
$form['advanced_options'] = array(
'#type' => 'fieldset',
'#title' => st('Advanced options'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider.")
);
// Database host.
$form['advanced_options']['host'] = array(
'#type' => 'textfield',
'#title' => st('Database host'),
'#default_value' => empty($database['host']) ? 'localhost' : $database['host'],
'#size' => 45,
// Hostnames can be 255 characters long.
'#maxlength' => 255,
'#required' => TRUE,
'#description' => st('If your database is located on a different server, change this.'),
);
// Database port.
$form['advanced_options']['port'] = array(
'#type' => 'textfield',
'#title' => st('Database port'),
'#default_value' => empty($database['port']) ? '' : $database['port'],
'#size' => 45,
// The maximum port number is 65536, 5 digits.
'#maxlength' => 5,
'#description' => st('If your database server is listening to a non-standard port, enter its number.'),
);
// Table prefix. $form['settings'][$key] = $driver->getFormOptions($database);
$db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_'; $form['settings'][$key]['#prefix'] = '<h2 class="js-hide">' . st('@driver_name settings', array('@driver_name' => $driver->name())) . '</h2>';
$form['advanced_options']['db_prefix'] = array( $form['settings'][$key]['#type'] = 'container';
'#type' => 'textfield', $form['settings'][$key]['#tree'] = TRUE;
'#title' => st('Table prefix'), $form['settings'][$key]['advanced_options']['#parents'] = array($key);
'#default_value' => '', $form['settings'][$key]['#states'] = array(
'#size' => 45, 'visible' => array(
'#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_distribution_name(), '%prefix' => $db_prefix)), ':input[name=driver]' => array('value' => $key),
)
); );
}
$form['actions'] = array('#type' => 'actions'); $form['actions'] = array('#type' => 'actions');
$form['actions']['save'] = array( $form['actions']['save'] = array(
'#type' => 'submit', '#type' => 'submit',
'#value' => st('Save and continue'), '#value' => st('Save and continue'),
'#limit_validation_errors' => array(
array('driver'),
array(isset($form_state['input']['driver']) ? $form_state['input']['driver'] : current($drivers_keys)),
),
'#submit' => array('install_settings_form_submit'),
); );
$form['errors'] = array(); $form['errors'] = array();
$form['settings_file'] = array('#type' => 'value', '#value' => $settings_file); $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file);
$form['_database'] = array('#type' => 'value');
return $form; return $form;
} }
/** /**
* Form API validate for install_settings form. * Form validation handler for install_settings_form().
*
* @see install_settings_form_submit()
*/ */
function install_settings_form_validate($form, &$form_state) { function install_settings_form_validate($form, &$form_state) {
$driver = $form_state['values']['driver'];
$database = $form_state['values'][$driver];
$database['driver'] = $driver;
// TODO: remove when PIFR will be updated to use 'db_prefix' instead of // TODO: remove when PIFR will be updated to use 'db_prefix' instead of
// 'prefix' in the database settings form. // 'prefix' in the database settings form.
$form_state['values']['prefix'] = $form_state['values']['db_prefix']; $database['prefix'] = $database['db_prefix'];
unset($database['db_prefix']);
form_set_value($form['_database'], $form_state['values'], $form_state); $form_state['storage']['database'] = $database;
$errors = install_database_errors($form_state['values'], $form_state['values']['settings_file']); $errors = install_database_errors($database, $form_state['values']['settings_file']);
foreach ($errors as $name => $message) { foreach ($errors as $name => $message) {
form_set_error($name, $message); form_set_error($name, $message);
} }
...@@ -967,22 +936,17 @@ function install_settings_form_validate($form, &$form_state) { ...@@ -967,22 +936,17 @@ function install_settings_form_validate($form, &$form_state) {
function install_database_errors($database, $settings_file) { function install_database_errors($database, $settings_file) {
global $databases; global $databases;
$errors = array(); $errors = array();
// Verify the table prefix.
if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) {
$errors['prefix'] = st('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $database['prefix']));
}
if (!empty($database['port']) && !is_numeric($database['port'])) {
$errors['db_port'] = st('Database port must be a number.');
}
// Check database type. // Check database type.
$database_types = drupal_detect_database_types(); $database_types = drupal_get_database_types();
$driver = $database['driver']; $driver = $database['driver'];
if (!isset($database_types[$driver])) { if (!isset($database_types[$driver])) {
$errors['driver'] = st("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_distribution_name(), '%driver' => $database['driver'])); $errors['driver'] = st("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_distribution_name(), '%driver' => $driver));
} }
else { else {
// Run driver specific validation
$errors += $database_types[$driver]->validateDatabaseSettings($database);
// Run tasks associated with the database type. Any errors are caught in the // Run tasks associated with the database type. Any errors are caught in the
// calling function. // calling function.
$databases['default']['default'] = $database; $databases['default']['default'] = $database;
...@@ -991,28 +955,29 @@ function install_database_errors($database, $settings_file) { ...@@ -991,28 +955,29 @@ function install_database_errors($database, $settings_file) {
Database::parseConnectionInfo(); Database::parseConnectionInfo();
try { try {
db_run_tasks($database['driver']); db_run_tasks($driver);
} }
catch (DatabaseTaskException $e) { catch (DatabaseTaskException $e) {
// These are generic errors, so we do not have any specific key of the // These are generic errors, so we do not have any specific key of the
// database connection array to attach them to; therefore, we just put // database connection array to attach them to; therefore, we just put
// them in the error array with standard numeric keys. // them in the error array with standard numeric keys.
$errors[] = $e->getMessage(); $errors[$driver . '][0'] = $e->getMessage();
} }
} }
return $errors; return $errors;
} }
/** /**
* Form API submit for install_settings form. * Form submission handler for install_settings_form().
*
* @see install_settings_form_validate()
*/ */
function install_settings_form_submit($form, &$form_state) { function install_settings_form_submit($form, &$form_state) {
global $install_state; global $install_state;
$database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port', 'prefix')));
// Update global settings array and save. // Update global settings array and save.
$settings['databases'] = array( $settings['databases'] = array(
'value' => array('default' => array('default' => $database)), 'value' => array('default' => array('default' => $form_state['storage']['database'])),
'required' => TRUE, 'required' => TRUE,
); );
$settings['drupal_hash_salt'] = array( $settings['drupal_hash_salt'] = array(
...@@ -1036,7 +1001,7 @@ function install_find_profiles() { ...@@ -1036,7 +1001,7 @@ function install_find_profiles() {
} }
/** /**
* Installation task; select which profile to install. * Selects which profile to install.
* *
* @param $install_state * @param $install_state
* An array of information about the current installation state. The chosen * An array of information about the current installation state. The chosen
...@@ -1076,8 +1041,21 @@ function install_select_profile(&$install_state) { ...@@ -1076,8 +1041,21 @@ function install_select_profile(&$install_state) {
} }
/** /**
* Helper function for automatically selecting an installation profile from a * Selects an installation profile.
* list or from a selection passed in via $_POST. *
* A profile will be selected if:
* - Only one profile is available,
* - A profile was submitted through $_POST,
* - Exactly one of the profiles is marked as "exclusive".
* If multiple profiles are marked as "exclusive" then no profile will be
* selected.
*
* @param array $profiles
* An associative array of profiles with the machine-readable names as keys.
*
* @return
* The machine-readable name of the selected profile or NULL if no profile was
* selected.
*/ */
function _install_select_profile($profiles) { function _install_select_profile($profiles) {
if (sizeof($profiles) == 0) { if (sizeof($profiles) == 0) {
...@@ -1097,15 +1075,34 @@ function _install_select_profile($profiles) { ...@@ -1097,15 +1075,34 @@ function _install_select_profile($profiles) {
} }
} }
} }
// Check for a profile marked as "exclusive" and ensure that only one
// profile is marked as such.
$exclusive_profile = NULL;
foreach ($profiles as $profile) {
$profile_info = install_profile_info($profile->name);
if (!empty($profile_info['exclusive'])) {
if (empty($exclusive_profile)) {
$exclusive_profile = $profile->name;
}
else {
// We found a second "exclusive" profile. There's no way to choose
// between them, so we ignore the property.
return;
}
}
}
return $exclusive_profile;
} }
/** /**
* Form API array definition for the profile selection form. * Form constructor for the profile selection form.
* *
* @param $form_state * @param $form_state
* Array of metadata about state of form processing. * Array of metadata about state of form processing.
* @param $profile_files * @param $profile_files
* Array of .profile files, as returned from file_scan_directory(). * Array of .profile files, as returned from file_scan_directory().
*
* @ingroup forms
*/ */
function install_select_profile_form($form, &$form_state, $profile_files) { function install_select_profile_form($form, &$form_state, $profile_files) {
$profiles = array(); $profiles = array();
...@@ -1170,6 +1167,15 @@ function install_select_profile_form($form, &$form_state, $profile_files) { ...@@ -1170,6 +1167,15 @@ function install_select_profile_form($form, &$form_state, $profile_files) {
function install_find_locales($profilename) { function install_find_locales($profilename) {
$locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', array('recurse' => FALSE)); $locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', array('recurse' => FALSE));
array_unshift($locales, (object) array('name' => 'en')); array_unshift($locales, (object) array('name' => 'en'));
foreach ($locales as $key => $locale) {
// The locale (file name) might be drupal-7.2.cs.po instead of cs.po.
$locales[$key]->langcode = preg_replace('!^(.+\.)?([^\.]+)$!', '\2', $locale->name);
// Language codes cannot exceed 12 characters to fit into the {languages}
// table.
if (strlen($locales[$key]->langcode) > 12) {
unset($locales[$key]);
}
}
return $locales; return $locales;
} }
...@@ -1195,8 +1201,8 @@ function install_select_locale(&$install_state) { ...@@ -1195,8 +1201,8 @@ function install_select_locale(&$install_state) {
if (!empty($_POST['locale'])) { if (!empty($_POST['locale'])) {
foreach ($locales as $locale) { foreach ($locales as $locale) {
if ($_POST['locale'] == $locale->name) { if ($_POST['locale'] == $locale->langcode) {
$install_state['parameters']['locale'] = $locale->name; $install_state['parameters']['locale'] = $locale->langcode;
return; return;
} }
} }
...@@ -1214,14 +1220,6 @@ function install_select_locale(&$install_state) { ...@@ -1214,14 +1220,6 @@ function install_select_locale(&$install_state) {
$output = '<p>Follow these steps to translate Drupal into your language:</p>'; $output = '<p>Follow these steps to translate Drupal into your language:</p>';
$output .= '<ol>'; $output .= '<ol>';
$output .= '<li>Download a translation from the <a href="http://localize.drupal.org/download" target="_blank">translation server</a>.</li>'; $output .= '<li>Download a translation from the <a href="http://localize.drupal.org/download" target="_blank">translation server</a>.</li>';
$output .= '<li>Rename the downloaded file retaining only the language code at the end of the file name and its extension. For example, if the file name is
<pre>
drupal-7.0.pt-br.po
</pre>
rename it to
<pre>
pt-br.po
</pre></li>';
$output .= '<li>Place it into the following directory: $output .= '<li>Place it into the following directory:
<pre> <pre>
/profiles/' . $profilename . '/translations/ /profiles/' . $profilename . '/translations/
...@@ -1281,22 +1279,23 @@ pt-br.po ...@@ -1281,22 +1279,23 @@ pt-br.po
} }
/** /**
* Form API array definition for language selection. * Form constructor for the language selection form.
*
* @ingroup forms
*/ */
function install_select_locale_form($form, &$form_state, $locales, $profilename) { function install_select_locale_form($form, &$form_state, $locales, $profilename) {
include_once DRUPAL_ROOT . '/includes/iso.inc'; include_once DRUPAL_ROOT . '/includes/iso.inc';
$languages = _locale_get_predefined_list(); $languages = _locale_get_predefined_list();
foreach ($locales as $locale) { foreach ($locales as $locale) {
// Try to use verbose locale name. $name = $locale->langcode;
$name = $locale->name;
if (isset($languages[$name])) { if (isset($languages[$name])) {
$name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . st('(@language)', array('@language' => $languages[$name][1])) : ''); $name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . st('(@language)', array('@language' => $languages[$name][1])) : '');
} }
$form['locale'][$locale->name] = array( $form['locale'][$locale->langcode] = array(
'#type' => 'radio', '#type' => 'radio',
'#return_value' => $locale->name, '#return_value' => $locale->langcode,
'#default_value' => $locale->name == 'en' ? 'en' : '', '#default_value' => $locale->langcode == 'en' ? 'en' : '',
'#title' => $name . ($locale->name == 'en' ? ' ' . st('(built-in)') : ''), '#title' => $name . ($locale->langcode == 'en' ? ' ' . st('(built-in)') : ''),
'#parents' => array('locale') '#parents' => array('locale')
); );
} }
...@@ -1332,7 +1331,7 @@ function install_already_done_error() { ...@@ -1332,7 +1331,7 @@ function install_already_done_error() {
} }
/** /**
* Installation task; load information about the chosen profile. * Loads information about the chosen profile during installation.
* *
* @param $install_state * @param $install_state
* An array of information about the current installation state. The loaded * An array of information about the current installation state. The loaded
...@@ -1341,7 +1340,7 @@ function install_already_done_error() { ...@@ -1341,7 +1340,7 @@ function install_already_done_error() {
*/ */
function install_load_profile(&$install_state) { function install_load_profile(&$install_state) {
$profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile'; $profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile';
if (is_file($profile_file)) { if (file_exists($profile_file)) {
include_once $profile_file; include_once $profile_file;
$install_state['profile_info'] = install_profile_info($install_state['parameters']['profile'], $install_state['parameters']['locale']); $install_state['profile_info'] = install_profile_info($install_state['parameters']['profile'], $install_state['parameters']['locale']);
} }
...@@ -1351,7 +1350,7 @@ function install_load_profile(&$install_state) { ...@@ -1351,7 +1350,7 @@ function install_load_profile(&$install_state) {
} }
/** /**
* Installation task; perform a full bootstrap of Drupal. * Performs a full bootstrap of Drupal during installation.
* *
* @param $install_state * @param $install_state
* An array of information about the current installation state. * An array of information about the current installation state.
...@@ -1361,7 +1360,7 @@ function install_bootstrap_full(&$install_state) { ...@@ -1361,7 +1360,7 @@ function install_bootstrap_full(&$install_state) {
} }
/** /**
* Installation task; install required modules via a batch process. * Installs required modules via a batch process.
* *
* @param $install_state * @param $install_state
* An array of information about the current installation state. * An array of information about the current installation state.
...@@ -1414,7 +1413,7 @@ function install_profile_modules(&$install_state) { ...@@ -1414,7 +1413,7 @@ function install_profile_modules(&$install_state) {
} }
/** /**
* Installation task; import languages via a batch process. * Imports languages via a batch process during installation.
* *
* @param $install_state * @param $install_state
* An array of information about the current installation state. * An array of information about the current installation state.
...@@ -1425,8 +1424,19 @@ function install_profile_modules(&$install_state) { ...@@ -1425,8 +1424,19 @@ function install_profile_modules(&$install_state) {
function install_import_locales(&$install_state) { function install_import_locales(&$install_state) {
include_once DRUPAL_ROOT . '/includes/locale.inc'; include_once DRUPAL_ROOT . '/includes/locale.inc';
$install_locale = $install_state['parameters']['locale']; $install_locale = $install_state['parameters']['locale'];
// Enable installation language as default site language.
locale_add_language($install_locale, NULL, NULL, NULL, '', NULL, 1, TRUE); include_once DRUPAL_ROOT . '/includes/iso.inc';
$predefined = _locale_get_predefined_list();
if (!isset($predefined[$install_locale])) {
// Drupal does not know about this language, so we prefill its values with
// our best guess. The user will be able to edit afterwards.
locale_add_language($install_locale, $install_locale, $install_locale, LANGUAGE_LTR, '', '', TRUE, TRUE);
}
else {
// A known predefined language, details will be filled in properly.
locale_add_language($install_locale, NULL, NULL, NULL, '', '', TRUE, TRUE);
}
// Collect files to import for this language. // Collect files to import for this language.
$batch = locale_batch_by_language($install_locale, NULL); $batch = locale_batch_by_language($install_locale, NULL);
if (!empty($batch)) { if (!empty($batch)) {
...@@ -1437,41 +1447,37 @@ function install_import_locales(&$install_state) { ...@@ -1437,41 +1447,37 @@ function install_import_locales(&$install_state) {
} }
/** /**
* Installation task; configure settings for the new site. * Form constructor for a form to configure the new site.
* *
* @param $form_state
* An associative array containing the current state of the form.
* @param $install_state * @param $install_state
* An array of information about the current installation state. * An array of information about the current installation state.
* *
* @return * @see install_configure_form_validate()
* The form API definition for the site configuration form. * @see install_configure_form_submit()
* @ingroup forms
*/ */
function install_configure_form($form, &$form_state, &$install_state) { function install_configure_form($form, &$form_state, &$install_state) {
if (variable_get('site_name', FALSE) || variable_get('site_mail', FALSE)) {
// Site already configured: This should never happen, means re-running the
// installer, possibly by an attacker after the 'install_task' variable got
// accidentally blown somewhere. Stop it now.
throw new Exception(install_already_done_error());
}
drupal_set_title(st('Configure site')); drupal_set_title(st('Configure site'));
// Warn about settings.php permissions risk // Warn about settings.php permissions risk
$settings_dir = './' . conf_path(); $settings_dir = conf_path();
$settings_file = $settings_dir . '/settings.php'; $settings_file = $settings_dir . '/settings.php';
if (!drupal_verify_install_file($settings_file, FILE_EXIST|FILE_READABLE|FILE_NOT_WRITABLE) || !drupal_verify_install_file($settings_dir, FILE_NOT_WRITABLE, 'dir')) { // Check that $_POST is empty so we only show this message when the form is
// first displayed, not on the next page after it is submitted. (We do not
// want to repeat it multiple times because it is a general warning that is
// not related to the rest of the installation process; it would also be
// 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'); 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');
} }
else {
drupal_set_message(st('All necessary changes to %dir and %file have been made. They have been set to read-only for security.', array('%dir' => $settings_dir, '%file' => $settings_file)));
}
drupal_add_js(drupal_get_path('module', 'system') . '/system.js'); drupal_add_js(drupal_get_path('module', 'system') . '/system.js');
// Add JavaScript time zone detection. // Add JavaScript time zone detection.
drupal_add_js('misc/timezone.js'); drupal_add_js('misc/timezone.js');
// We add these strings as settings because JavaScript translation does not // We add these strings as settings because JavaScript translation does not
// work on install time. // work during installation.
drupal_add_js(array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail'))), 'setting'); drupal_add_js(array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail'))), 'setting');
drupal_add_js('jQuery(function () { Drupal.cleanURLsInstallCheck(); });', 'inline'); drupal_add_js('jQuery(function () { Drupal.cleanURLsInstallCheck(); });', 'inline');
// Add JS to show / hide the 'Email administrator about site updates' elements // Add JS to show / hide the 'Email administrator about site updates' elements
...@@ -1514,7 +1520,7 @@ function install_import_locales_remaining(&$install_state) { ...@@ -1514,7 +1520,7 @@ function install_import_locales_remaining(&$install_state) {
} }
/** /**
* Installation task; perform final steps and display a 'finished' page. * Finishes importing files at end of installation.
* *
* @param $install_state * @param $install_state
* An array of information about the current installation state. * An array of information about the current installation state.
...@@ -1530,13 +1536,13 @@ function install_finished(&$install_state) { ...@@ -1530,13 +1536,13 @@ function install_finished(&$install_state) {
// Flush all caches to ensure that any full bootstraps during the installer // Flush all caches to ensure that any full bootstraps during the installer
// do not leave stale cached data, and that any content types or other items // do not leave stale cached data, and that any content types or other items
// registered by the install profile are registered correctly. // registered by the installation profile are registered correctly.
drupal_flush_all_caches(); drupal_flush_all_caches();
// Remember the profile which was used. // Remember the profile which was used.
variable_set('install_profile', drupal_get_profile()); variable_set('install_profile', drupal_get_profile());
// Install profiles are always loaded last // Installation profiles are always loaded last
db_update('system') db_update('system')
->fields(array('weight' => 1000)) ->fields(array('weight' => 1000))
->condition('type', 'module') ->condition('type', 'module')
...@@ -1701,7 +1707,15 @@ function install_check_requirements($install_state) { ...@@ -1701,7 +1707,15 @@ function install_check_requirements($install_state) {
} }
/** /**
* Forms API array definition for site configuration. * Form constructor for a site configuration form.
*
* @param $install_state
* An array of information about the current installation state.
*
* @see install_configure_form()
* @see install_configure_form_validate()
* @see install_configure_form_submit()
* @ingroup forms
*/ */
function _install_configure_form($form, &$form_state, &$install_state) { function _install_configure_form($form, &$form_state, &$install_state) {
include_once DRUPAL_ROOT . '/includes/locale.inc'; include_once DRUPAL_ROOT . '/includes/locale.inc';
...@@ -1763,7 +1777,7 @@ function _install_configure_form($form, &$form_state, &$install_state) { ...@@ -1763,7 +1777,7 @@ function _install_configure_form($form, &$form_state, &$install_state) {
$countries = country_get_list(); $countries = country_get_list();
$form['server_settings']['site_default_country'] = array( $form['server_settings']['site_default_country'] = array(
'#type' => 'select', '#type' => 'select',
'#title' => t('Default country'), '#title' => st('Default country'),
'#empty_value' => '', '#empty_value' => '',
'#default_value' => variable_get('site_default_country', NULL), '#default_value' => variable_get('site_default_country', NULL),
'#options' => $countries, '#options' => $countries,
...@@ -1814,7 +1828,9 @@ function _install_configure_form($form, &$form_state, &$install_state) { ...@@ -1814,7 +1828,9 @@ function _install_configure_form($form, &$form_state, &$install_state) {
} }
/** /**
* Forms API validate for the site configuration form. * Form validation handler for install_configure_form().
*
* @see install_configure_form_submit()
*/ */
function install_configure_form_validate($form, &$form_state) { function install_configure_form_validate($form, &$form_state) {
if ($error = user_validate_name($form_state['values']['account']['name'])) { if ($error = user_validate_name($form_state['values']['account']['name'])) {
...@@ -1829,7 +1845,9 @@ function install_configure_form_validate($form, &$form_state) { ...@@ -1829,7 +1845,9 @@ function install_configure_form_validate($form, &$form_state) {
} }
/** /**
* Forms API submit for the site configuration form. * Form submission handler for install_configure_form().
*
* @see install_configure_form_validate()
*/ */
function install_configure_form_submit($form, &$form_state) { function install_configure_form_submit($form, &$form_state) {
global $user; global $user;
...@@ -1852,7 +1870,7 @@ function install_configure_form_submit($form, &$form_state) { ...@@ -1852,7 +1870,7 @@ function install_configure_form_submit($form, &$form_state) {
// We precreated user 1 with placeholder values. Let's save the real values. // We precreated user 1 with placeholder values. Let's save the real values.
$account = user_load(1); $account = user_load(1);
$merge_data = array('init' => $form_state['values']['account']['mail'], 'roles' => !empty($account->roles) ? $account->roles : array(), 'status' => 1); $merge_data = array('init' => $form_state['values']['account']['mail'], 'roles' => !empty($account->roles) ? $account->roles : array(), 'status' => 1, 'timezone' => $form_state['values']['date_default_timezone']);
user_save($account, array_merge($form_state['values']['account'], $merge_data)); user_save($account, array_merge($form_state['values']['account'], $merge_data));
// Load global $user and perform final login tasks. // Load global $user and perform final login tasks.
$user = user_load(1); $user = user_load(1);
......