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
  • 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

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
  • 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
Show changes
Showing
with 1752 additions and 582 deletions
<?php
// $Id: database.inc,v 1.39 2010/10/03 01:29:41 dries Exp $
/**
* @file
......@@ -24,7 +23,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
* Version of sqlite lower then 3.6.8 can't use savepoints.
* See http://www.sqlite.org/releaselog/3_6_8.html
*
* @var bool
* @var boolean
*/
protected $savepointSupport = FALSE;
......@@ -35,23 +34,65 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
*/
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()) {
// We don't need a specific PDOStatement class here, we simulate it below.
$this->statementClass = NULL;
// 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;
parent::__construct('sqlite:' . $connection_options['database'], '', '', array(
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
// Force column names to lower case.
PDO::ATTR_CASE => PDO::CASE_LOWER,
// Convert numeric values to strings when fetching.
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.
$version = $this->query('SELECT sqlite_version()')->fetchField();
......@@ -67,6 +108,41 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
$this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
$this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
$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 +248,9 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
// Generate a new temporary table name and protect it from prefixing.
// SQLite requires that temporary tables to be non-qualified.
$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);
return $tablename;
......
<?php
// $Id: install.inc,v 1.2 2009/07/27 19:42:56 dries Exp $
/**
* @file
......@@ -8,8 +7,45 @@
class DatabaseTasks_sqlite extends DatabaseTasks {
protected $pdoDriver = 'sqlite';
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
// $Id: query.inc,v 1.13 2010/09/25 01:41:26 dries Exp $
/**
* @file
......@@ -11,16 +10,6 @@
* @{
*/
/**
* 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.
*
......@@ -43,8 +32,8 @@ class InsertQuery_sqlite extends InsertQuery {
}
public function __toString() {
// Create a comments string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : '';
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// Produce as many generic placeholders as necessary.
$placeholders = array_fill(0, count($this->insertFields), '?');
......@@ -107,12 +96,12 @@ class UpdateQuery_sqlite extends UpdateQuery {
foreach ($fields as $field => $data) {
if (is_array($data)) {
// The field is an expression.
$condition->condition($field, $data['expression'], '<>');
$condition->where($field . ' <> ' . $data['expression']);
$condition->isNull($field);
}
elseif (!isset($data)) {
// The field will be set to NULL.
$condition->isNull($field);
$condition->isNotNull($field);
}
else {
$condition->condition($field, $data, '<>');
......@@ -159,8 +148,8 @@ class DeleteQuery_sqlite extends DeleteQuery {
*/
class TruncateQuery_sqlite extends TruncateQuery {
public function __toString() {
// Create a comments string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : '';
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
}
......
<?php
// $Id: schema.inc,v 1.20 2010/10/22 15:18:56 webchick Exp $
/**
* @file
......@@ -197,6 +196,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
// $map does not use drupal_static as its value never changes.
static $map = array(
'varchar:normal' => 'VARCHAR',
'char:normal' => 'CHAR',
'text:tiny' => 'TEXT',
'text:small' => 'TEXT',
......@@ -271,12 +271,12 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
if (!$this->tableExists($table)) {
return FALSE;
}
$this->connection->tableDropped = TRUE;
$this->connection->query('DROP TABLE {' . $table . '}');
return TRUE;
}
public function addField($table, $field, $spec, $keys_new = array()) {
public function addField($table, $field, $specification, $keys_new = array()) {
if (!$this->tableExists($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 {
throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
}
// TODO: $keys_new is not supported yet.
$query = 'ALTER TABLE {' . $table . '} ADD ';
$query .= $this->createFieldSql($field, $this->processField($spec));
// SQLite doesn't have a full-featured ALTER TABLE statement. It only
// supports adding new fields to a table, in some simple cases. In most
// 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);
// 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 {
* @param $new_schema
* The new schema array for the table.
* @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()) {
$i = 0;
......@@ -313,22 +359,25 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
$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.
$old_keys = array_keys($old_schema['fields']);
$mapping += array_combine($old_keys, $old_keys);
$possible_keys = array_keys($new_schema['fields']);
$mapping += array_combine($possible_keys, $possible_keys);
$select = $this->connection->select($table);
$fields = &$select->getFields();
// Map the fields.
foreach ($old_keys as $key) {
if (isset($mapping[$key])) {
// Don't use ->addField() here because it messes-up with the aliases.
$fields[$mapping[$key]] = array(
'field' => $key,
'table' => $table,
'alias' => $mapping[$key],
);
// Now add the fields.
foreach ($mapping as $field_alias => $field_source) {
// Just ignore this field (ie. use it's default value).
if (!isset($field_source)) {
continue;
}
if (is_array($field_source)) {
$select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
}
else {
$select->addField($table, $field_source, $field_alias);
}
}
......@@ -427,8 +476,6 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
$mapping = array($field => NULL);
unset($new_schema['fields'][$field]);
foreach ($new_schema['indexes'] as $index => $fields) {
foreach ($fields as $key => $field_name) {
......@@ -441,7 +488,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
unset($new_schema['indexes'][$index]);
}
}
$this->alterTable($table, $old_schema, $new_schema, $mapping);
$this->alterTable($table, $old_schema, $new_schema);
return TRUE;
}
......@@ -458,7 +505,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
// Map the old field to the new field.
if ($field != $field_new) {
$mapping[$field] = $field_new;
$mapping[$field_new] = $field;
}
else {
$mapping = array();
......@@ -622,9 +669,14 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
}
public function findTables($table_expression) {
// Don't use {} around sqlite_master table.
$result = db_query("SELECT name FROM sqlite_master WHERE name LIKE :table_name", array(
':table_name' => $table_expression,
// Don't add the prefix, $table_expression already includes the prefix.
$info = $this->getPrefixInfo($table_expression, FALSE);
// 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);
}
......
<?php
/**
* @file
* Select builder for SQLite embedded database engine.
*/
/**
* @ingroup 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 "ingroup database".
*/
<?php
// $Id: date.inc,v 1.1 2009/10/13 21:34:14 dries Exp $
/**
* @file
......@@ -7,7 +6,7 @@
*/
/**
* Implements hook_date_formats().
* Provides a default system list of date formats for system_date_formats().
*/
function system_default_date_formats() {
$formats = array();
......
<?php
// $Id: entity.inc,v 1.16 2010/10/09 02:36:46 webchick Exp $
/**
* Interface for entity controller classes.
......@@ -24,8 +23,12 @@ interface DrupalEntityControllerInterface {
/**
* 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.
......@@ -36,7 +39,8 @@ interface DrupalEntityControllerInterface {
* An array of conditions in the form 'field' => $value.
*
* @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());
}
......@@ -139,9 +143,16 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
/**
* 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();
}
}
/**
* Implements DrupalEntityControllerInterface::load().
......@@ -296,7 +307,7 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
* @param $queried_entities
* Associative array of query results, keyed on the entity 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.
*/
protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
......@@ -398,7 +409,7 @@ class EntityFieldQueryException extends Exception {}
* direct access to the collected properties in order to handle alternate
* execution routines. We therefore use public properties for simplicity. Note
* 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,
* an EntityFieldQueryException will be raised if an unsupported condition is
......@@ -418,6 +429,16 @@ class EntityFieldQuery {
*/
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.
*
......@@ -436,6 +457,21 @@ class EntityFieldQuery {
*/
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::fielDeltaCondition()
*/
public $fieldMetaConditions = array();
/**
* List of property conditions.
*
......@@ -461,6 +497,15 @@ class EntityFieldQuery {
*/
public $range = array();
/**
* The query pager data.
*
* @var array
*
* @see EntityFieldQuery::pager()
*/
public $pager = array();
/**
* Query behavior for deleted data.
*
......@@ -549,6 +594,10 @@ class EntityFieldQuery {
*
* 'bundle', 'revision_id' and 'entity_id' have no such restrictions.
*
* Note: The "comment" and "taxonomy_term" entity types don't support bundle
* conditions. For "taxonomy_term", propertyCondition('vid') can be used
* instead.
*
* @param $name
* 'entity_type', 'bundle', 'revision_id' or 'entity_id'.
* @param $value
......@@ -557,18 +606,25 @@ class EntityFieldQuery {
* dependent on $operator.
* @param $operator
* Possible values:
* - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* operators expect $value to be a literal of the same type as the
* column.
* - 'IN', 'NOT IN': These operators expect $value to be an array of
* literals of the same type as the column.
* - 'BETWEEN': This operator expects $value to be an array of two literals
* 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
* The called object.
*/
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(
'value' => $value,
'operator' => $operator,
......@@ -582,6 +638,88 @@ class EntityFieldQuery {
* @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
* Either a field name or a field array.
* @param $column
* A column defined in the hook_field_schema() of this field. If this is
* omitted then the query will find only entities that have data in this
* field, using the entity and property conditions if there are any.
......@@ -591,13 +729,15 @@ class EntityFieldQuery {
* element in the array is dependent on $operator.
* @param $operator
* Possible values:
* - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* operators expect $value to be a literal of the same type as the
* column.
* - 'IN', 'NOT IN': These operators expect $value to be an array of
* literals of the same type as the column.
* - 'BETWEEN': This operator expects $value to be an array of two literals
* 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
* An arbitrary identifier: conditions in the same group must have the same
* $delta_group. For example, let's presume a multivalue field which has
......@@ -615,15 +755,24 @@ class EntityFieldQuery {
* @return EntityFieldQuery
* 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)) {
$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;
}
// Ensure the same index is used for fieldConditions as for fields.
// Ensure the same index is used for field conditions as for fields.
$index = count($this->fields);
$this->fields[$index] = $field;
if (isset($column)) {
$this->fieldConditions[$index] = array(
$conditions[$index] = array(
'field' => $field,
'column' => $column,
'value' => $value,
......@@ -652,7 +801,7 @@ class EntityFieldQuery {
* array is dependent on $operator.
* @param $operator
* Possible values:
* - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': These
* operators expect $value to be a literal of the same type as the
* column.
* - 'IN', 'NOT IN': These operators expect $value to be an array of
......@@ -666,6 +815,11 @@ class EntityFieldQuery {
* The called object.
*/
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(
'column' => $column,
'value' => $value,
......@@ -680,6 +834,9 @@ class EntityFieldQuery {
* If called multiple times, the query will order by each specified column in
* 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
* 'entity_type', 'bundle', 'revision_id' or 'entity_id'.
* @param $direction
......@@ -716,7 +873,11 @@ class EntityFieldQuery {
*/
public function fieldOrderBy($field, $column, $direction = 'ASC') {
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.
$index = count($this->fields);
......@@ -791,6 +952,68 @@ class EntityFieldQuery {
return $this;
}
/**
* Enable 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;
}
/**
* Enable 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.
*
......@@ -904,6 +1127,10 @@ class EntityFieldQuery {
public function execute() {
// Give a chance to other modules to alter the query.
drupal_alter('entity_query', $this);
$this->altered = TRUE;
// Initialize the pager.
$this->initializePager();
// Execute the query using the correct callback.
$result = call_user_func($this->queryCallback(), $this);
......@@ -959,12 +1186,12 @@ class EntityFieldQuery {
throw new EntityFieldQueryException(t('For this query an entity type must be specified.'));
}
$entity_type = $this->entityConditions['entity_type']['value'];
unset($this->entityConditions['entity_type']);
$entity_info = entity_get_info($entity_type);
if (empty($entity_info['base table'])) {
throw new EntityFieldQueryException(t('Entity %entity has no base table.', array('%entity' => $entity_type)));
}
$base_table = $entity_info['base table'];
$base_table_schema = drupal_get_schema($base_table);
$select_query = db_select($base_table);
$select_query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type));
// Process the property conditions.
......@@ -997,8 +1224,11 @@ class EntityFieldQuery {
// Handle bundles.
if (!empty($entity_info['entity keys']['bundle'])) {
$sql_field = $entity_info['entity keys']['bundle'];
$select_query->addField($base_table, $sql_field, 'bundle');
$having = FALSE;
if (!empty($base_table_schema['fields'][$sql_field])) {
$select_query->addField($base_table, $sql_field, 'bundle');
}
}
else {
$sql_field = 'bundle';
......@@ -1027,6 +1257,24 @@ class EntityFieldQuery {
return $this->finishQuery($select_query);
}
/**
* Get the total number of results and initialize a pager for the query.
*
* This query can be detected by checking for ($this->pager && $this->count),
* which allows a driver to return 0 from the count query and disable
* the pager.
*/
function initializePager() {
if ($this->pager && !$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.
*
......@@ -1057,7 +1305,8 @@ class EntityFieldQuery {
}
$return = array();
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;
$this->ordered_results[] = $partial_entity;
}
......@@ -1095,3 +1344,8 @@ class EntityFieldQuery {
}
}
/**
* Exception thrown when a malformed entity is passed.
*/
class EntityMalformedException extends Exception { }
<?php
// $Id: errors.inc,v 1.9 2010/10/16 00:00:16 webchick Exp $
/**
* @file
......@@ -25,6 +24,8 @@ define('ERROR_REPORTING_DISPLAY_ALL', 2);
* Map PHP error constants to watchdog severity levels.
* The error constants are documented at
* http://php.net/manual/en/errorfunc.constants.php
*
* @ingroup logging_severity_levels
*/
function drupal_error_levels() {
$types = array(
......@@ -135,7 +136,7 @@ function _drupal_decode_exception($exception) {
}
/**
* Render an error message for an exception without any possibility of a further exception occuring.
* Render an error message for an exception without any possibility of a further exception occurring.
*
* @param $exception
* The exception object that was thrown.
......@@ -173,9 +174,9 @@ function error_displayable($error = NULL) {
* Log a PHP error or exception, display an error page in fatal cases.
*
* @param $error
* An array with the following keys: %type, !message, %function, %file, %line.
* All the parameters are plain-text, exception message, which needs to be
* a safe HTML string.
* An array with the following keys: %type, !message, %function, %file, %line
* and severity_level. All the parameters are plain-text, with the exception of
* !message, which needs to be a safe HTML string.
* @param $fatal
* TRUE if the error is fatal.
*/
......
<?php
// $Id: file.inc,v 1.239 2010/10/21 12:09:41 dries Exp $
/**
* @file
......@@ -202,9 +201,8 @@ function file_stream_wrapper_get_class($scheme) {
* @see file_uri_target()
*/
function file_uri_scheme($uri) {
$data = explode('://', $uri, 2);
return count($data) == 2 ? $data[0] : FALSE;
$position = strpos($uri, '://');
return $position ? substr($uri, 0, $position) : FALSE;
}
/**
......@@ -424,7 +422,7 @@ function file_create_url($uri) {
* Directories need to have execute permissions to be considered a directory by
* FTP servers, etc.
*
* @param &$directory
* @param $directory
* A string reference containing the name of a directory path or URI. A
* trailing slash will be trimmed from a path.
* @param $options
......@@ -519,15 +517,21 @@ function file_create_htaccess($directory, $private = TRUE) {
* @param $fids
* An array of file IDs.
* @param $conditions
* An array of conditions to match against the {file_managed} table.
* These should be supplied in the form array('field_name' =>
* 'field_value').
* (deprecated) An associative array of conditions on the {file_managed}
* table, where the keys are the database fields and the values are the
* values those fields must have. Instead, it is preferable to use
* EntityFieldQuery to retrieve a list of entity IDs loadable by
* this function.
*
* @return
* An array of file objects, indexed by fid.
*
* @see hook_file_load()
* @see file_load()
* @see entity_load()
* @see EntityFieldQuery
*
* @todo Remove $conditions in Drupal 8.
*/
function file_load_multiple($fids = array(), $conditions = array()) {
return entity_load('file', $fids, $conditions);
......@@ -568,6 +572,14 @@ function file_save(stdClass $file) {
$file->timestamp = REQUEST_TIME;
$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)) {
drupal_write_record('file_managed', $file);
// Inform modules about the newly added file.
......@@ -581,6 +593,7 @@ function file_save(stdClass $file) {
module_invoke_all('entity_update', $file, 'file');
}
unset($file->original);
return $file;
}
......@@ -592,7 +605,8 @@ function file_save(stdClass $file) {
*
* @return
* 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_delete()
......@@ -605,7 +619,7 @@ function file_usage_list(stdClass $file) {
->execute();
$references = array();
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;
}
......@@ -724,8 +738,7 @@ function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $c
* A file object.
* @param $destination
* 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
* default files scheme will be used, usually "public://".
* This must be a stream wrapper URI.
* @param $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
......@@ -743,7 +756,12 @@ function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $c
*/
function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
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');
return FALSE;
}
......@@ -752,7 +770,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
$file = clone $source;
$file->fid = NULL;
$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 ($replace == FILE_EXISTS_REPLACE) {
$existing_files = file_load_multiple(array(), array('uri' => $uri));
......@@ -765,7 +783,7 @@ function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
// If we are renaming around an existing file (rather than a directory),
// use its basename for the filename.
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
$file->filename = basename($destination);
$file->filename = drupal_basename($destination);
}
$file = file_save($file);
......@@ -810,6 +828,10 @@ function file_valid_uri($uri) {
* is reported.
* - If file already exists in $destination either the call will error out,
* 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
* A string specifying the filepath or URI of the source file.
......@@ -835,31 +857,35 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
$original_destination = $destination;
// Assert that the source file actually exists.
$source = drupal_realpath($source);
if (!file_exists($source)) {
// @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');
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;
}
// Build a destination URI if necessary.
if (!isset($destination)) {
$destination = file_build_uri(basename($source));
$destination = file_build_uri(drupal_basename($source));
}
// Prepare the destination directory.
if (file_prepare_directory($destination)) {
// 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 {
// Perhaps $destination is a dir/file?
$dirname = drupal_dirname($destination);
if (!file_prepare_directory($dirname)) {
// 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');
return FALSE;
}
......@@ -869,12 +895,14 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
$destination = file_destination($destination, $replace);
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');
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, '%destination' => $destination));
return FALSE;
}
// 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');
watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
return FALSE;
......@@ -883,9 +911,13 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
file_ensure_htaccess();
// Perform the copy operation.
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;
}
}
// Set the permissions on the new file.
drupal_chmod($destination);
......@@ -926,7 +958,7 @@ function file_destination($destination, $replace) {
break;
case FILE_EXISTS_RENAME:
$basename = basename($destination);
$basename = drupal_basename($destination);
$directory = drupal_dirname($destination);
$destination = file_create_filename($basename, $directory);
break;
......@@ -954,8 +986,7 @@ function file_destination($destination, $replace) {
* A file object.
* @param $destination
* 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
* default files scheme will be used, usually "public://".
* This must be a stream wrapper URI.
* @param $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
......@@ -975,7 +1006,12 @@ function file_destination($destination, $replace) {
*/
function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
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');
return FALSE;
}
......@@ -997,7 +1033,7 @@ function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS
// If we are renaming around an existing file (rather than a directory),
// use its basename for the filename.
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
$file->filename = basename($destination);
$file->filename = drupal_basename($destination);
}
$file = file_save($file);
......@@ -1110,7 +1146,7 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
}
/**
* Undo the effect of upload_munge_filename().
* Undo the effect of file_munge_filename().
*
* @param $filename
* String with the filename to be unmunged.
......@@ -1201,7 +1237,12 @@ function file_create_filename($basename, $directory) {
*/
function file_delete(stdClass $file, $force = FALSE) {
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');
return FALSE;
}
......@@ -1245,8 +1286,6 @@ function file_delete(stdClass $file, $force = FALSE) {
* @see file_unmanaged_delete_recursive()
*/
function file_unmanaged_delete($path) {
// Resolve streamwrapper URI to local path.
$path = drupal_realpath($path);
if (is_dir($path)) {
watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
return FALSE;
......@@ -1288,8 +1327,6 @@ function file_unmanaged_delete($path) {
* @see file_unmanaged_delete()
*/
function file_unmanaged_delete_recursive($path) {
// Resolve streamwrapper URI to local path.
$path = drupal_realpath($path);
if (is_dir($path)) {
$dir = dir($path);
while (($entry = $dir->read()) !== FALSE) {
......@@ -1414,7 +1451,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
$file = new stdClass();
$file->uid = $user->uid;
$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->filemime = file_get_mimetype($file->filename);
$file->filesize = $_FILES['files']['size'][$source];
......@@ -1510,7 +1547,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
// directory. This overcomes open_basedir restrictions for future file
// operations.
$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.'));
watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
return FALSE;
......@@ -1537,6 +1574,42 @@ function file_save_upload($source, $validators = array(), $destination = 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.
* @see http://drupal.org/node/515192
*
* @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()
* @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.
......@@ -1742,9 +1815,9 @@ function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0,
* @param $data
* A string containing the contents of the file.
* @param $destination
* A string containing the destination URI.
* This must be a stream wrapper URI. If this value is omitted, Drupal's
* default files scheme will be used, usually "public://".
* A string containing the destination URI. This must be a stream wrapper URI.
* If no value is provided, a randomized name will be generated and the file
* will be saved using Drupal's default files scheme, usually "public://".
* @param $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
......@@ -1776,7 +1849,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
$file = new stdClass();
$file->fid = NULL;
$file->uri = $uri;
$file->filename = basename($uri);
$file->filename = drupal_basename($uri);
$file->filemime = file_get_mimetype($file->uri);
$file->uid = $user->uid;
$file->status = FILE_STATUS_PERMANENT;
......@@ -1792,7 +1865,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
// If we are renaming around an existing file (rather than a directory),
// use its basename for the filename.
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
$file->filename = basename($destination);
$file->filename = drupal_basename($destination);
}
return file_save($file);
......@@ -1810,10 +1883,9 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
* @param $data
* A string containing the contents of the file.
* @param $destination
* A string containing the destination location.
* This must be a stream wrapper URI. If no value is provided, a
* randomized name will be generated and the file is saved using Drupal's
* default files scheme, usually "public://".
* A string containing the destination location. This must be a stream wrapper
* URI. If no value is provided, a randomized name will be generated and the
* file will be saved using Drupal's default files scheme, usually "public://".
* @param $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file.
......@@ -1875,10 +1947,11 @@ function file_transfer($uri, $headers) {
* Menu handler for private file transfers.
*
* 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
* returns -1 drupal_access_denied() will be returned. If one or more modules
* returned headers the download will start with the returned headers. If no
* modules respond drupal_not_found() will be returned.
* accessible and what headers it should be transferred with. If one or more
* modules returned headers the download will start with the returned headers.
* If a module returns -1 drupal_access_denied() will be returned. If the file
* 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()
*/
......@@ -1907,6 +1980,7 @@ function file_download() {
if (count($headers)) {
file_transfer($uri, $headers);
}
return drupal_access_denied();
}
return drupal_not_found();
}
......@@ -2126,28 +2200,32 @@ function drupal_unlink($uri, $context = NULL) {
}
/**
* Returns the absolute path of a file or directory
*
* 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.
* Returns the absolute local filesystem path of a stream URI.
*
* @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.
* @see http://drupal.org/node/515192
* Only use this function if you know that the stream wrapper in the URI uses
* the local file system, and you need to pass an absolute path to a function
* that is incompatible with stream URIs.
*
* @param $uri
* A string containing the URI to verify. If this value is omitted,
* Drupal's public files directory will be used [public://].
* A stream wrapper URI or a filesystem path, possibly including one or more
* symbolic links.
*
* @return
* The absolute pathname, or FALSE on failure.
* The absolute local filesystem path (with no symbolic links), or FALSE on
* failure.
*
* @see realpath()
* @see DrupalStreamWrapperInterface::realpath()
* @see http://php.net/manual/function.realpath.php
* @ingroup php_wrappers
* @todo: This function is deprecated, and should be removed wherever possible.
*/
function drupal_realpath($uri) {
// If this URI is a stream, pass it off to the appropriate stream wrapper.
......@@ -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.
*
......@@ -2292,7 +2399,7 @@ function drupal_tempnam($directory, $prefix) {
$wrapper = file_stream_wrapper_get_instance_by_scheme($scheme);
if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) {
return $scheme . '://' . basename($filename);
return $scheme . '://' . drupal_basename($filename);
}
else {
return FALSE;
......@@ -2322,11 +2429,9 @@ function file_directory_temp() {
if (substr(PHP_OS, 0, 3) == 'WIN') {
$directories[] = 'c:\\windows\\temp';
$directories[] = 'c:\\winnt\\temp';
$path_delimiter = '\\';
}
else {
$directories[] = '/tmp';
$path_delimiter = '/';
}
// PHP may be able to find an alternative tmp directory.
// This function exists in PHP 5 >= 5.2.1, but Drupal
......@@ -2343,8 +2448,14 @@ function file_directory_temp() {
}
if (empty($temporary_directory)) {
// If no directory has been found default to 'files/tmp' or 'files\\tmp'.
$temporary_directory = variable_get('file_public_path', conf_path() . '/files') . $path_delimiter . 'tmp';
// If no directory has been found default to 'files/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.
variable_set('file_temporary_path', $temporary_directory);
......
<?php
// $Id: file.mimetypes.inc,v 1.5 2010/07/16 02:40:48 dries Exp $
/**
* @file
......@@ -374,6 +373,7 @@ function file_default_mimetype_mapping() {
333 => 'video/vnd.mpegurl',
347 => 'video/x-flv',
334 => 'video/x-la-asf',
348 => 'video/x-m4v',
335 => 'video/x-mng',
336 => 'video/x-ms-asf',
337 => 'video/x-ms-wm',
......@@ -408,7 +408,6 @@ function file_default_mimetype_mapping() {
'doc' => 14,
'bin' => 15,
'oda' => 16,
'ogg' => 17,
'ogx' => 17,
'pdf' => 18,
'key' => 19,
......@@ -630,6 +629,7 @@ function file_default_mimetype_mapping() {
'm4a' => 188,
'mp3' => 188,
'mp2' => 188,
'ogg' => 189,
'oga' => 189,
'spx' => 189,
'sid' => 190,
......@@ -853,6 +853,7 @@ function file_default_mimetype_mapping() {
'f4a' => 346,
'f4b' => 346,
'flv' => 347,
'm4v' => 348,
),
);
}
<?php
// $Id: filetransfer.inc,v 1.9 2010/04/11 18:33:43 dries Exp $
/*
* Base FileTransfer class.
......@@ -28,8 +27,15 @@ abstract class FileTransfer {
* Classes that extend this class must override the factory() static method.
*
* @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
* @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) {
throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
......@@ -205,10 +211,10 @@ abstract class FileTransfer {
*/
protected function copyDirectoryJailed($source, $destination) {
if ($this->isDirectory($destination)) {
$destination = $destination . '/' . basename($source);
$destination = $destination . '/' . drupal_basename($source);
}
$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));
if ($file->isDir()) {
$this->createDirectory($destination . $relative_path);
......@@ -296,7 +302,7 @@ abstract class FileTransfer {
$chroot = '';
while (count($parts)) {
$check = implode($parts, '/');
if ($this->isFile($check . '/' . basename(__FILE__))) {
if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
// Remove the trailing slash.
return substr($chroot, 0, -1);
}
......@@ -313,6 +319,42 @@ abstract class FileTransfer {
$this->chroot = $this->findChroot();
$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;
}
}
/**
......@@ -345,3 +387,31 @@ interface FileTransferChmodInterface {
*/
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);
}
function next() {
parent::next();
while ($this->isDot()) {
parent::next();
}
}
}
<?php
// $Id: ftp.inc,v 1.14 2010/10/05 02:08:53 dries Exp $
/**
* Base class for FTP implementations.
......@@ -24,8 +23,10 @@ abstract class FileTransferFTP extends FileTransfer {
* options. If the FTP PHP extension is available, use it.
*/
static function factory($jail, $settings) {
$settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname'];
$settings['port'] = empty($settings['port']) ? 21 : $settings['port'];
$username = empty($settings['username']) ? '' : $settings['username'];
$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')) {
$class = 'FileTransferFTPExtension';
......@@ -34,7 +35,16 @@ abstract class FileTransferFTP extends FileTransfer {
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
// $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.
......@@ -31,7 +30,7 @@ class FileTransferLocal extends FileTransfer implements FileTransferChmodInterfa
// 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));
}
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 (@!drupal_rmdir($filename)) {
throw new FileTransferException('Cannot remove directory %directory.', NULL, array('%directory' => $filename));
......@@ -64,7 +63,7 @@ class FileTransferLocal extends FileTransfer implements FileTransferChmodInterfa
public function chmodJailed($path, $mode, $recursive) {
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)) {
throw new FileTransferException('Cannot chmod %path.', NULL, array('%path' => $filename));
}
......
<?php
// $Id: ssh.inc,v 1.5 2010/01/30 07:59:24 dries Exp $
/**
* The SSH connection class for the update module.
......@@ -17,7 +16,7 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
function connect() {
$this->connection = @ssh2_connect($this->hostname, $this->port);
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)) {
throw new FileTransferException('The supplied username/password combination was not accepted.');
......@@ -25,9 +24,11 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
}
static function factory($jail, $settings) {
$settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname'];
$settings['port'] = empty($settings['port']) ? 22 : $settings['port'];
return new FileTransferSSH($jail, $settings['username'], $settings['password'], $settings['hostname'], $settings['port']);
$username = empty($settings['username']) ? '' : $settings['username'];
$password = empty($settings['password']) ? '' : $settings['password'];
$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) {
......@@ -95,4 +96,13 @@ class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface
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
// $Id: form.inc,v 1.506 2010/10/21 20:46:58 webchick Exp $
/**
* @defgroup forms Form builder functions
......@@ -17,7 +16,7 @@
* \@see user_pass_validate().
* \@see user_pass_submit().
*
* @} End of "defgroup forms".
* @}
*/
/**
......@@ -77,7 +76,7 @@
* workflow, see the
* @link http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html Form API reference @endlink
* 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
* @link form_example_tutorial.inc the Form Example Tutorial @endlink which
* provide basics all the way up through multistep forms.
......@@ -87,66 +86,7 @@
* passed by reference to most functions, so they use it to communicate with
* the form system and each other.
*
* The $form_state keys are:
* - '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'.
* See drupal_build_form() for documentation of $form_state keys.
*/
/**
......@@ -197,7 +137,7 @@ function drupal_get_form($form_id) {
* can implement hook_forms(), which maps different $form_id values to the
* proper form constructor function. Examples may be found in node_forms(),
* search_forms(), and user_forms().
* @param &$form_state
* @param $form_state
* 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
* when the form submission process is complete. Furthermore, it may be used
......@@ -205,22 +145,27 @@ function drupal_get_form($form_id) {
* 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
* is rendered:
* - build_info: A keyed array of build information that is necessary to
* rebuild the form from cache when the original context may no longer be
* available:
* - args: An array of arguments to pass to the form builder.
* - build_info: Internal. An associative array of information stored by Form
* API that is necessary to build and rebuild the form from cache when the
* original context may no longer be available:
* - args: A list of arguments to pass to the form constructor.
* - 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
* another array containing values for the parameters 'type', 'module' and
* 'name' as needed by module_load_include(). The files listed here are
* automatically loaded by form_get_cache(). Defaults to the current menu
* router item's 'file' definition, if existent.
* automatically loaded by form_get_cache(). By default the current menu
* router item's 'file' definition is added, if any. Use
* form_load_include() to add include files from a form constructor.
* - 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
* 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
* 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
* 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.
* 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
......@@ -228,32 +173,107 @@ function drupal_get_form($form_id) {
* set $form_state['rebuild'] to cause the form processing to bypass submit
* handlers and rebuild the form instead, even if there are no validation
* errors.
* - input: An array of input that corresponds to $_POST or $_GET, depending
* on the 'method' chosen (see below).
* - redirect: Used to redirect the form on submission. It may either be a
* 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.
* 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
* 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.
* - no_redirect: If set to TRUE the form will NOT perform a drupal_goto(),
* even if 'redirect' is set.
* forms that do not change data, as that is exclusively the domain of
* 'post.'
* - 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
* 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
* @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.)
* - 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'. 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
* 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
* searches do not need to have a form_id in their query arguments to
* 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
* 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
* page request only. It may be used to temporary save any data that doesn't
* need to or shouldn't be cached during the whole form workflow, e.g. data
* that needs to be accessed during the current form build process only.
* page request only. All $form_state properties that are not reserved keys
* (see form_state_keys_no_cache()) persist throughout a multistep form
* 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
* common elements, such as back/next/save buttons in multi-step form
* wizards, may define a form builder function name that returns a form
......@@ -262,11 +282,12 @@ function drupal_get_form($form_id) {
* hook_forms() or have to invoke drupal_build_form() (instead of
* drupal_get_form()) on their own in a custom menu callback to prepare
* $form_state accordingly.
* Further $form_state properties controlling the redirection behavior after
* form submission may be found in drupal_redirect_form().
* Information on how certain $form_state properties control redirection
* behavior after form submission may be found in drupal_redirect_form().
*
* @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()
*/
......@@ -363,7 +384,12 @@ function form_state_defaults() {
'rebuild' => FALSE,
'rebuild_info' => array(),
'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(),
'submitted' => FALSE,
'executed' => FALSE,
......@@ -386,9 +412,9 @@ function form_state_defaults() {
* function is called to generate a new $form, the next step in the form
* 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()
* for more information about creating AJAX-enabled forms.
* for more information about creating Ajax-enabled forms.
*
* @param $form_id
* The unique string identifying the desired form. If a function
......@@ -401,7 +427,7 @@ function form_state_defaults() {
* A keyed array containing the current state of the form.
* @param $old_form
* (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
* $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.
......@@ -417,7 +443,7 @@ function form_state_defaults() {
function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
$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
// manually update the hidden 'build_id' input element.
// Otherwise, a new #build_id is generated, to not clobber the previous
......@@ -431,7 +457,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());
}
// #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
// #action needs to be retained.
if (isset($old_form['#action']) && !empty($form_state['rebuild_info']['copy']['#action'])) {
......@@ -470,7 +496,7 @@ function form_get_cache($form_build_id, &$form_state) {
$form_state = $cached->data + $form_state;
// 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());
foreach ($form_state['build_info']['files'] as $file) {
if (is_array($file)) {
......@@ -537,6 +563,55 @@ function form_state_keys_no_cache() {
);
}
/**
* Loads an include file and makes sure it 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.
*
......@@ -602,9 +677,14 @@ function drupal_form_submit($form_id, &$form_state) {
// Merge in default values.
$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['programmed'] = TRUE;
$form = drupal_retrieve_form($form_id, $form_state);
// Programmed forms are always submitted.
$form_state['submitted'] = TRUE;
......@@ -641,6 +721,8 @@ function drupal_retrieve_form($form_id, &$form_state) {
if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) {
$item = menu_get_item();
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'];
}
}
......@@ -816,13 +898,13 @@ function drupal_process_form($form_id, &$form, &$form_state) {
// 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
// $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
// $form_state['rebuild']. It would not make sense to redisplay the
// identical form without an error for the user to correct, so we also
// rebuild error-free non-executed forms, regardless of
// $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
// divergent form execution based on whether the triggering element has
// #executes_submit_callback set to TRUE.
......@@ -839,7 +921,7 @@ function drupal_process_form($form_id, &$form, &$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
// 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()
// already takes care of that.
if (!$form_state['rebuild'] && $form_state['cache'] && empty($form_state['no_cache'])) {
......@@ -928,6 +1010,8 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
$form += array('#tree' => FALSE, '#parents' => array());
if (!isset($form['#validate'])) {
// Ensure that modules can rely on #validate being set.
$form['#validate'] = array();
// Check for a handler specific to $form_id.
if (function_exists($form_id . '_validate')) {
$form['#validate'][] = $form_id . '_validate';
......@@ -940,6 +1024,8 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
}
if (!isset($form['#submit'])) {
// Ensure that modules can rely on #submit being set.
$form['#submit'] = array();
// Check for a handler specific to $form_id.
if (function_exists($form_id . '_submit')) {
$form['#submit'][] = $form_id . '_submit';
......@@ -1010,8 +1096,12 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
// matches the current user's session.
if (isset($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.
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 +1121,29 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
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'])) {
$values[$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value'];
drupal_array_set_nested_value($values, $form_state['triggering_element']['#parents'], $form_state['triggering_element']['#value']);
$button_value = $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;
}
......@@ -1044,10 +1153,28 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
* 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
* is redirected. By default, we redirect to a new destination page. The path of
* the destination page can be set in $form_state['redirect']. If that is not
* set, the user is redirected to the current page to display a fresh,
* unpopulated copy of the form.
* is redirected. By default, we redirect to a new destination page. The path
* of the destination page can be set in $form_state['redirect'], as either a
* string containing the destination or an array of arguments compatible with
* drupal_goto(). If that is not set, the user is redirected to the current
* page to display a fresh, unpopulated copy of the form.
*
* For example, to redirect to 'node':
* @code
* $form_state['redirect'] = 'node';
* @endcode
* Or to redirect to 'node/123?foo=bar#baz':
* @code
* $form_state['redirect'] = array(
* 'node/123',
* array(
* 'query' => array(
* 'foo' => 'bar',
* ),
* 'fragment' => 'baz',
* ),
* );
* @endcode
*
* There are several triggers that may prevent a redirection though:
* - If $form_state['redirect'] is FALSE, a form builder function or form
......@@ -1061,7 +1188,7 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
* - If $form_state['no_redirect'] is TRUE, then the callback that originally
* built the form explicitly disallows any redirection, regardless of the
* redirection value in $form_state['redirect']. For example, ajax_get_form()
* defines $form_state['no_redirect'] when building a form in an AJAX
* defines $form_state['no_redirect'] when building a form in an Ajax
* callback to prevent any redirection. $form_state['no_redirect'] should NOT
* be altered by form builder functions or form validation/submit handlers.
* - If $form_state['programmed'] is TRUE, the form submission was usually
......@@ -1201,7 +1328,7 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
// 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
// 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
// #limit_validation_errors property. For button element types,
// #limit_validation_errors defaults to FALSE (via system_element_info()),
......@@ -1427,8 +1554,10 @@ function form_set_error($name = NULL, $message = '', $limit_validation_errors =
// reconstructed #parents begin with the same keys as the specified
// section, then the element's values are within the part of
// $form_state['values'] that the clicked button requires to be valid,
// so errors for this element must be recorded.
if (array_slice(explode('][', $name), 0, count($section)) === $section) {
// so errors for this element must be recorded. As the exploded array
// 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;
break;
}
......@@ -1559,7 +1688,7 @@ function form_error(&$element, $message = '') {
* This is most commonly implemented with a submit handler setting persistent
* data within $form_state based on *validated* values in
* $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.
* - Where user input must affect the rendering of the form without affecting
* its structure, the necessary conditional rendering logic should reside
......@@ -1618,6 +1747,9 @@ function form_builder($form_id, &$element, &$form_state) {
else {
$form_state['process_input'] = FALSE;
}
// All form elements should have an #array_parents property.
$element['#array_parents'] = array();
}
if (!isset($element['#id'])) {
......@@ -1674,7 +1806,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);
}
// 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;
$element[$key]['#array_parents'] = $array_parents;
......@@ -1811,7 +1943,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
// #access=FALSE on an element usually allow access for some users, so forms
// submitted with drupal_form_submit() may bypass access restriction and be
// treated as high-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'])));
// Set the element's #value property.
......@@ -1872,12 +2004,12 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
// form_state_values_clean(). Enforce the same input processing restrictions
// as above.
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)) {
$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
// button within the constraints of how browsers provide this information.
if (isset($element['#button_type'])) {
......@@ -1902,7 +2034,7 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
* Helper function to handle the convoluted logic of button click detection.
*
* 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
* 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
......@@ -1927,7 +2059,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
* 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
* submissions from Internet Explorer in response to an ENTER key pressed in a
* textfield (form_builder() has extra code for that).
......@@ -1972,7 +2104,7 @@ function _form_button_was_clicked($element, &$form_state) {
* - form_build_id
* - op
*
* @param &$form_state
* @param $form_state
* A keyed array containing the current state of the form, including
* submitted form values; altered by reference.
*/
......@@ -2073,11 +2205,29 @@ function form_type_image_button_value($form, $input, $form_state) {
* for this element. Return nothing to use the default.
*/
function form_type_checkbox_value($element, $input = FALSE) {
if ($input !== FALSE) {
// Successful (checked) checkboxes are present with a value (possibly '0').
// http://www.w3.org/TR/html401/interact/forms.html#successful-controls
// For an unchecked checkbox, we return integer 0, so we can explicitly
// test for a value different than string '0'.
if ($input === FALSE) {
// Use #default_value as the default value of a checkbox, except change
// NULL to 0, because _form_builder_handle_input_element() would otherwise
// replace NULL with empty string, but an empty string is a potentially
// 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;
}
}
......@@ -2109,7 +2259,7 @@ function form_type_checkboxes_value($element, $input = FALSE) {
// NULL elements from the array before constructing the return value, to
// simulate the behavior of web browsers (which do not send unchecked
// 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) {
if (!isset($value)) {
unset($input[$key]);
......@@ -2256,11 +2406,13 @@ function form_type_token_value($element, $input = FALSE) {
}
/**
* Change submitted form values during form validation.
* Changes submitted form values in $form_state.
*
* 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
* 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'
* component of the form array (the value of $form['#validate'] is an array of
......@@ -2283,7 +2435,7 @@ function form_type_token_value($element, $input = FALSE) {
* Form state array where the value change should be recorded.
*/
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);
}
/**
......@@ -2550,6 +2702,9 @@ function theme_fieldset($variables) {
/**
* 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
* An associative array containing:
* - element: An associative array containing the properties of the element.
......@@ -2563,7 +2718,7 @@ function theme_radio($variables) {
$element['#attributes']['type'] = 'radio';
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';
}
_form_set_class($element, array('form-radio'));
......@@ -2661,7 +2816,17 @@ function password_confirm_validate($element, &$element_state) {
*/
function theme_date($variables) {
$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>';
}
/**
......@@ -2723,9 +2888,9 @@ function form_process_date($element) {
/**
* Validates the date type to stop dates like February 30, 2006.
*/
function date_validate($form) {
if (!checkdate($form['#value']['month'], $form['#value']['day'], $form['#value']['year'])) {
form_error($form, t('The specified date is invalid.'));
function date_validate($element) {
if (!checkdate($element['#value']['month'], $element['#value']['day'], $element['#value']['year'])) {
form_error($element, t('The specified date is invalid.'));
}
}
......@@ -2768,24 +2933,32 @@ function weight_value(&$form) {
*/
function form_process_radios($element) {
if (count($element['#options']) > 0) {
$weight = 0;
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
// unique id for each radio button.
$parents_for_id = array_merge($element['#parents'], array($key));
$element[$key] = array(
$element[$key] += array(
'#type' => 'radio',
'#title' => $choice,
'#return_value' => check_plain($key),
// The key is sanitized in drupal_attributes() during output from the
// theme function.
'#return_value' => $key,
'#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL,
'#attributes' => $element['#attributes'],
'#parents' => $element['#parents'],
'#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
'#weight' => $weight,
);
}
}
}
return $element;
}
......@@ -2807,7 +2980,7 @@ function theme_checkbox($variables) {
element_set_attributes($element, array('id', 'name', '#return_value' => 'value'));
// 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';
}
_form_set_class($element, array('form-checkbox'));
......@@ -2859,6 +3032,33 @@ function form_pre_render_conditional_form_element($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;
}
function form_process_checkboxes($element) {
$value = is_array($element['#value']) ? $element['#value'] : array();
$element['#tree'] = TRUE;
......@@ -2866,20 +3066,32 @@ function form_process_checkboxes($element) {
if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
$element['#default_value'] = array();
}
$weight = 0;
foreach ($element['#options'] as $key => $choice) {
if (!isset($element[$key])) {
$element[$key] = array(
// Integer 0 is not a valid #return_value, so use '0' instead.
// @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',
'#processed' => TRUE,
'#title' => $choice,
'#return_value' => $key,
'#default_value' => isset($value[$key]) ? $key : NULL,
'#attributes' => $element['#attributes'],
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
'#weight' => $weight,
);
}
}
}
return $element;
}
......@@ -2920,7 +3132,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
* An associative array containing:
......@@ -2931,11 +3147,17 @@ function form_process_container($element, &$form_state) {
*/
function theme_container($variables) {
$element = $variables['element'];
// Special handling for form elements.
if (isset($element['#array_parents'])) {
// Assign an html ID.
if (!isset($element['#attributes']['id'])) {
$element['#attributes']['id'] = $element['#id'];
}
// Force the 'form-wrapper' class.
// Add the 'form-wrapper' class.
$element['#attributes']['class'][] = 'form-wrapper';
}
return '<div' . drupal_attributes($element['#attributes']) . '>' . $element['#children'] . '</div>';
}
......@@ -3129,6 +3351,13 @@ function form_process_machine_name($element, &$form_state) {
'replace' => '_',
);
// 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.
if (empty($element['#machine_name']['source'])) {
return $element;
......@@ -3200,7 +3429,7 @@ function form_validate_machine_name(&$element, &$form_state) {
// Verify that the machine name is unique.
if ($element['#default_value'] !== $element['#value']) {
$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.'));
}
}
......@@ -3210,7 +3439,7 @@ function form_validate_machine_name(&$element, &$form_state) {
* Adds fieldsets to the specified group or adds group members to this
* fieldset.
*
* @param &$element
* @param $element
* An associative array containing the properties and children of the
* fieldset. Note that $element must be taken by reference here, so processed
* child elements are taken over into $form_state.
......@@ -3235,7 +3464,7 @@ function form_process_fieldset(&$element, &$form_state) {
}
// 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
// containers.
......@@ -3245,7 +3474,7 @@ function form_process_fieldset(&$element, &$form_state) {
// Collapsible fieldsets
if (!empty($element['#collapsible'])) {
$element['#attached']['js'][] = 'misc/collapse.js';
$element['#attached']['library'][] = array('system', 'drupal.collapse');
$element['#attributes']['class'][] = 'collapsible';
if (!empty($element['#collapsed'])) {
$element['#attributes']['class'][] = 'collapsed';
......@@ -3361,7 +3590,7 @@ function form_process_vertical_tabs($element, &$form_state) {
function theme_vertical_tabs($variables) {
$element = $variables['element'];
// 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 .= '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
......@@ -3470,7 +3699,7 @@ function theme_textfield($variables) {
$extra = '';
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';
$attributes = array();
......@@ -3532,7 +3761,7 @@ function theme_textarea($variables) {
// Add resizable behavior.
if (!empty($element['#resizable'])) {
drupal_add_js('misc/textarea.js');
drupal_add_library('system', 'drupal.textarea');
$wrapper_attributes['class'][] = 'resizable';
}
......@@ -3556,7 +3785,7 @@ function theme_textarea($variables) {
function theme_password($variables) {
$element = $variables['element'];
$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'));
return '<input' . drupal_attributes($element['#attributes']) . ' />';
......@@ -3566,13 +3795,27 @@ function theme_password($variables) {
* Expand weight elements into selects.
*/
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++) {
$weights[$n] = $n;
}
$element['#options'] = $weights;
$element['#type'] = 'select';
$element['#is_weight'] = TRUE;
$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;
}
......@@ -3756,7 +3999,7 @@ function theme_form_element_label($variables) {
$t = get_t();
// 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 '';
}
......@@ -3788,7 +4031,7 @@ function theme_form_element_label($variables) {
*
* Adds 'required' and 'error' classes as needed.
*
* @param &$element
* @param $element
* The form element.
* @param $name
* Array of new class names to be added.
......@@ -3811,6 +4054,36 @@ function _form_set_class(&$element, $class = array()) {
}
}
/**
* Helper form element validator: integer.
*/
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'])));
}
}
/**
* Helper form element validator: integer > 0.
*/
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'])));
}
}
/**
* Helper form element validator: number.
*/
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".
*/
......@@ -3818,6 +4091,8 @@ function _form_set_class(&$element, $class = array()) {
/**
* @defgroup batch Batch operations
* @{
* Create and process batch operations.
*
* Functions allowing forms processing to be spread out over several page
* requests, thus ensuring that the processing does not get interrupted
* because of a PHP timeout, while allowing the user to receive feedback
......@@ -3839,14 +4114,17 @@ function _form_set_class(&$element, $class = array()) {
* 'file' => 'path_to_file_containing_myfunctions',
* );
* batch_set($batch);
* // only needed if not inside a form _submit handler :
* batch_process();
* // Only needed if not inside a form _submit handler.
* // Setting redirect in batch_process.
* batch_process('node/1');
* @endcode
*
* Note: if the batch 'title', 'init_message', 'progress_message', or
* '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
* 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:
* @code
......@@ -3869,8 +4147,8 @@ function _form_set_class(&$element, $class = array()) {
* // and the batch processing can continue to the next operation.
*
* $node = node_load(array('uid' => $uid, 'type' => $type));
* $context['results'][] = $node->nid . ' : ' . $node->title;
* $context['message'] = $node->title;
* $context['results'][] = $node->nid . ' : ' . check_plain($node->title);
* $context['message'] = check_plain($node->title);
* }
*
* // More advanced example: multi-step operation - load all nodes, five by five
......@@ -3889,10 +4167,10 @@ function _form_set_class(&$element, $class = array()) {
* ->execute();
* foreach ($result as $row) {
* $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']['current_node'] = $node->nid;
* $context['message'] = $node->title;
* $context['message'] = check_plain($node->title);
* }
* if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
* $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
......@@ -3903,6 +4181,8 @@ function _form_set_class(&$element, $class = array()) {
* Sample 'finished' callback:
* @code
* 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) {
* $message = format_plural(count($results), 'One post processed.', '@count posts processed.');
* }
......
<?php
// $Id: graph.inc,v 1.4 2010/04/22 19:21:12 dries Exp $
/**
* @file
......@@ -33,7 +32,7 @@
* @endcode
*
* @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
* this vertex.
* - 'reverse_paths': Contains a list of vertices that has a path from them
......@@ -75,15 +74,15 @@ function drupal_depth_first_search(&$graph) {
/**
* Helper function to perform a depth first sort.
*
* @param &$graph
* @param $graph
* A three dimensional associated graph array.
* @param &$state
* @param $state
* An associative array. The key 'last_visit_order' stores a list of the
* vertices visited. The key components stores list of vertices belonging
* to the same the component.
* @param $start
* An arbitrary vertex where we started traversing the graph.
* @param &$component
* @param $component
* The component of the last vertex.
*
* @see drupal_depth_first_search()
......@@ -144,4 +143,3 @@ function _drupal_depth_first_search(&$graph, &$state, $start, &$component = NULL
// topological order if the graph is acyclic.
$state['last_visit_order'][] = $start;
}
<?php
// $Id: image.inc,v 1.40 2010/07/16 02:39:38 dries Exp $
/**
* @file
......@@ -9,6 +8,8 @@
/**
* @defgroup image Image toolkits
* @{
* Functions for image file manipulations.
*
* Drupal's image toolkits provide an abstraction layer for common image file
* manipulations like scaling, cropping, and rotating. The abstraction frees
* module authors from the need to support multiple image libraries, and it
......@@ -122,7 +123,7 @@ function image_toolkit_invoke($method, stdClass $image, array $params = array())
*/
function image_get_info($filepath, $toolkit = FALSE) {
$details = FALSE;
if (!is_file($filepath)) {
if (!is_file($filepath) && !is_uploaded_file($filepath)) {
return $details;
}
......@@ -159,7 +160,7 @@ function image_get_info($filepath, $toolkit = FALSE) {
* The target height, in pixels.
*
* @return
* TRUE or FALSE, based on success.
* TRUE on success, FALSE on failure.
*
* @see image_load()
* @see image_resize()
......@@ -177,7 +178,55 @@ 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.
*
......@@ -194,38 +243,21 @@ function image_scale_and_crop(stdClass $image, $width, $height) {
* up. This generally results in a low quality image.
*
* @return
* TRUE or FALSE, based on success.
* TRUE on success, FALSE on failure.
*
* @see image_dimensions_scale()
* @see image_load()
* @see image_scale_and_crop()
*/
function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale = FALSE) {
$aspect = $image->info['height'] / $image->info['width'];
$dimensions = $image->info;
if ($upscale) {
// Set width/height according to aspect ratio if either is empty.
$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']) {
// Scale the dimensions - if they don't change then just return success.
if (!image_dimensions_scale($dimensions, $width, $height, $upscale)) {
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']);
}
/**
......@@ -239,7 +271,7 @@ function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale =
* The target height, in pixels.
*
* @return
* TRUE or FALSE, based on success.
* TRUE on success, FALSE on failure.
*
* @see image_load()
* @see image_gd_resize()
......@@ -266,7 +298,7 @@ function image_resize(stdClass $image, $width, $height) {
* be white.
*
* @return
* TRUE or FALSE, based on success.
* TRUE on success, FALSE on failure.
*
* @see image_load()
* @see image_gd_rotate()
......@@ -290,7 +322,7 @@ function image_rotate(stdClass $image, $degrees, $background = NULL) {
* The target height, in pixels.
*
* @return
* TRUE or FALSE, based on success.
* TRUE on success, FALSE on failure.
*
* @see image_load()
* @see image_scale_and_crop()
......@@ -314,7 +346,7 @@ function image_crop(stdClass $image, $x, $y, $width, $height) {
* An image object returned by image_load().
*
* @return
* TRUE or FALSE, based on success.
* TRUE on success, FALSE on failure.
*
* @see image_load()
* @see image_gd_desaturate()
......@@ -378,7 +410,7 @@ function image_load($file, $toolkit = FALSE) {
* original image file will be overwritten.
*
* @return
* TRUE or FALSE, based on success.
* TRUE on success, FALSE on failure.
*
* @see image_load()
* @see image_gd_save()
......
<?php
// $Id: install.core.inc,v 1.40 2010/10/15 03:31:28 webchick Exp $
/**
* @file
......@@ -289,7 +288,6 @@ function install_begin_request(&$install_state) {
// Initialize the database system. Note that the connection
// won't be initialized until it is actually requested.
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.
$task = install_verify_completed_task();
......@@ -572,6 +570,12 @@ function install_tasks($install_state) {
// Now add any tasks defined by the installation 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';
if (function_exists($function)) {
$result = $function($install_state);
......@@ -597,7 +601,7 @@ function install_tasks($install_state) {
// Allow the installation profile to modify the full list of tasks.
if (!empty($install_state['parameters']['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;
$function = $install_state['parameters']['profile'] . '_install_tasks_alter';
if (function_exists($function)) {
......@@ -712,8 +716,10 @@ function install_display_output($output, $install_state) {
*
* @return
* 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
* in the same page request.
* If there are only requirement warnings, a themed status report is shown
* 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) {
// Check the installation requirements for Drupal and this profile.
......@@ -725,25 +731,33 @@ function install_verify_requirements(&$install_state) {
// Check the severity of the requirements reported.
$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']) {
drupal_set_title(st('Requirements problem'));
$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;
}
else {
// Throw an exception showing all unmet requirements.
// Throw an exception showing any unmet requirements.
$failures = array();
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) {
$failures[] = $requirement['title'] . ': ' . $requirement['value'] . "\n\n" . $requirement['description'];
}
}
if (!empty($failures)) {
throw new Exception(implode("\n\n", $failures));
}
}
}
}
/**
* Installation task; install the Drupal system module.
......@@ -850,14 +864,13 @@ function install_settings_form($form, &$form_state, &$install_state) {
drupal_set_title(st('Database configuration'));
$drivers = drupal_detect_database_types();
$drivers = drupal_get_database_types();
$drivers_keys = array_keys($drivers);
$form['driver'] = array(
'#type' => 'radios',
'#title' => st('Database type'),
'#required' => TRUE,
'#options' => $drivers,
'#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())),
);
......@@ -866,82 +879,35 @@ 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.');
}
// Database name.
$form['database'] = array(
'#type' => 'textfield',
'#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,
);
// Add driver specific configuration options.
foreach ($drivers as $key => $driver) {
$form['driver']['#options'][$key] = $driver->name();
$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.
$db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_';
$form['advanced_options']['db_prefix'] = array(
'#type' => 'textfield',
'#title' => st('Table prefix'),
'#default_value' => '',
'#size' => 45,
'#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)),
$form['settings'][$key] = $driver->getFormOptions($database);
$form['settings'][$key]['#prefix'] = '<h2 class="js-hide">' . st('@driver_name settings', array('@driver_name' => $driver->name())) . '</h2>';
$form['settings'][$key]['#type'] = 'container';
$form['settings'][$key]['#tree'] = TRUE;
$form['settings'][$key]['advanced_options']['#parents'] = array($key);
$form['settings'][$key]['#states'] = array(
'visible' => array(
':input[name=driver]' => array('value' => $key),
)
);
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['save'] = array(
'#type' => 'submit',
'#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['settings_file'] = array('#type' => 'value', '#value' => $settings_file);
$form['_database'] = array('#type' => 'value');
return $form;
}
......@@ -950,12 +916,17 @@ function install_settings_form($form, &$form_state, &$install_state) {
* Form API validate for install_settings form.
*/
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
// '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);
$errors = install_database_errors($form_state['values'], $form_state['values']['settings_file']);
$form_state['storage']['database'] = $database;
$errors = install_database_errors($database, $form_state['values']['settings_file']);
foreach ($errors as $name => $message) {
form_set_error($name, $message);
}
......@@ -967,22 +938,17 @@ function install_settings_form_validate($form, &$form_state) {
function install_database_errors($database, $settings_file) {
global $databases;
$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.
$database_types = drupal_detect_database_types();
$database_types = drupal_get_database_types();
$driver = $database['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 {
// Run driver specific validation
$errors += $database_types[$driver]->validateDatabaseSettings($database);
// Run tasks associated with the database type. Any errors are caught in the
// calling function.
$databases['default']['default'] = $database;
......@@ -991,13 +957,13 @@ function install_database_errors($database, $settings_file) {
Database::parseConnectionInfo();
try {
db_run_tasks($database['driver']);
db_run_tasks($driver);
}
catch (DatabaseTaskException $e) {
// 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
// them in the error array with standard numeric keys.
$errors[] = $e->getMessage();
$errors[$driver . '][0'] = $e->getMessage();
}
}
return $errors;
......@@ -1009,10 +975,9 @@ function install_database_errors($database, $settings_file) {
function install_settings_form_submit($form, &$form_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.
$settings['databases'] = array(
'value' => array('default' => array('default' => $database)),
'value' => array('default' => array('default' => $form_state['storage']['database'])),
'required' => TRUE,
);
$settings['drupal_hash_salt'] = array(
......@@ -1170,6 +1135,15 @@ function install_select_profile_form($form, &$form_state, $profile_files) {
function install_find_locales($profilename) {
$locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', array('recurse' => FALSE));
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;
}
......@@ -1195,8 +1169,8 @@ function install_select_locale(&$install_state) {
if (!empty($_POST['locale'])) {
foreach ($locales as $locale) {
if ($_POST['locale'] == $locale->name) {
$install_state['parameters']['locale'] = $locale->name;
if ($_POST['locale'] == $locale->langcode) {
$install_state['parameters']['locale'] = $locale->langcode;
return;
}
}
......@@ -1214,14 +1188,6 @@ function install_select_locale(&$install_state) {
$output = '<p>Follow these steps to translate Drupal into your language:</p>';
$output .= '<ol>';
$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:
<pre>
/profiles/' . $profilename . '/translations/
......@@ -1287,16 +1253,15 @@ function install_select_locale_form($form, &$form_state, $locales, $profilename)
include_once DRUPAL_ROOT . '/includes/iso.inc';
$languages = _locale_get_predefined_list();
foreach ($locales as $locale) {
// Try to use verbose locale name.
$name = $locale->name;
$name = $locale->langcode;
if (isset($languages[$name])) {
$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',
'#return_value' => $locale->name,
'#default_value' => $locale->name == 'en' ? 'en' : '',
'#title' => $name . ($locale->name == 'en' ? ' ' . st('(built-in)') : ''),
'#return_value' => $locale->langcode,
'#default_value' => $locale->langcode == 'en' ? 'en' : '',
'#title' => $name . ($locale->langcode == 'en' ? ' ' . st('(built-in)') : ''),
'#parents' => array('locale')
);
}
......@@ -1341,7 +1306,7 @@ function install_already_done_error() {
*/
function install_load_profile(&$install_state) {
$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;
$install_state['profile_info'] = install_profile_info($install_state['parameters']['profile'], $install_state['parameters']['locale']);
}
......@@ -1425,8 +1390,19 @@ function install_profile_modules(&$install_state) {
function install_import_locales(&$install_state) {
include_once DRUPAL_ROOT . '/includes/locale.inc';
$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.
$batch = locale_batch_by_language($install_locale, NULL);
if (!empty($batch)) {
......@@ -1448,24 +1424,21 @@ function install_import_locales(&$install_state) {
* The form API definition for the site configuration form.
*/
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'));
// Warn about settings.php permissions risk
$settings_dir = './' . conf_path();
$settings_dir = conf_path();
$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');
}
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');
// Add JavaScript time zone detection.
......@@ -1763,7 +1736,7 @@ function _install_configure_form($form, &$form_state, &$install_state) {
$countries = country_get_list();
$form['server_settings']['site_default_country'] = array(
'#type' => 'select',
'#title' => t('Default country'),
'#title' => st('Default country'),
'#empty_value' => '',
'#default_value' => variable_get('site_default_country', NULL),
'#options' => $countries,
......@@ -1852,7 +1825,7 @@ function install_configure_form_submit($form, &$form_state) {
// We precreated user 1 with placeholder values. Let's save the real values.
$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));
// Load global $user and perform final login tasks.
$user = user_load(1);
......
<?php
// $Id: install.inc,v 1.142 2010/08/22 15:31:18 dries Exp $
/**
* Indicates that a module has not been installed yet.
......@@ -175,6 +174,9 @@ function drupal_set_installed_schema_version($module, $version) {
->fields(array('schema_version' => $version))
->condition('name', $module)
->execute();
// Reset the static cache of module schema versions.
drupal_get_installed_schema_version(NULL, TRUE);
}
/**
......@@ -196,7 +198,7 @@ function drupal_install_profile_distribution_name() {
// At all other times, we load the profile via standard methods.
else {
$profile = drupal_get_profile();
$info = install_profile_info($profile);
$info = system_get_info('module', $profile);
return $info['distribution_name'];
}
}
......@@ -227,6 +229,22 @@ function drupal_detect_baseurl($file = 'install.php') {
* An array of database types compiled into PHP.
*/
function drupal_detect_database_types() {
$databases = drupal_get_database_types();
foreach ($databases as $driver => $installer) {
$databases[$driver] = $installer->name();
}
return $databases;
}
/**
* Return all supported database installer objects that are compiled into PHP.
*
* @return
* An array of database installer objects compiled into PHP.
*/
function drupal_get_database_types() {
$databases = array();
// We define a driver as a directory in /includes/database that in turn
......@@ -235,7 +253,6 @@ function drupal_detect_database_types() {
// Because we have no registry yet, we need to also include the install.inc
// file for the driver explicitly.
require_once DRUPAL_ROOT . '/includes/database/database.inc';
spl_autoload_register('db_autoload');
foreach (file_scan_directory(DRUPAL_ROOT . '/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) {
if (file_exists($file->uri . '/database.inc') && file_exists($file->uri . '/install.inc')) {
$drivers[$file->filename] = $file->uri;
......@@ -243,10 +260,9 @@ function drupal_detect_database_types() {
}
foreach ($drivers as $driver => $file) {
$class = 'DatabaseTasks_' . $driver;
$installer = new $class();
$installer = db_installer_object($driver);
if ($installer->installable()) {
$databases[$driver] = $installer->name();
$databases[$driver] = $installer;
}
}
......@@ -276,9 +292,13 @@ abstract class DatabaseTasks {
* to call (optional) and any arguments to be passed to the function.
*/
protected $tasks = array(
array(
'function' => 'checkEngineVersion',
'arguments' => array(),
),
array(
'arguments' => array(
'CREATE TABLE drupal_install_test (id int NULL)',
'CREATE TABLE {drupal_install_test} (id int NULL)',
'Drupal can use CREATE TABLE database commands.',
'Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>',
TRUE,
......@@ -286,33 +306,34 @@ abstract class DatabaseTasks {
),
array(
'arguments' => array(
'INSERT INTO drupal_install_test (id) VALUES (1)',
'INSERT INTO {drupal_install_test} (id) VALUES (1)',
'Drupal can use INSERT database commands.',
'Failed to <strong>INSERT</strong> a value into a test table on your database server. We tried inserting a value with the command %query and the server reported the following error: %error.',
),
),
array(
'arguments' => array(
'UPDATE drupal_install_test SET id = 2',
'UPDATE {drupal_install_test} SET id = 2',
'Drupal can use UPDATE database commands.',
'Failed to <strong>UPDATE</strong> a value in a test table on your database server. We tried updating a value with the command %query and the server reported the following error: %error.',
),
),
array(
'arguments' => array(
'DELETE FROM drupal_install_test',
'DELETE FROM {drupal_install_test}',
'Drupal can use DELETE database commands.',
'Failed to <strong>DELETE</strong> a value from a test table on your database server. We tried deleting a value with the command %query and the server reported the following error: %error.',
),
),
array(
'arguments' => array(
'DROP TABLE drupal_install_test',
'DROP TABLE {drupal_install_test}',
'Drupal can use DROP TABLE database commands.',
'Failed to <strong>DROP</strong> a test table from your database server. We tried dropping a table with the command %query and the server reported the following error %error.',
),
),
);
/**
* Results from tasks.
*
......@@ -348,8 +369,22 @@ abstract class DatabaseTasks {
return $this->hasPdoDriver() && empty($this->error);
}
/**
* Return the human-readable name of the driver.
*/
abstract public function name();
/**
* Return the minimum required version of the engine.
*
* @return
* A version string. If not NULL, it will be checked against the version
* reported by the Database engine using version_compare().
*/
public function minimumVersion() {
return NULL;
}
/**
* Run database tasks and tests to see if Drupal can run on the database.
*/
......@@ -415,9 +450,127 @@ abstract class DatabaseTasks {
return !$fatal;
}
}
/**
* Check the engine version.
*/
protected function checkEngineVersion() {
if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) {
$this->fail(st("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
}
}
/**
* Return driver specific configuration options.
*
* @param $database
* An array of driver specific configuration options.
*
* @return
* The options form array.
*/
public function getFormOptions($database) {
$form['database'] = array(
'#type' => 'textfield',
'#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())),
);
$form['username'] = array(
'#type' => 'textfield',
'#title' => st('Database username'),
'#default_value' => empty($database['username']) ? '' : $database['username'],
'#required' => TRUE,
'#size' => 45,
);
$form['password'] = array(
'#type' => 'password',
'#title' => st('Database password'),
'#default_value' => empty($database['password']) ? '' : $database['password'],
'#required' => FALSE,
'#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."),
'#weight' => 10,
);
$profile = drupal_get_profile();
$db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_';
$form['advanced_options']['db_prefix'] = array(
'#type' => 'textfield',
'#title' => st('Table prefix'),
'#default_value' => '',
'#size' => 45,
'#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)),
'#weight' => 10,
);
$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.'),
);
$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.'),
);
return $form;
}
/**
* Validates driver specific configuration settings.
*
* Checks to ensure correct basic database settings and that a proper
* connection to the database can be established.
*
* @param $database
* An array of driver specific configuration options.
*
* @return
* An array of driver configuration errors, keyed by form element name.
*/
public function validateDatabaseSettings($database) {
$errors = array();
// Verify the table prefix.
if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) {
$errors[$database['driver'] . '][advanced_options][db_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']));
}
// Verify the database port.
if (!empty($database['port']) && !is_numeric($database['port'])) {
$errors[$database['driver'] . '][advanced_options][port'] = st('Database port must be a number.');
}
return $errors;
}
}
/**
* @class Exception class used to throw error if the DatabaseInstaller fails.
* Exception thrown if the database installer fails.
*/
class DatabaseTaskException extends Exception {
}
......@@ -524,7 +677,7 @@ function drupal_verify_profile($install_state) {
// Get a list of modules that exist in Drupal's assorted subdirectories.
$present_modules = array();
foreach (drupal_system_listing('/\.module$/', 'modules', 'name', 0) as $present_module) {
foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0) as $present_module) {
$present_modules[] = $present_module->name;
}
......@@ -581,51 +734,60 @@ function drupal_install_system() {
}
/**
* Calls the uninstall function and updates the system table for a given module.
* Uninstalls a given list of modules.
*
* @param $module_list
* The modules to uninstall.
* @param $uninstall_dependents
* If TRUE, the function will check that all modules which depend on the
* passed-in module list either are already uninstalled or contained in the
* list, and it will ensure that the modules are uninstalled in the correct
* order. This incurs a significant performance cost, so use FALSE if you
* know $module_list is already complete and in the correct order.
*
* @return
* FALSE if one or more dependent modules are missing from the list, TRUE
* otherwise.
*/
function drupal_uninstall_modules($module_list = array()) {
foreach ($module_list as $module) {
// First, retrieve all the module's menu paths from db.
drupal_load('module', $module);
$paths = module_invoke($module, 'menu');
function drupal_uninstall_modules($module_list = array(), $uninstall_dependents = TRUE) {
if ($uninstall_dependents) {
// Get all module data so we can find dependents and sort.
$module_data = system_rebuild_module_data();
// Create an associative array with weights as values.
$module_list = array_flip(array_values($module_list));
// Uninstall the module.
module_load_install($module);
module_invoke($module, 'uninstall');
drupal_uninstall_schema($module);
watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO);
// Now remove the menu links for all paths declared by this module.
if (!empty($paths)) {
$paths = array_keys($paths);
// Clean out the names of load functions.
foreach ($paths as $index => $path) {
$parts = explode('/', $path, MENU_MAX_PARTS);
foreach ($parts as $k => $part) {
if (preg_match('/^%[a-z_]*$/', $part)) {
$parts[$k] = '%';
$profile = drupal_get_profile();
while (list($module) = each($module_list)) {
if (!isset($module_data[$module]) || drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) {
// This module doesn't exist or is already uninstalled, skip it.
unset($module_list[$module]);
continue;
}
$module_list[$module] = $module_data[$module]->sort;
// If the module has any dependents which are not already uninstalled and
// not included in the passed-in list, abort. It is not safe to uninstall
// them automatically because uninstalling a module is a destructive
// operation.
foreach (array_keys($module_data[$module]->required_by) as $dependent) {
if (!isset($module_list[$dependent]) && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED && $dependent != $profile) {
return FALSE;
}
}
$paths[$index] = implode('/', $parts);
}
$result = db_select('menu_links')
->fields('menu_links')
->condition('router_path', $paths, 'IN')
->condition('external', 0)
->orderBy('depth')
->execute();
// Remove all such items. Starting from those with the greatest depth will
// minimize the amount of re-parenting done by menu_link_delete().
foreach ($result as $item) {
_menu_delete_item($item, TRUE);
}
// Sort the module list by pre-calculated weights.
asort($module_list);
$module_list = array_keys($module_list);
}
foreach ($module_list as $module) {
// Uninstall the module.
module_load_install($module);
module_invoke($module, 'uninstall');
drupal_uninstall_schema($module);
watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO);
drupal_set_installed_schema_version($module, SCHEMA_UNINSTALLED);
}
......@@ -633,6 +795,8 @@ function drupal_uninstall_modules($module_list = array()) {
// Call hook_module_uninstall to let other modules act
module_invoke_all('modules_uninstalled', $module_list);
}
return TRUE;
}
/**
......@@ -835,7 +999,6 @@ function drupal_install_fix_file($file, $mask, $message = TRUE) {
}
}
/**
* Send the user to a different installer page.
*
......@@ -852,13 +1015,80 @@ function install_goto($path) {
drupal_exit();
}
/**
* Returns the URL of the current script, with modified query parameters.
*
* This function can be called by low-level scripts (such as install.php and
* update.php) and returns the URL of the current script. Existing query
* parameters are preserved by default, but new ones can optionally be merged
* in.
*
* This function is used when the script must maintain certain query parameters
* over multiple page requests in order to work correctly. In such cases (for
* example, update.php, which requires the 'continue=1' parameter to remain in
* the URL throughout the update process if there are any requirement warnings
* that need to be bypassed), using this function to generate the URL for links
* to the next steps of the script ensures that the links will work correctly.
*
* @param $query
* (optional) An array of query parameters to merge in to the existing ones.
*
* @return
* The URL of the current script, with query parameters modified by the
* passed-in $query. The URL is not sanitized, so it still needs to be run
* through check_url() if it will be used as an HTML attribute value.
*
* @see drupal_requirements_url()
*/
function drupal_current_script_url($query = array()) {
$uri = $_SERVER['SCRIPT_NAME'];
$query = array_merge(drupal_get_query_parameters(), $query);
if (!empty($query)) {
$uri .= '?' . drupal_http_build_query($query);
}
return $uri;
}
/**
* Returns a URL for proceeding to the next page after a requirements problem.
*
* This function can be called by low-level scripts (such as install.php and
* update.php) and returns a URL that can be used to attempt to proceed to the
* next step of the script.
*
* @param $severity
* The severity of the requirements problem, as returned by
* drupal_requirements_severity().
*
* @return
* A URL for attempting to proceed to the next step of the script. The URL is
* not sanitized, so it still needs to be run through check_url() if it will
* be used as an HTML attribute value.
*
* @see drupal_current_script_url()
*/
function drupal_requirements_url($severity) {
$query = array();
// If there are no errors, only warnings, append 'continue=1' to the URL so
// the user can bypass this screen on the next page load.
if ($severity == REQUIREMENT_WARNING) {
$query['continue'] = 1;
}
return drupal_current_script_url($query);
}
/**
* Functional equivalent of t(), used when some systems are not available.
*
* Used during the install process, when database, theme, and localization
* system is possibly not yet available.
*
* Use t() if your code will never run during the Drupal installation phase.
* Use st() if your code will only run during installation and never any other
* time. Use get_t() if your code could run in either circumstance.
*
* @see t()
* @see get_t()
* @ingroup sanitization
*/
function st($string, array $args = array(), array $options = array()) {
......@@ -872,11 +1102,16 @@ function st($string, array $args = array(), array $options = array()) {
if (!isset($locale_strings)) {
$locale_strings = array();
if (isset($install_state['parameters']['profile']) && isset($install_state['parameters']['locale'])) {
$filename = 'profiles/' . $install_state['parameters']['profile'] . '/translations/' . $install_state['parameters']['locale'] . '.po';
if (file_exists(DRUPAL_ROOT . '/' . $filename)) {
// If the given locale was selected, there should be at least one .po file
// with its name ending in {$install_state['parameters']['locale']}.po
// This might or might not be the entire filename. It is also possible
// that multiple files end with the same extension, even if unlikely.
$po_files = file_scan_directory('./profiles/' . $install_state['parameters']['profile'] . '/translations', '/'. $install_state['parameters']['locale'] .'\.po$/', array('recurse' => FALSE));
if (count($po_files)) {
require_once DRUPAL_ROOT . '/includes/locale.inc';
$file = (object) array('uri' => $filename);
_locale_import_read_po('mem-store', $file);
foreach ($po_files as $po_file) {
_locale_import_read_po('mem-store', $po_file);
}
$locale_strings = _locale_import_one_string('mem-report');
}
}
......@@ -997,6 +1232,10 @@ function drupal_check_module($module) {
* installed, to be shown throughout the installation process. Defaults to
* 'Drupal'.
*
* Note that this function does an expensive file system scan to get info file
* information for dependencies. If you only need information from the info
* file itself, use system_get_info().
*
* Example of .info file:
* @code
* name = Minimal
......@@ -1005,10 +1244,11 @@ function drupal_check_module($module) {
* dependencies[] = dblog
* @endcode
*
* @param profile
* @param $profile
* Name of profile.
* @param locale
* @param $locale
* Name of locale used (if any).
*
* @return
* The info array.
*/
......@@ -1050,8 +1290,18 @@ function install_profile_info($profile, $locale = 'en') {
* encoding.
*/
function db_run_tasks($driver) {
$task_class = 'DatabaseTasks_' . $driver;
$DatabaseTasks = new $task_class();
$DatabaseTasks->runTasks();
db_installer_object($driver)->runTasks();
return TRUE;
}
/**
* Returns a database installer object.
*
* @param $driver
* The name of the driver.
*/
function db_installer_object($driver) {
Database::loadDriverFile($driver, array('install.inc'));
$task_class = 'DatabaseTasks_' . $driver;
return new $task_class();
}
<?php
// $Id: iso.inc,v 1.12 2010/10/09 18:20:43 webchick Exp $
/**
* @file
......@@ -75,6 +74,7 @@ function _country_get_predefined_list() {
'CO' => $t('Colombia'),
'CR' => $t('Costa Rica'),
'CU' => $t('Cuba'),
'CW' => $t('Curaçao'),
'CV' => $t('Cape Verde'),
'CX' => $t('Christmas Island'),
'CY' => $t('Cyprus'),
......@@ -241,7 +241,7 @@ function _country_get_predefined_list() {
'TH' => $t('Thailand'),
'TJ' => $t('Tajikistan'),
'TK' => $t('Tokelau'),
'TL' => $t('East Timor'),
'TL' => $t('Timor-Leste'),
'TM' => $t('Turkmenistan'),
'TN' => $t('Tunisia'),
'TO' => $t('Tonga'),
......