diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d12ca6e50c9d0a5d7ccd665108fc2f44c32431bc..b68b7fa92338ebded531108032445265719f422d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,7 +1,44 @@ -Drupal 7.11 xxxx-xx-xx (development version) +Drupal 7.13 xxxx-xx-xx (development version) ---------------------- +Drupal 7.12, 2012-02-01 +---------------------- +- Fixed bug preventing custom menus from receiving an active trail. +- Fixed hook_field_delete() no longer invoked during field_purge_data(). +- Fixed bug causing entity info cache to not be cleared with the rest of caches. +- Fixed file_unmanaged_copy() fails with Drupal 7.7+ and safe_mode() or + open_basedir(). +- Fixed Nested transactions throw exceptions when they got out of scope. +- Fixed bugs with the Return-Path when sending mail on both Windows and + non-Windows systems. +- Fixed bug with DrupalCacheArray property visibility preventing others from + extending it (API change: http://drupal.org/node/1422264). +- Fixed bug with handling of non-ASCII characters in file names (API change: + http://drupal.org/node/1424840). +- Reconciled field maximum length with database column size in image and + aggregator modules. +- Fixes to various core JavaScript files to allow for minification and + aggregation. +- Fixed Prevent tests from deleting main installation's tables when + parent::setUp() is not called. +- Fixed several Poll module bugs. +- Fixed several Shortcut module bugs. +- Added new hook_system_theme_info() to provide ability for contributed modules + to test theme functionality. +- Added ability to cancel mail sending from hook_mail_alter(). +- Added support for configurable PDO connection options, enabling master-master + database replication. +- Numerous improvements to tests and test runner to pave the way for faster test + runs. +- Expanded test coverage. +- Numerous API documentation improvements. +- Numerous performance improvements, including token replacement and render + cache. + +Drupal 7.11, 2012-02-01 +---------------------- +- Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-001. Drupal 7.10, 2011-12-05 ---------------------- diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 4ef4f2b83b8d8274222838429a1d02a7a82d7663..c32c05d5fac32f62d7e6642572c2fb6f14d9df48 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.11-dev'); +define('VERSION', '7.12'); /** * Core API compatibility. diff --git a/modules/aggregator/aggregator.admin.inc b/modules/aggregator/aggregator.admin.inc index 91bc75f34d9a55fd38fc48f31ec2156e8f41b293..9f92a67052a7970fa4a975ea48406da19e93fd6b 100644 --- a/modules/aggregator/aggregator.admin.inc +++ b/modules/aggregator/aggregator.admin.inc @@ -33,7 +33,7 @@ function aggregator_view() { ($feed->checked && $feed->refresh ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - REQUEST_TIME))) : t('never')), l(t('edit'), "admin/config/services/aggregator/edit/feed/$feed->fid"), l(t('remove items'), "admin/config/services/aggregator/remove/$feed->fid"), - l(t('update items'), "admin/config/services/aggregator/update/$feed->fid"), + l(t('update items'), "admin/config/services/aggregator/update/$feed->fid", array('query' => array('token' => drupal_get_token("aggregator/update/$feed->fid")))), ); } $output .= theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('No feeds available. Add feed.', array('@link' => url('admin/config/services/aggregator/add/feed'))))); @@ -404,6 +404,9 @@ function _aggregator_parse_opml($opml) { * An object describing the feed to be refreshed. */ function aggregator_admin_refresh_feed($feed) { + if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'aggregator/update/' . $feed->fid)) { + return MENU_ACCESS_DENIED; + } aggregator_refresh($feed); drupal_goto('admin/config/services/aggregator'); } diff --git a/modules/aggregator/aggregator.test b/modules/aggregator/aggregator.test index b224b79389ea34a3bca64b1bb19880e618db3908..5609d68aed22a849dea3095b056275a3c1975a17 100644 --- a/modules/aggregator/aggregator.test +++ b/modules/aggregator/aggregator.test @@ -92,8 +92,13 @@ class AggregatorTestCase extends DrupalWebTestCase { $this->drupalGet($feed->url); $this->assertResponse(200, t('!url is reachable.', array('!url' => $feed->url))); - // Refresh the feed (simulated link click). + // Attempt to access the update link directly without an access token. $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); + $this->assertResponse(403); + + // Refresh the feed (simulated link click). + $this->drupalGet('admin/config/services/aggregator'); + $this->clickLink('update items'); // Ensure we have the right number of items. $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid)); @@ -498,8 +503,8 @@ class UpdateFeedItemTestCase extends AggregatorTestCase { $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), t('The feed !name has been added.', array('!name' => $edit['title']))); $feed = db_query("SELECT * FROM {aggregator_feed} WHERE url = :url", array(':url' => $edit['url']))->fetchObject(); - $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); + aggregator_refresh($feed); $before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); // Sleep for 3 second. @@ -513,10 +518,9 @@ class UpdateFeedItemTestCase extends AggregatorTestCase { 'modified' => 0, )) ->execute(); - $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); + aggregator_refresh($feed); $after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); - $this->assertTrue($before === $after, t('Publish timestamp of feed item was not updated (!before === !after)', array('!before' => $before, '!after' => $after))); } } @@ -916,4 +920,3 @@ class FeedParserTestCase extends AggregatorTestCase { $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', array(':link' => 'http://example.org/2003/12/13/atom03'))->fetchField(), 'Atom entry id element is parsed correctly.'); } } - diff --git a/modules/file/file.api.php b/modules/file/file.api.php index 7f20d83f852ce1ec8fa5735c40706871e0770ec1..72aae40c9b105acbe6b080e02cfb20238225d47e 100644 --- a/modules/file/file.api.php +++ b/modules/file/file.api.php @@ -12,8 +12,8 @@ * file is referenced, e.g., only users with access to a node should be allowed * to download files attached to that node. * - * @param $field - * The field to which the file belongs. + * @param array $file_item + * The array of information about the file to check access for. * @param $entity_type * The type of $entity; for example, 'node' or 'user'. * @param $entity @@ -26,7 +26,7 @@ * * @see hook_field_access(). */ -function hook_file_download_access($field, $entity_type, $entity) { +function hook_file_download_access($file_item, $entity_type, $entity) { if ($entity_type == 'node') { return node_access('view', $entity); } @@ -45,8 +45,8 @@ function hook_file_download_access($field, $entity_type, $entity) { * An array of grants gathered by hook_file_download_access(). The array is * keyed by the module that defines the entity type's access control; the * values are Boolean grant responses for each module. - * @param $field - * The field to which the file belongs. + * @param array $file_item + * The array of information about the file to alter access for. * @param $entity_type * The type of $entity; for example, 'node' or 'user'. * @param $entity @@ -58,7 +58,7 @@ function hook_file_download_access($field, $entity_type, $entity) { * module's value in addition to other grants or to overwrite the values set * by other modules. */ -function hook_file_download_access_alter(&$grants, $field, $entity_type, $entity) { +function hook_file_download_access_alter(&$grants, $file_item, $entity_type, $entity) { // For our example module, we always enforce the rules set by node module. if (isset($grants['node'])) { $grants = array('node' => $grants['node']); diff --git a/modules/file/file.module b/modules/file/file.module index a9d35d518535310bc62ebd5ea349c4e2cb77016a..506b0e91dc26f49b38befb220fc22b30244deb8b 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -164,24 +164,27 @@ function file_file_download($uri, $field_type = 'file') { // Try to load $entity and $field. $entity = entity_load($entity_type, array($id)); $entity = reset($entity); - $field = NULL; + $field = field_info_field($field_name); + + // Load the field item that references the file. + $field_item = NULL; if ($entity) { - // Load all fields for that entity. + // Load all field items for that entity. $field_items = field_get_items($entity_type, $entity, $field_name); // Find the field item with the matching URI. - foreach ($field_items as $field_item) { - if ($field_item['uri'] == $uri) { - $field = $field_item; + foreach ($field_items as $item) { + if ($item['uri'] == $uri) { + $field_item = $item; break; } } } - // Check that $entity and $field were loaded successfully and check if - // access to that field is not disallowed. If any of these checks fail, - // stop checking access for this reference. - if (empty($entity) || empty($field) || !field_access('view', $field, $entity_type, $entity)) { + // Check that $entity, $field and $field_item were loaded successfully + // and check if access to that field is not disallowed. If any of these + // checks fail, stop checking access for this reference. + if (empty($entity) || empty($field) || empty($field_item) || !field_access('view', $field, $entity_type, $entity)) { $denied = TRUE; break; } @@ -190,10 +193,10 @@ function file_file_download($uri, $field_type = 'file') { // Default to FALSE and let entities overrule this ruling. $grants = array('system' => FALSE); foreach (module_implements('file_download_access') as $module) { - $grants = array_merge($grants, array($module => module_invoke($module, 'file_download_access', $field, $entity_type, $entity))); + $grants = array_merge($grants, array($module => module_invoke($module, 'file_download_access', $field_item, $entity_type, $entity))); } // Allow other modules to alter the returned grants/denies. - drupal_alter('file_download_access', $grants, $field, $entity_type, $entity); + drupal_alter('file_download_access', $grants, $field_item, $entity_type, $entity); if (in_array(TRUE, $grants)) { // If TRUE is returned, access is granted and no further checks are diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test index ee02d38c1c817c925b11f47e3a305a94589cba23..1b5fdf5cd9f7b78b6942fd12806bd5c6aef06ea8 100644 --- a/modules/file/tests/file.test +++ b/modules/file/tests/file.test @@ -1123,7 +1123,7 @@ class FilePrivateTestCase extends FileFieldTestCase { } function setUp() { - parent::setUp('node_access_test'); + parent::setUp(array('node_access_test', 'field_test')); node_access_rebuild(); variable_set('node_access_test_private', TRUE); } @@ -1140,6 +1140,10 @@ class FilePrivateTestCase extends FileFieldTestCase { $field_name = strtolower($this->randomName()); $this->createFileField($field_name, $type_name, array('uri_scheme' => 'private')); + // Create a field with no view access - see field_test_field_access(). + $no_access_field_name = 'field_no_view_access'; + $this->createFileField($no_access_field_name, $type_name, array('uri_scheme' => 'private')); + $test_file = $this->getTestFile('text'); $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => TRUE)); $node = node_load($nid, NULL, TRUE); @@ -1150,5 +1154,14 @@ class FilePrivateTestCase extends FileFieldTestCase { $this->drupalLogOut(); $this->drupalGet(file_create_url($node_file->uri)); $this->assertResponse(403, t('Confirmed that access is denied for the file without the needed permission.')); + + // Test with the field that should deny access through field access. + $this->drupalLogin($this->admin_user); + $nid = $this->uploadNodeFile($test_file, $no_access_field_name, $type_name, TRUE, array('private' => TRUE)); + $node = node_load($nid, NULL, TRUE); + $node_file = (object) $node->{$no_access_field_name}[LANGUAGE_NONE][0]; + // Ensure the file cannot be downloaded. + $this->drupalGet(file_create_url($node_file->uri)); + $this->assertResponse(403, t('Confirmed that access is denied for the file without view field access permission.')); } } diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc index 98af518c76598317324fb204921ec219cb3d0caa..9b793d368a2ec4c48015cfaf973d5caeb0bef04c 100644 --- a/modules/openid/openid.inc +++ b/modules/openid/openid.inc @@ -617,18 +617,31 @@ function _openid_get_params($str) { * @param $fallback_prefix * An optional prefix that will be used in case no prefix is found for the * target extension namespace. + * @param $only_signed + * Return only keys that are included in the message signature in openid.sig. + * Unsigned fields may have been modified or added by other parties than the + * OpenID Provider. + * * @return * An associative array containing all the parameters in the response message * that belong to the extension. The keys are stripped from their namespace * prefix. + * * @see http://openid.net/specs/openid-authentication-2_0.html#extensions */ -function openid_extract_namespace($response, $extension_namespace, $fallback_prefix = NULL) { +function openid_extract_namespace($response, $extension_namespace, $fallback_prefix = NULL, $only_signed = FALSE) { + $signed_keys = explode(',', $response['openid.signed']); + // Find the namespace prefix. $prefix = $fallback_prefix; foreach ($response as $key => $value) { if ($value == $extension_namespace && preg_match('/^openid\.ns\.([^.]+)$/', $key, $matches)) { $prefix = $matches[1]; + if ($only_signed && !in_array('ns.' . $matches[1], $signed_keys)) { + // The namespace was defined but was not signed as required. In this + // case we do not fall back to $fallback_prefix. + $prefix = NULL; + } break; } } @@ -641,7 +654,9 @@ function openid_extract_namespace($response, $extension_namespace, $fallback_pre foreach ($response as $key => $value) { if (preg_match('/^openid\.' . $prefix . '\.(.+)$/', $key, $matches)) { $local_key = $matches[1]; - $output[$local_key] = $value; + if (!$only_signed || in_array($prefix . '.' . $local_key, $signed_keys)) { + $output[$local_key] = $value; + } } } @@ -837,8 +852,8 @@ function _openid_invalid_openid_transition($identity, $response) { // Try to extract e-mail address from Simple Registration (SREG) or // Attribute Exchanges (AX) keys. $email = ''; - $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg'); - $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax'); + $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE); + $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax', TRUE); if (!empty($sreg_values['email']) && valid_email_address($sreg_values['email'])) { $email = $sreg_values['email']; } diff --git a/modules/openid/openid.module b/modules/openid/openid.module index f2847fc0d4e120f442e11ca3edb223f10307a766..e08d55718793f3d3cdcb1ceb23b60897e06cf561 100644 --- a/modules/openid/openid.module +++ b/modules/openid/openid.module @@ -185,10 +185,15 @@ function openid_form_user_register_form_alter(&$form, &$form_state) { $response = $_SESSION['openid']['response']; - // Extract Simple Registration keys from the response. - $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg'); - // Extract Attribute Exchanges keys from the response. - $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax'); + // Extract Simple Registration keys from the response. We only include + // signed keys as required by OpenID Simple Registration Extension 1.0, + // section 4. + $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE); + // Extract Attribute Exchanges keys from the response. We only include + // signed keys. This is not required by the specification, but it is + // recommended by Google, see + // http://googlecode.blogspot.com/2011/05/security-advisory-to-websites-using.html + $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax', TRUE); if (!empty($sreg_values['nickname'])) { // Use the nickname returned by Simple Registration if available. diff --git a/modules/openid/openid.test b/modules/openid/openid.test index afb9068c6bc6072c3efa25321680acebd266524a..9b6b1ad5fe5d5109a5f7e9542ebf3b0365df10a7 100644 --- a/modules/openid/openid.test +++ b/modules/openid/openid.test @@ -343,17 +343,49 @@ class OpenIDFunctionalTestCase extends OpenIDWebTestCase { // Use a User-supplied Identity that is the URL of an XRDS document. $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); - // Do not sign all mandatory fields (e.g. assoc_handle). + // Respond with an invalid signature. + variable_set('openid_test_response', array('openid.sig' => 'this-is-an-invalid-signature')); + $this->submitLoginForm($identity); + $this->assertRaw('OpenID login failed.'); + + // Do not sign the mandatory field openid.assoc_handle. variable_set('openid_test_response', array('openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce')); $this->submitLoginForm($identity); $this->assertRaw('OpenID login failed.'); - // Sign all mandatory fields and some custom fields. - variable_set('openid_test_response', array('openid.foo' => 'bar', 'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle,foo')); + // Sign all mandatory fields and a custom field. + $keys_to_sign = array('op_endpoint', 'claimed_id', 'identity', 'return_to', 'response_nonce', 'assoc_handle', 'foo'); + $association = new stdClass(); + $association->mac_key = variable_get('mac_key'); + $response = array( + 'openid.op_endpoint' => url('openid-test/endpoint', array('absolute' => TRUE)), + 'openid.claimed_id' => $identity, + 'openid.identity' => $identity, + 'openid.return_to' => url('openid/authenticate', array('absolute' => TRUE)), + 'openid.response_nonce' => _openid_nonce(), + 'openid.assoc_handle' => 'openid-test', + 'openid.foo' => 123, + 'openid.signed' => implode(',', $keys_to_sign), + ); + $response['openid.sig'] = _openid_signature($association, $response, $keys_to_sign); + variable_set('openid_test_response', $response); $this->submitLoginForm($identity); $this->assertNoRaw('OpenID login failed.'); - } + $this->assertFieldByName('name', '', t('No username was supplied by provider.')); + $this->assertFieldByName('mail', '', t('No e-mail address was supplied by provider.')); + // Check that unsigned SREG fields are ignored. + $response = array( + 'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle,sreg.nickname', + 'openid.sreg.nickname' => 'john', + 'openid.sreg.email' => 'john@example.com', + ); + variable_set('openid_test_response', $response); + $this->submitLoginForm($identity); + $this->assertNoRaw('OpenID login failed.'); + $this->assertFieldByName('name', 'john', t('Username was supplied by provider.')); + $this->assertFieldByName('mail', '', t('E-mail address supplied by provider was ignored.')); + } } /** @@ -728,4 +760,41 @@ class OpenIDUnitTest extends DrupalWebTestCase { $this->assertEqual(openid_normalize('http://example.com/path#fragment'), 'http://example.com/path', t('openid_normalize() correctly normalized a URL with a fragment.')); } + + /** + * Test openid_extract_namespace(). + */ + function testOpenidExtractNamespace() { + $response = array( + 'openid.sreg.nickname' => 'john', + 'openid.ns.ext1' => OPENID_NS_SREG, + 'openid.ext1.nickname' => 'george', + 'openid.ext1.email' => 'george@example.com', + 'openid.ns.ext2' => 'http://example.com/ns/ext2', + 'openid.ext2.foo' => '123', + 'openid.ext2.bar' => '456', + 'openid.signed' => 'sreg.nickname,ns.ext1,ext1.email,ext2.foo', + ); + + $values = openid_extract_namespace($response, 'http://example.com/ns/dummy', NULL, FALSE); + $this->assertEqual($values, array(), t('Nothing found for unused namespace.')); + + $values = openid_extract_namespace($response, 'http://example.com/ns/dummy', 'sreg', FALSE); + $this->assertEqual($values, array('nickname' => 'john'), t('Value found for fallback prefix.')); + + $values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', FALSE); + $this->assertEqual($values, array('nickname' => 'george', 'email' => 'george@example.com'), t('Namespace takes precedence over fallback prefix.')); + + // ext1.email is signed, but ext1.nickname is not. + $values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE); + $this->assertEqual($values, array('email' => 'george@example.com'), t('Unsigned namespaced fields ignored.')); + + $values = openid_extract_namespace($response, 'http://example.com/ns/ext2', 'sreg', FALSE); + $this->assertEqual($values, array('foo' => '123', 'bar' => '456'), t('Unsigned fields found.')); + + // ext2.foo and ext2.bar are ignored, because ns.ext2 is not signed. The + // fallback prefix is not used, because the namespace is specified. + $values = openid_extract_namespace($response, 'http://example.com/ns/ext2', 'sreg', TRUE); + $this->assertEqual($values, array(), t('Unsigned fields ignored.')); + } } diff --git a/modules/openid/tests/openid_test.module b/modules/openid/tests/openid_test.module index 629dcd3356a59023591f2be7518f64e6ba69d65d..1b0de4ec5de14079e075b3457e0fe9e0a2bb29eb 100644 --- a/modules/openid/tests/openid_test.module +++ b/modules/openid/tests/openid_test.module @@ -324,9 +324,7 @@ function _openid_test_endpoint_authenticate() { // Generate unique identifier for this authentication. $nonce = _openid_nonce(); - // Generate response containing the user's identity. The openid.sreg.xxx - // entries contain profile data stored by the OpenID Provider (see OpenID - // Simple Registration Extension 1.0). + // Generate response containing the user's identity. $response = variable_get('openid_test_response', array()) + array( 'openid.ns' => OPENID_NS_2_0, 'openid.mode' => 'id_res', @@ -336,14 +334,27 @@ function _openid_test_endpoint_authenticate() { 'openid.return_to' => $_REQUEST['openid_return_to'], 'openid.response_nonce' => $nonce, 'openid.assoc_handle' => 'openid-test', - 'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle', ); + if (isset($response['openid.signed'])) { + $keys_to_sign = explode(',', $response['openid.signed']); + } + else { + // Unless openid.signed is explicitly defined, all keys are signed. + $keys_to_sign = array(); + foreach ($response as $key => $value) { + // Strip off the "openid." prefix. + $keys_to_sign[] = substr($key, 7); + } + $response['openid.signed'] = implode(',', $keys_to_sign); + } + // Sign the message using the MAC key that was exchanged during association. $association = new stdClass(); $association->mac_key = variable_get('mac_key'); - $keys_to_sign = explode(',', $response['openid.signed']); - $response['openid.sig'] = _openid_signature($association, $response, $keys_to_sign); + if (!isset($response['openid.sig'])) { + $response['openid.sig'] = _openid_signature($association, $response, $keys_to_sign); + } // Put the signed message into the query string of a URL supplied by the // Relying Party, and redirect the user.