diff --git a/includes/common.inc b/includes/common.inc index 4168ec9a20e..5dadb4d165f 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 4539b37a74d..4cc1a33d755 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -540,6 +540,63 @@ abstract class DatabaseConnection extends PDO { return $this->prefixTables('{' . $table . '}_' . $field . '_seq'); } + /** + * 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. * diff --git a/includes/database/mysql/query.inc b/includes/database/mysql/query.inc index f7fb52f0459..888b6a5a450 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 fe7909e17c5..f3783a9ca8f 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 7f3e9ff8570..23b652f9b4b 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 6e4b0dc4869..716c2fc3dbf 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 a176ed64937..6b8a72f2ab4 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 2a8c80c4120..cfd35c7944a 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 836f2fed884..499adc84774 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 82aabc054fc..3b15c76c87b 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 c5378dc8529..14290e3ad11 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 4f079edd700..524a57fa7e7 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 6d4b1d7ff78..7673de8864a 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 09632ba1417..202a8355ea2 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 231355ceb1d..c22d1fc5d0b 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -1324,6 +1324,27 @@ class DatabaseSelectTestCase extends DatabaseTestCase { $this->assertEqual($query, $expected, t('The flattened query contains the comment string.')); } + /** + * 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. */ diff --git a/modules/simpletest/tests/menu.test b/modules/simpletest/tests/menu.test index 2578bebc08c..5642fcee0e0 100644 --- a/modules/simpletest/tests/menu.test +++ b/modules/simpletest/tests/menu.test @@ -856,6 +856,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. */ diff --git a/modules/system/system.api.php b/modules/system/system.api.php index 1b295b4f426..c7db6f1dda4 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 5fb41e61f75..50d2fd60834 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 204155a4cc8..90d313b106d 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.