summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--includes/common.inc13
-rw-r--r--includes/database/database.inc57
-rw-r--r--includes/database/mysql/query.inc8
-rw-r--r--includes/database/pgsql/query.inc4
-rw-r--r--includes/database/query.inc22
-rw-r--r--includes/database/select.inc5
-rw-r--r--includes/database/sqlite/query.inc8
-rw-r--r--includes/menu.inc2
-rw-r--r--modules/comment/comment.module8
-rw-r--r--modules/field/field.attach.inc25
-rw-r--r--modules/node/node.install34
-rw-r--r--modules/node/node.module10
-rw-r--r--modules/openid/openid.module10
-rw-r--r--modules/openid/openid.test12
-rw-r--r--modules/simpletest/tests/database_test.test21
-rw-r--r--modules/simpletest/tests/menu.test58
-rw-r--r--modules/system/system.api.php24
-rw-r--r--modules/taxonomy/taxonomy.module4
-rw-r--r--modules/user/user.module119
19 files changed, 295 insertions, 149 deletions
diff --git a/includes/common.inc b/includes/common.inc
index 4168ec9a2..5dadb4d16 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -7496,8 +7496,15 @@ function entity_get_controller($entity_type) {
* The type of entity, i.e. 'node', 'user'.
* @param $entities
* The entity objects which are being prepared for view, keyed by object ID.
+ * @param $langcode
+ * (optional) A language code to be used for rendering. Defaults to the global
+ * content language of the current request.
*/
-function entity_prepare_view($entity_type, $entities) {
+function entity_prepare_view($entity_type, $entities, $langcode = NULL) {
+ if (!isset($langcode)) {
+ $langcode = $GLOBALS['language_content']->language;
+ }
+
// To ensure hooks are only run once per entity, check for an
// entity_view_prepared flag and only process items without it.
// @todo: resolve this more generally for both entity and field level hooks.
@@ -7513,7 +7520,7 @@ function entity_prepare_view($entity_type, $entities) {
}
if (!empty($prepare)) {
- module_invoke_all('entity_prepare_view', $prepare, $entity_type);
+ module_invoke_all('entity_prepare_view', $prepare, $entity_type, $langcode);
}
}
@@ -7588,7 +7595,7 @@ function entity_label($entity_type, $entity) {
$label = FALSE;
$info = entity_get_info($entity_type);
if (isset($info['label callback']) && function_exists($info['label callback'])) {
- $label = $info['label callback']($entity);
+ $label = $info['label callback']($entity, $entity_type);
}
elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) {
$label = $entity->{$info['entity keys']['label']};
diff --git a/includes/database/database.inc b/includes/database/database.inc
index 4539b37a7..4cc1a33d7 100644
--- a/includes/database/database.inc
+++ b/includes/database/database.inc
@@ -541,6 +541,63 @@ abstract class DatabaseConnection extends PDO {
}
/**
+ * Flatten an array of query comments into a single comment string.
+ *
+ * The comment string will be sanitized to avoid SQL injection attacks.
+ *
+ * @param $comments
+ * An array of query comment strings.
+ *
+ * @return
+ * A sanitized comment string.
+ */
+ public function makeComment($comments) {
+ if (empty($comments))
+ return '';
+
+ // Flatten the array of comments.
+ $comment = implode('; ', $comments);
+
+ // Sanitize the comment string so as to avoid SQL injection attacks.
+ return '/* ' . $this->filterComment($comment) . ' */ ';
+ }
+
+ /**
+ * Sanitize a query comment string.
+ *
+ * Ensure a query comment does not include strings such as "* /" that might
+ * terminate the comment early. This avoids SQL injection attacks via the
+ * query comment. The comment strings in this example are separated by a
+ * space to avoid PHP parse errors.
+ *
+ * For example, the comment:
+ * @code
+ * db_update('example')
+ * ->condition('id', $id)
+ * ->fields(array('field2' => 10))
+ * ->comment('Exploit * / DROP TABLE node; --')
+ * ->execute()
+ * @endcode
+ *
+ * Would result in the following SQL statement being generated:
+ * @code
+ * "/ * Exploit * / DROP TABLE node; -- * / UPDATE example SET field2=..."
+ * @endcode
+ *
+ * Unless the comment is sanitised first, the SQL server would drop the
+ * node table and ignore the rest of the SQL statement.
+ *
+ * @param $comment
+ * A query comment string.
+ *
+ * @return
+ * A sanitized version of the query comment string.
+ */
+ protected function filterComment($comment = '') {
+ return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment);
+ }
+
+ /**
* Executes a query string against the database.
*
* This method provides a central handler for the actual execution of every
diff --git a/includes/database/mysql/query.inc b/includes/database/mysql/query.inc
index f7fb52f04..888b6a5a4 100644
--- a/includes/database/mysql/query.inc
+++ b/includes/database/mysql/query.inc
@@ -42,8 +42,8 @@ class InsertQuery_mysql 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);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
@@ -92,8 +92,8 @@ class TruncateQuery_mysql extends TruncateQuery {
// not transactional, and result in an implicit COMMIT. When we are in a
// transaction, fallback to the slower, but transactional, DELETE.
if ($this->connection->inTransaction()) {
- // Create a comments string to prepend to the query.
- $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : '';
+ // Create a comment string to prepend to the query.
+ $comments = $this->connection->makeComment($this->comments);
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}';
}
else {
diff --git a/includes/database/pgsql/query.inc b/includes/database/pgsql/query.inc
index fe7909e17..f3783a9ca 100644
--- a/includes/database/pgsql/query.inc
+++ b/includes/database/pgsql/query.inc
@@ -103,8 +103,8 @@ class InsertQuery_pgsql 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);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
diff --git a/includes/database/query.inc b/includes/database/query.inc
index 7f3e9ff85..23b652f9b 100644
--- a/includes/database/query.inc
+++ b/includes/database/query.inc
@@ -361,6 +361,9 @@ abstract class Query implements QueryPlaceholderInterface {
* for easier debugging and allows you to more easily find where a query
* with a performance problem is being generated.
*
+ * The comment string will be sanitized to remove * / and other characters
+ * that may terminate the string early so as to avoid SQL injection attacks.
+ *
* @param $comment
* The comment string to be inserted into the query.
*
@@ -623,9 +626,8 @@ class InsertQuery extends Query {
* The prepared statement.
*/
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);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
@@ -815,9 +817,8 @@ class DeleteQuery extends Query implements QueryConditionInterface {
* The prepared statement.
*/
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);
$query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
@@ -884,8 +885,8 @@ class TruncateQuery extends Query {
* The prepared statement.
*/
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 . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} ';
}
@@ -1111,9 +1112,8 @@ class UpdateQuery extends Query implements QueryConditionInterface {
* The prepared statement.
*/
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);
// Expressions take priority over literal fields, so we process those first
// and remove any literal fields that conflict.
diff --git a/includes/database/select.inc b/includes/database/select.inc
index 6e4b0dc48..716c2fc3d 100644
--- a/includes/database/select.inc
+++ b/includes/database/select.inc
@@ -1439,9 +1439,8 @@ class SelectQuery extends Query implements SelectQueryInterface {
}
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);
// SELECT
$query = $comments . 'SELECT ';
diff --git a/includes/database/sqlite/query.inc b/includes/database/sqlite/query.inc
index a176ed649..6b8a72f2a 100644
--- a/includes/database/sqlite/query.inc
+++ b/includes/database/sqlite/query.inc
@@ -32,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), '?');
@@ -148,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) . '} ';
}
diff --git a/includes/menu.inc b/includes/menu.inc
index 2a8c80c41..cfd35c794 100644
--- a/includes/menu.inc
+++ b/includes/menu.inc
@@ -1044,7 +1044,7 @@ function menu_tree_output($tree) {
}
// Allow menu-specific theme overrides.
- $element['#theme'] = 'menu_link__' . $data['link']['menu_name'];
+ $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
$element['#attributes']['class'] = $class;
$element['#title'] = $data['link']['title'];
$element['#href'] = $data['link']['href'];
diff --git a/modules/comment/comment.module b/modules/comment/comment.module
index 836f2fed8..499adc847 100644
--- a/modules/comment/comment.module
+++ b/modules/comment/comment.module
@@ -986,8 +986,8 @@ function comment_build_content($comment, $node, $view_mode = 'full', $langcode =
$comment->content = array();
// Build fields content.
- field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode);
- entity_prepare_view('comment', array($comment->cid => $comment));
+ field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
+ entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
$comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
$comment->content['links'] = array(
@@ -1089,8 +1089,8 @@ function comment_links($comment, $node) {
* An array in the format expected by drupal_render().
*/
function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
- field_attach_prepare_view('comment', $comments, $view_mode);
- entity_prepare_view('comment', $comments);
+ field_attach_prepare_view('comment', $comments, $view_mode, $langcode);
+ entity_prepare_view('comment', $comments, $langcode);
$build = array(
'#sorted' => TRUE,
diff --git a/modules/field/field.attach.inc b/modules/field/field.attach.inc
index 82aabc054..3b15c76c8 100644
--- a/modules/field/field.attach.inc
+++ b/modules/field/field.attach.inc
@@ -257,9 +257,9 @@ function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $opti
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
- * - 'language': A language code or an array of language codes keyed by field
- * name. It will be used to narrow down to a single value the available
- * languages to act on.
+ * - 'language': A language code or an array of arrays of language codes keyed
+ * by entity id and field name. It will be used to narrow down to a single
+ * value the available languages to act on.
*
* @return
* An array of returned values keyed by entity id.
@@ -311,7 +311,8 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b =
// Unless a language suggestion is provided we iterate on all the
// available languages.
$available_languages = field_available_languages($entity_type, $field);
- $languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
+ $language = !empty($options['language'][$id]) ? $options['language'][$id] : $options['language'];
+ $languages = _field_language_suggestion($available_languages, $language, $field_name);
foreach ($languages as $langcode) {
$grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
}
@@ -1074,8 +1075,13 @@ function field_attach_delete_revision($entity_type, $entity) {
* An array of entities, keyed by entity id.
* @param $view_mode
* View mode, e.g. 'full', 'teaser'...
+ * @param $langcode
+ * (Optional) The language the field values are to be shown in. If no language
+ * is provided the current language is used.
*/
-function field_attach_prepare_view($entity_type, $entities, $view_mode) {
+function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcode = NULL) {
+ $options = array('language' => array());
+
// To ensure hooks are only run once per entity, only process items without
// the _field_view_prepared flag.
// @todo: resolve this more generally for both entity and field level hooks.
@@ -1085,17 +1091,22 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode) {
// Add this entity to the items to be prepared.
$prepare[$id] = $entity;
+ // Determine the actual language to display for each field, given the
+ // languages available in the field data.
+ $options['language'][$id] = field_language($entity_type, $entity, NULL, $langcode);
+
// Mark this item as prepared.
$entity->_field_view_prepared = TRUE;
}
}
+ $null = NULL;
// First let the field types do their preparation.
- _field_invoke_multiple('prepare_view', $entity_type, $prepare);
+ _field_invoke_multiple('prepare_view', $entity_type, $prepare, $null, $null, $options);
// Then let the formatters do their own specific massaging.
// field_default_prepare_view() takes care of dispatching to the correct
// formatters according to the display settings for the view mode.
- _field_invoke_multiple_default('prepare_view', $entity_type, $prepare, $view_mode);
+ _field_invoke_multiple_default('prepare_view', $entity_type, $prepare, $view_mode, $null, $options);
}
/**
diff --git a/modules/node/node.install b/modules/node/node.install
index c5378dc85..14290e3ad 100644
--- a/modules/node/node.install
+++ b/modules/node/node.install
@@ -469,7 +469,26 @@ function node_update_dependencies() {
* @ingroup update-api-6.x-to-7.x
*/
function _update_7000_node_get_types() {
- return db_query('SELECT * FROM {node_type}')->fetchAllAssoc('type', PDO::FETCH_OBJ);
+ $node_types = db_query('SELECT * FROM {node_type}')->fetchAllAssoc('type', PDO::FETCH_OBJ);
+
+ // Create default settings for orphan nodes.
+ $all_types = db_query('SELECT DISTINCT type FROM {node}')->fetchCol();
+ $extra_types = array_diff($all_types, array_keys($node_types));
+
+ foreach ($extra_types as $type) {
+ $type_object = new stdClass;
+ $type_object->type = $type;
+
+ // In Drupal 6, whether you have a body field or not is a flag in the node
+ // type table. If it's enabled, nodes may or may not have an empty string
+ // for the bodies. As we can't detect what this setting should be in
+ // Drupal 7 without access to the Drupal 6 node type settings, we assume
+ // the default, which is to enable the body field.
+ $type_object->has_body = 1;
+ $type_object->body_label = 'Body';
+ $node_types[$type_object->type] = $type_object;
+ }
+ return $node_types;
}
/**
@@ -600,19 +619,6 @@ function node_update_7006(&$sandbox) {
// Get node type info, specifically the body field settings.
$node_types = _update_7000_node_get_types();
- // Create default settings for orphan nodes.
- $extra_types = db_query('SELECT DISTINCT type FROM {node} WHERE type NOT IN (:types)', array(':types' => array_keys($node_types)))->fetchCol();
- foreach ($extra_types as $type) {
- $type_object = new stdClass;
- $type_object->type = $type;
- // Always create a body. Querying node_revisions for a non-empty body
- // would skip creating body fields for types that have a body but
- // the nodes of that type so far had empty bodies.
- $type_object->has_body = 1;
- $type_object->body_label = 'Body';
- $node_types[$type_object->type] = $type_object;
- }
-
// Add body field instances for existing node types.
foreach ($node_types as $node_type) {
if ($node_type->has_body) {
diff --git a/modules/node/node.module b/modules/node/node.module
index 4f079edd7..524a57fa7 100644
--- a/modules/node/node.module
+++ b/modules/node/node.module
@@ -1350,15 +1350,15 @@ function node_build_content($node, $view_mode = 'full', $langcode = NULL) {
// The 'view' hook can be implemented to overwrite the default function
// to display nodes.
if (node_hook($node, 'view')) {
- $node = node_invoke($node, 'view', $view_mode);
+ $node = node_invoke($node, 'view', $view_mode, $langcode);
}
// Build fields content.
// In case of a multiple view, node_view_multiple() already ran the
// 'prepare_view' step. An internal flag prevents the operation from running
// twice.
- field_attach_prepare_view('node', array($node->nid => $node), $view_mode);
- entity_prepare_view('node', array($node->nid => $node));
+ field_attach_prepare_view('node', array($node->nid => $node), $view_mode, $langcode);
+ entity_prepare_view('node', array($node->nid => $node), $langcode);
$node->content += field_attach_view('node', $node, $view_mode, $langcode);
// Always display a read more link on teasers because we have no way
@@ -2513,8 +2513,8 @@ function node_feed($nids = FALSE, $channel = array()) {
* An array in the format expected by drupal_render().
*/
function node_view_multiple($nodes, $view_mode = 'teaser', $weight = 0, $langcode = NULL) {
- field_attach_prepare_view('node', $nodes, $view_mode);
- entity_prepare_view('node', $nodes);
+ field_attach_prepare_view('node', $nodes, $view_mode, $langcode);
+ entity_prepare_view('node', $nodes, $langcode);
$build = array();
foreach ($nodes as $node) {
$build['nodes'][$node->nid] = node_view($node, $view_mode, $langcode);
diff --git a/modules/openid/openid.module b/modules/openid/openid.module
index 6d4b1d7ff..7673de886 100644
--- a/modules/openid/openid.module
+++ b/modules/openid/openid.module
@@ -341,18 +341,14 @@ function openid_complete($response = array()) {
$response['openid.claimed_id'] = $service['claimed_id'];
}
elseif ($service['version'] == 2) {
- // Returned Claimed Identifier could contain unique fragment
- // identifier to allow identifier recycling so we need to preserve
- // it in the response.
- $response_claimed_id = openid_normalize($response['openid.claimed_id']);
-
+ $response['openid.claimed_id'] = openid_normalize($response['openid.claimed_id']);
// OpenID Authentication, section 11.2:
// If the returned Claimed Identifier is different from the one sent
// to the OpenID Provider, we need to do discovery on the returned
// identififer to make sure that the provider is authorized to
// respond on behalf of this.
- if ($response_claimed_id != $claimed_id) {
- $services = openid_discovery($response_claimed_id);
+ if ($response['openid.claimed_id'] != $claimed_id) {
+ $services = openid_discovery($response['openid.claimed_id']);
$uris = array();
foreach ($services as $discovered_service) {
if (in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) || in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) {
diff --git a/modules/openid/openid.test b/modules/openid/openid.test
index 09632ba14..202a8355e 100644
--- a/modules/openid/openid.test
+++ b/modules/openid/openid.test
@@ -89,12 +89,12 @@ class OpenIDFunctionalTestCase extends OpenIDWebTestCase {
// Identifier is the URL of an XRDS document containing an OP Identifier
// Element. The Relying Party sends the special value
// "http://specs.openid.net/auth/2.0/identifier_select" as Claimed
- // Identifier. The OpenID Provider responds with the actual identifier
- // including the fragment.
- $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE, 'fragment' => $this->randomName()));
- // Tell openid_test.module to respond with this identifier. We test if
- // openid_complete() processes it right.
- variable_set('openid_test_response', array('openid.claimed_id' => $identity));
+ // Identifier. The OpenID Provider responds with the actual identifier.
+ $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE));
+ // Tell openid_test.module to respond with this identifier. The URL scheme
+ // is stripped in order to test that the returned identifier is normalized in
+ // openid_complete().
+ variable_set('openid_test_response', array('openid.claimed_id' => preg_replace('@^https?://@', '', $identity)));
$this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, 'http://specs.openid.net/auth/2.0/identifier_select', $identity);
variable_set('openid_test_response', array());
diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test
index 231355ceb..c22d1fc5d 100644
--- a/modules/simpletest/tests/database_test.test
+++ b/modules/simpletest/tests/database_test.test
@@ -1325,6 +1325,27 @@ class DatabaseSelectTestCase extends DatabaseTestCase {
}
/**
+ * Test query COMMENT system against vulnerabilities.
+ */
+ function testVulnerableComment() {
+ $query = db_select('test')->comment('Testing query comments */ SELECT nid FROM {node}; --');
+ $name_field = $query->addField('test', 'name');
+ $age_field = $query->addField('test', 'age', 'age');
+ $result = $query->execute();
+
+ $num_records = 0;
+ foreach ($result as $record) {
+ $num_records++;
+ }
+
+ $query = (string)$query;
+ $expected = "/* Testing query comments SELECT nid FROM {node}; -- */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test";
+
+ $this->assertEqual($num_records, 4, t('Returned the correct number of rows.'));
+ $this->assertEqual($query, $expected, t('The flattened query contains the sanitised comment string.'));
+ }
+
+ /**
* Test basic conditionals on SELECT statements.
*/
function testSimpleSelectConditional() {
diff --git a/modules/simpletest/tests/menu.test b/modules/simpletest/tests/menu.test
index 2578bebc0..5642fcee0 100644
--- a/modules/simpletest/tests/menu.test
+++ b/modules/simpletest/tests/menu.test
@@ -857,6 +857,64 @@ class MenuTreeDataTestCase extends DrupalUnitTestCase {
}
/**
+ * Menu tree output related tests.
+ */
+class MenuTreeOutputTestCase extends DrupalWebTestCase {
+ /**
+ * Dummy link structure acceptable for menu_tree_output().
+ */
+ var $tree_data = array(
+ '1'=> array(
+ 'link' => array( 'menu_name' => 'main-menu', 'mlid' => 1, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>'')) ),
+ 'below' => array(
+ '2' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 2, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')) ),
+ 'below' => array(
+ '3' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 3, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')) ),
+ 'below' => array() ),
+ '4' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 4, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')) ),
+ 'below' => array() )
+ )
+ )
+ )
+ ),
+ '5' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 5, 'hidden'=>1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access'=>1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ),
+ '6' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 6, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access'=>0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ),
+ '7' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 7, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access'=>1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) )
+ );
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Menu tree output',
+ 'description' => 'Tests menu tree output functions.',
+ 'group' => 'Menu',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ }
+
+ /**
+ * Validate the generation of a proper menu tree output.
+ */
+ function testMenuTreeData() {
+ $output = menu_tree_output($this->tree_data);
+
+ // Validate that the - in main-menu is changed into an underscore
+ $this->assertEqual( $output['1']['#theme'], 'menu_link__main_menu', t('Hyphen is changed to a dash on menu_link'));
+ $this->assertEqual( $output['#theme_wrappers'][0], 'menu_tree__main_menu', t('Hyphen is changed to a dash on menu_tree wrapper'));
+ // Looking for child items in the data
+ $this->assertEqual( $output['1']['#below']['2']['#href'], 'a/b', t('Checking the href on a child item'));
+ $this->assertTrue( in_array('active-trail',$output['1']['#below']['2']['#attributes']['class']) , t('Checking the active trail class'));
+ // Validate that the hidden and no access items are missing
+ $this->assertFalse( isset($output['5']), t('Hidden item should be missing'));
+ $this->assertFalse( isset($output['6']), t('False access should be missing'));
+ // Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are skipped and 7 still included
+ $this->assertTrue( isset($output['7']), t('Item after hidden items is present'));
+ }
+}
+
+/**
* Menu breadcrumbs related tests.
*/
class MenuBreadcrumbTestCase extends DrupalWebTestCase {
diff --git a/modules/system/system.api.php b/modules/system/system.api.php
index 1b295b4f4..c7db6f1dd 100644
--- a/modules/system/system.api.php
+++ b/modules/system/system.api.php
@@ -87,16 +87,16 @@ function hook_hook_info_alter(&$hooks) {
* - uri callback: A function taking an entity as argument and returning the
* uri elements of the entity, e.g. 'path' and 'options'. The actual entity
* uri can be constructed by passing these elements to url().
- * - label callback: (optional) A function taking an entity as argument and
- * returning the label of the entity. The entity label is the main string
- * associated with an entity; for example, the title of a node or the
- * subject of a comment. If there is an entity object property that defines
- * the label, use the 'label' element of the 'entity keys' return
- * value component to provide this information (see below). If more complex
- * logic is needed to determine the label of an entity, you can instead
- * specify a callback function here, which will be called to determine the
- * entity label. See also the entity_label() function, which implements this
- * logic.
+ * - label callback: (optional) A function taking an entity and an entity type
+ * as arguments and returning the label of the entity. The entity label is
+ * the main string associated with an entity; for example, the title of a
+ * node or the subject of a comment. If there is an entity object property
+ * that defines the label, use the 'label' element of the 'entity keys'
+ * return value component to provide this information (see below). If more
+ * complex logic is needed to determine the label of an entity, you can
+ * instead specify a callback function here, which will be called to
+ * determine the entity label. See also the entity_label() function, which
+ * implements this logic.
* - fieldable: Set to TRUE if you want your entity type to accept fields
* being attached to it.
* - translation: An associative array of modules registered as field
@@ -502,8 +502,10 @@ function hook_admin_paths_alter(&$paths) {
* The entities keyed by entity ID.
* @param $type
* The type of entities being loaded (i.e. node, user, comment).
+ * @param $langcode
+ * The language to display the entity in.
*/
-function hook_entity_prepare_view($entities, $type) {
+function hook_entity_prepare_view($entities, $type, $langcode) {
// Load a specific node into the user object for later theming.
if ($type == 'user') {
$nodes = mymodule_get_user_nodes(array_keys($entities));
diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module
index 5fb41e61f..50d2fd608 100644
--- a/modules/taxonomy/taxonomy.module
+++ b/modules/taxonomy/taxonomy.module
@@ -682,8 +682,8 @@ function taxonomy_term_view($term, $view_mode = 'full', $langcode = NULL) {
$langcode = $GLOBALS['language_content']->language;
}
- field_attach_prepare_view('taxonomy_term', array($term->tid => $term), $view_mode);
- entity_prepare_view('taxonomy_term', array($term->tid => $term));
+ field_attach_prepare_view('taxonomy_term', array($term->tid => $term), $view_mode, $langcode);
+ entity_prepare_view('taxonomy_term', array($term->tid => $term), $langcode);
$build = array(
'#theme' => 'taxonomy_term',
diff --git a/modules/user/user.module b/modules/user/user.module
index 204155a4c..90d313b10 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -442,27 +442,18 @@ function user_save($account, $edit = array(), $category = 'account') {
user_module_invoke('presave', $edit, $account, $category);
// Invoke presave operations of Field Attach API and Entity API. Those APIs
- // require a fully-fledged (and updated) entity object, so $edit is not
- // necessarily sufficient, as it technically contains submitted form values
- // only. Therefore, we need to clone $account into a new object and copy any
- // new property values of $edit into it.
- $account_updated = clone $account;
+ // require a fully-fledged and updated entity object. Therefore, we need to
+ // copy any new property values of $edit into it.
foreach ($edit as $key => $value) {
- $account_updated->$key = $value;
- }
- field_attach_presave('user', $account_updated);
- module_invoke_all('entity_presave', $account_updated, 'user');
- // Update $edit with any changes modules might have applied to the account.
- foreach ($account_updated as $key => $value) {
- if (!property_exists($account, $key) || $value !== $account->$key) {
- $edit[$key] = $value;
- }
+ $account->$key = $value;
}
+ field_attach_presave('user', $account);
+ module_invoke_all('entity_presave', $account, 'user');
if (is_object($account) && !$account->is_new) {
// Process picture uploads.
- if (!$delete_previous_picture = empty($edit['picture']->fid)) {
- $picture = $edit['picture'];
+ if (!empty($account->picture->fid) && (!isset($account->original->picture->fid) || $account->picture->fid != $account->original->picture->fid)) {
+ $picture = $account->picture;
// If the picture is a temporary file move it to its final location and
// make it permanent.
if (!$picture->status) {
@@ -475,26 +466,23 @@ function user_save($account, $edit = array(), $category = 'account') {
// Move the temporary file into the final location.
if ($picture = file_move($picture, $destination, FILE_EXISTS_RENAME)) {
- $delete_previous_picture = TRUE;
$picture->status = FILE_STATUS_PERMANENT;
- $edit['picture'] = file_save($picture);
+ $account->picture = file_save($picture);
file_usage_add($picture, 'user', 'user', $account->uid);
}
}
+ // Delete the previous picture if it was deleted or replaced.
+ if (!empty($account->original->picture->fid)) {
+ file_usage_delete($account->original->picture, 'user', 'user', $account->uid);
+ file_delete($account->original->picture);
+ }
}
-
- // Delete the previous picture if it was deleted or replaced.
- if ($delete_previous_picture && !empty($account->picture->fid)) {
- file_usage_delete($account->picture, 'user', 'user', $account->uid);
- file_delete($account->picture);
- }
-
- $edit['picture'] = empty($edit['picture']->fid) ? 0 : $edit['picture']->fid;
+ $account->picture = empty($account->picture->fid) ? 0 : $account->picture->fid;
// Do not allow 'uid' to be changed.
- $edit['uid'] = $account->uid;
+ $account->uid = $account->original->uid;
// Save changes to the user table.
- $success = drupal_write_record('users', $edit, 'uid');
+ $success = drupal_write_record('users', $account, 'uid');
if ($success === FALSE) {
// The query failed - better to abort the save than risk further
// data loss.
@@ -502,13 +490,13 @@ function user_save($account, $edit = array(), $category = 'account') {
}
// Reload user roles if provided.
- if (isset($edit['roles']) && is_array($edit['roles'])) {
+ if ($account->roles != $account->original->roles) {
db_delete('users_roles')
->condition('uid', $account->uid)
->execute();
$query = db_insert('users_roles')->fields(array('uid', 'rid'));
- foreach (array_keys($edit['roles']) as $rid) {
+ foreach (array_keys($account->roles) as $rid) {
if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
$query->values(array(
'uid' => $account->uid,
@@ -520,13 +508,13 @@ function user_save($account, $edit = array(), $category = 'account') {
}
// Delete a blocked user's sessions to kick them if they are online.
- if (isset($edit['status']) && $edit['status'] == 0) {
+ if ($account->original->status != $account->status && $account->status == 0) {
drupal_session_destroy_uid($account->uid);
}
// If the password changed, delete all open sessions and recreate
// the current one.
- if (!empty($edit['pass'])) {
+ if ($account->pass != $account->original->pass) {
drupal_session_destroy_uid($account->uid);
if ($account->uid == $GLOBALS['user']->uid) {
drupal_session_regenerate();
@@ -534,60 +522,56 @@ function user_save($account, $edit = array(), $category = 'account') {
}
// Save Field data.
- $entity = (object) $edit;
- field_attach_update('user', $entity);
-
- // Refresh user object.
- $user = user_load($account->uid, TRUE);
- // Make the original, unchanged user account available to update hooks.
- if (isset($account->original)) {
- $user->original = $account->original;
- }
+ field_attach_update('user', $account);
// Send emails after we have the new user object.
- if (isset($edit['status']) && $edit['status'] != $account->status) {
+ if ($account->status != $account->original->status) {
// The user's status is changing; conditionally send notification email.
- $op = $edit['status'] == 1 ? 'status_activated' : 'status_blocked';
- _user_mail_notify($op, $user);
+ $op = $account->status == 1 ? 'status_activated' : 'status_blocked';
+ _user_mail_notify($op, $account);
}
- user_module_invoke('update', $edit, $user, $category);
- module_invoke_all('entity_update', $user, 'user');
- unset($user->original);
+ // Update $edit with any interim changes to $account.
+ foreach ($account as $key => $value) {
+ if (!property_exists($account->original, $key) || $value !== $account->original->$key) {
+ $edit[$key] = $value;
+ }
+ }
+ user_module_invoke('update', $edit, $account, $category);
+ module_invoke_all('entity_update', $account, 'user');
}
else {
// Allow 'uid' to be set by the caller. There is no danger of writing an
// existing user as drupal_write_record will do an INSERT.
- if (empty($edit['uid'])) {
- $edit['uid'] = db_next_id(db_query('SELECT MAX(uid) FROM {users}')->fetchField());
+ if (empty($account->uid)) {
+ $account->uid = db_next_id(db_query('SELECT MAX(uid) FROM {users}')->fetchField());
}
// Allow 'created' to be set by the caller.
- if (!isset($edit['created'])) {
- $edit['created'] = REQUEST_TIME;
+ if (!isset($account->created)) {
+ $account->created = REQUEST_TIME;
}
- $success = drupal_write_record('users', $edit);
+ $success = drupal_write_record('users', $account);
if ($success === FALSE) {
// On a failed INSERT some other existing user's uid may be returned.
// We must abort to avoid overwriting their account.
return FALSE;
}
- // Build a stub user object.
- $user = (object) $edit;
- $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
+ // Make sure $account is properly initialized.
+ $account->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
- field_attach_insert('user', $user);
-
- user_module_invoke('insert', $edit, $user, $category);
- module_invoke_all('entity_insert', $user, 'user');
+ field_attach_insert('user', $account);
+ $edit = (array) $account;
+ user_module_invoke('insert', $edit, $account, $category);
+ module_invoke_all('entity_insert', $account, 'user');
// Save user roles.
- if (isset($edit['roles']) && is_array($edit['roles'])) {
+ if (count($account->roles) > 1) {
$query = db_insert('users_roles')->fields(array('uid', 'rid'));
- foreach (array_keys($edit['roles']) as $rid) {
+ foreach (array_keys($account->roles) as $rid) {
if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
$query->values(array(
- 'uid' => $edit['uid'],
+ 'uid' => $account->uid,
'rid' => $rid,
));
}
@@ -595,8 +579,13 @@ function user_save($account, $edit = array(), $category = 'account') {
$query->execute();
}
}
+ // Clear internal properties.
+ unset($account->is_new);
+ unset($account->original);
+ // Clear the static loading cache.
+ entity_get_controller('user')->resetCache(array($account->uid));
- return $user;
+ return $account;
}
catch (Exception $e) {
$transaction->rollback();
@@ -2523,8 +2512,8 @@ function user_build_content($account, $view_mode = 'full', $langcode = NULL) {
$account->content = array();
// Build fields content.
- field_attach_prepare_view('user', array($account->uid => $account), $view_mode);
- entity_prepare_view('user', array($account->uid => $account));
+ field_attach_prepare_view('user', array($account->uid => $account), $view_mode, $langcode);
+ entity_prepare_view('user', array($account->uid => $account), $langcode);
$account->content += field_attach_view('user', $account, $view_mode, $langcode);
// Populate $account->content with a render() array.