From a0fee30d766a4760db96fac8aacac462e50f61b9 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Wed, 15 Oct 2014 10:31:54 -0400 Subject: [PATCH 01/68] SA-CORE-2014-005 by Stefan Horst, greggles, larowlan, David_Rothstein, klausi: Fixed SQL injection vulnerability --- includes/database/database.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/database/database.inc b/includes/database/database.inc index f78098bc0..01b638584 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -736,7 +736,7 @@ abstract class DatabaseConnection extends PDO { // to expand it out into a comma-delimited set of placeholders. foreach (array_filter($args, 'is_array') as $key => $data) { $new_keys = array(); - foreach ($data as $i => $value) { + foreach (array_values($data) as $i => $value) { // This assumes that there are no other placeholders that use the same // name. For example, if the array placeholder is defined as :example // and there is already an :example_2 placeholder, this will generate From 72f34da9a609589fefd527f7638ca20d02bd68e2 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Wed, 15 Oct 2014 10:36:05 -0400 Subject: [PATCH 02/68] Tests for SA-CORE-2014-005 by Stefan Horst, greggles, larowlan, David_Rothstein, klausi --- modules/simpletest/tests/database_test.test | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index dba04b27b..209bf6813 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -3384,6 +3384,34 @@ class DatabaseQueryTestCase extends DatabaseTestCase { $this->assertEqual(count($names), 3, 'Correct number of names returned'); } + + /** + * Test SQL injection via database query array arguments. + */ + public function testArrayArgumentsSQLInjection() { + // Attempt SQL injection and verify that it does not work. + $condition = array( + "1 ;INSERT INTO {test} SET name = 'test12345678'; -- " => '', + '1' => '', + ); + try { + db_query("SELECT * FROM {test} WHERE name = :name", array(':name' => $condition))->fetchObject(); + $this->fail('SQL injection attempt via array arguments should result in a PDOException.'); + } + catch (PDOException $e) { + $this->pass('SQL injection attempt via array arguments should result in a PDOException.'); + } + + // Test that the insert query that was used in the SQL injection attempt did + // not result in a row being inserted in the database. + $result = db_select('test') + ->condition('name', 'test12345678') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertFalse($result, 'SQL injection attempt did not result in a row being inserted in the database table.'); + } + } /** From 76933a3efb6afce2b2eae5a2a4491aa86c3eb924 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Wed, 15 Oct 2014 10:36:46 -0400 Subject: [PATCH 03/68] Drupal 7.32 --- CHANGELOG.txt | 4 ++++ includes/bootstrap.inc | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3a3abdc0c..02c946550 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,8 @@ +Drupal 7.32, 2014-10-15 +---------------------- +- Fixed security issues (SQL injection). See SA-CORE-2014-005. + Drupal 7.31, 2014-08-06 ---------------------- - Fixed security issues (denial of service). See SA-CORE-2014-004. diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 9e6b57c82..44a16dc75 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.31'); +define('VERSION', '7.32'); /** * Core API compatibility. From 76faa7de48e759cff770269c25d6ed6f92cf6b29 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Fri, 7 Nov 2014 10:29:24 -0500 Subject: [PATCH 04/68] Back to 7.x-dev --- CHANGELOG.txt | 3 +++ includes/bootstrap.inc | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6530aa874..79d9910b1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,7 @@ +Drupal 7.34, xxxx-xx-xx (development version) +----------------------- + Drupal 7.33, 2014-11-07 ----------------------- - Began storing the file modification time of each module and theme in the diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 6fa369d40..b6c531c3b 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.33'); +define('VERSION', '7.34-dev'); /** * Core API compatibility. From de8762b201863542b1867737997a45c7100b8f2f Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 24 Nov 2014 19:18:35 -0500 Subject: [PATCH 05/68] Issue #2380143 by Lendude, pwolanin: Contact forms set an incorrect name and e-mail address on the global user object after the form is submitted. --- CHANGELOG.txt | 3 +++ modules/contact/contact.pages.inc | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e97215da1..4e0f43386 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,9 @@ Drupal 7.35, xxxx-xx-xx (development version) ----------------------- +- Fixed a bug in the Contact module which caused the global user object to have + the incorrect name and e-mail address during the remainder of the page + request after the contact form is submitted. Drupal 7.34, 2014-11-19 ---------------------- diff --git a/modules/contact/contact.pages.inc b/modules/contact/contact.pages.inc index ba8918bf5..233818ce5 100644 --- a/modules/contact/contact.pages.inc +++ b/modules/contact/contact.pages.inc @@ -134,7 +134,7 @@ function contact_site_form_submit($form, &$form_state) { global $user, $language; $values = $form_state['values']; - $values['sender'] = $user; + $values['sender'] = clone $user; $values['sender']->name = $values['name']; $values['sender']->mail = $values['mail']; $values['category'] = contact_load($values['cid']); @@ -270,7 +270,7 @@ function contact_personal_form_submit($form, &$form_state) { global $user, $language; $values = $form_state['values']; - $values['sender'] = $user; + $values['sender'] = clone $user; $values['sender']->name = $values['name']; $values['sender']->mail = $values['mail']; From 8bbc2d2ea0bfb6cf12f5f6f3edf82cca6429d046 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 1 Dec 2014 18:33:09 -0500 Subject: [PATCH 06/68] Issue #2380053 by klausi, pwolanin, tsphethean, sun, David_Rothstein: Posting an array as value of a form element is allowed even when a string is expected (and bypasses #maxlength constraints) - first step: text fields --- CHANGELOG.txt | 2 ++ includes/form.inc | 41 +++++++++++++++++++-- modules/simpletest/tests/form.test | 58 ++++++++++++++++++++++++++++++ modules/system/system.module | 6 ++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4e0f43386..d03e71cc3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.35, xxxx-xx-xx (development version) ----------------------- +- Prevented the form API from allowing arrays to be submitted for various form + elements (such as textfields, textareas, and password fields). - Fixed a bug in the Contact module which caused the global user object to have the incorrect name and e-mail address during the remainder of the page request after the contact form is submitted. diff --git a/includes/form.inc b/includes/form.inc index da1caa819..223c4cd68 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -2451,6 +2451,17 @@ function form_type_password_confirm_value($element, $input = FALSE) { $element += array('#default_value' => array()); return $element['#default_value'] + array('pass1' => '', 'pass2' => ''); } + $value = array('pass1' => '', 'pass2' => ''); + // Throw out all invalid array keys; we only allow pass1 and pass2. + foreach ($value as $allowed_key => $default) { + // These should be strings, but allow other scalars since they might be + // valid input in programmatic form submissions. Any nested array values + // are ignored. + if (isset($input[$allowed_key]) && is_scalar($input[$allowed_key])) { + $value[$allowed_key] = (string) $input[$allowed_key]; + } + } + return $value; } /** @@ -2494,6 +2505,27 @@ function form_type_select_value($element, $input = FALSE) { } } +/** + * Determines the value for a textarea form element. + * + * @param array $element + * The form element whose value is being populated. + * @param mixed $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * + * @return string + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_textarea_value($element, $input = FALSE) { + if ($input !== FALSE) { + // This should be a string, but allow other scalars since they might be + // valid input in programmatic form submissions. + return is_scalar($input) ? (string) $input : ''; + } +} + /** * Determines the value for a textfield form element. * @@ -2509,9 +2541,12 @@ function form_type_select_value($element, $input = FALSE) { */ function form_type_textfield_value($element, $input = FALSE) { if ($input !== FALSE && $input !== NULL) { - // Equate $input to the form value to ensure it's marked for - // validation. - return str_replace(array("\r", "\n"), '', $input); + // This should be a string, but allow other scalars since they might be + // valid input in programmatic form submissions. + if (!is_scalar($input)) { + $input = ''; + } + return str_replace(array("\r", "\n"), '', (string) $input); } } diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index f90b854c7..0bf6c8c65 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -470,6 +470,64 @@ class FormsTestCase extends DrupalWebTestCase { $this->drupalPost(NULL, array('checkboxes[one]' => TRUE, 'checkboxes[two]' => TRUE), t('Submit')); $this->assertText('An illegal choice has been detected.', 'Input forgery was detected.'); } + + /** + * Tests that submitted values are converted to scalar strings for textfields. + */ + public function testTextfieldStringValue() { + // Check multivalued submissions. + $multivalue = array('evil' => 'multivalue', 'not so' => 'good'); + $this->checkFormValue('textfield', $multivalue, ''); + $this->checkFormValue('password', $multivalue, ''); + $this->checkFormValue('textarea', $multivalue, ''); + $this->checkFormValue('machine_name', $multivalue, ''); + $this->checkFormValue('password_confirm', $multivalue, array('pass1' => '', 'pass2' => '')); + // Check integer submissions. + $integer = 5; + $string = '5'; + $this->checkFormValue('textfield', $integer, $string); + $this->checkFormValue('password', $integer, $string); + $this->checkFormValue('textarea', $integer, $string); + $this->checkFormValue('machine_name', $integer, $string); + $this->checkFormValue('password_confirm', array('pass1' => $integer, 'pass2' => $integer), array('pass1' => $string, 'pass2' => $string)); + // Check that invalid array keys are ignored for password confirm elements. + $this->checkFormValue('password_confirm', array('pass1' => 'test', 'pass2' => 'test', 'extra' => 'invalid'), array('pass1' => 'test', 'pass2' => 'test')); + } + + /** + * Checks that a given form input value is sanitized to the expected result. + * + * @param string $element_type + * The form element type. Example: textfield. + * @param mixed $input_value + * The submitted user input value for the form element. + * @param mixed $expected_value + * The sanitized result value in the form state after calling + * form_builder(). + */ + protected function checkFormValue($element_type, $input_value, $expected_value) { + $form_id = $this->randomName(); + $form = array(); + $form_state = form_state_defaults(); + $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); + $form[$element_type] = array( + '#type' => $element_type, + '#title' => 'test', + ); + + $form_state['input'][$element_type] = $input_value; + $form_state['input']['form_id'] = $form_id; + $form_state['method'] = 'post'; + $form_state['values'] = array(); + drupal_prepare_form($form_id, $form, $form_state); + + // This is the main function we want to test: it is responsible for + // populating user supplied $form_state['input'] to sanitized + // $form_state['values']. + form_builder($form_id, $form, $form_state); + + $this->assertIdentical($form_state['values'][$element_type], $expected_value, format_string('Form submission for the "@element_type" element type has been correctly sanitized.', array('@element_type' => $element_type))); + } } /** diff --git a/modules/system/system.module b/modules/system/system.module index a27493579..6a6200ea1 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -374,6 +374,9 @@ function system_element_info() { '#element_validate' => array('form_validate_machine_name'), '#theme' => 'textfield', '#theme_wrappers' => array('form_element'), + // Use the same value callback as for textfields; this ensures that we only + // get string values. + '#value_callback' => 'form_type_textfield_value', ); $types['password'] = array( '#input' => TRUE, @@ -382,6 +385,9 @@ function system_element_info() { '#process' => array('ajax_process_form'), '#theme' => 'password', '#theme_wrappers' => array('form_element'), + // Use the same value callback as for textfields; this ensures that we only + // get string values. + '#value_callback' => 'form_type_textfield_value', ); $types['password_confirm'] = array( '#input' => TRUE, From d84d4ce4d85ed2fd84951c8fd2a0dfe9c5df3bba Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 2 Dec 2014 08:53:15 -0800 Subject: [PATCH 07/68] Issue #2383491 by er.pushpinderrana, Sweetchuck: Inaccurate documentation - hook_image_toolkits() --- modules/system/system.api.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/system/system.api.php b/modules/system/system.api.php index b60f50deb..362e32263 100644 --- a/modules/system/system.api.php +++ b/modules/system/system.api.php @@ -1890,9 +1890,8 @@ function hook_init() { /** * Define image toolkits provided by this module. * - * The file which includes each toolkit's functions must be declared as part of - * the files array in the module .info file so that the registry will find and - * parse it. + * The file which includes each toolkit's functions must be included in this + * hook. * * The toolkit's functions must be named image_toolkitname_operation(). * where the operation may be: From a6c50f0526eac3cee71f48059e5ed569dac8a055 Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 2 Dec 2014 08:54:41 -0800 Subject: [PATCH 08/68] Issue #2382801 by er.pushpinderrana, Liam Morland: Improve documentation for module_exists() --- includes/module.inc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/module.inc b/includes/module.inc index fe2a9805e..494924f53 100644 --- a/includes/module.inc +++ b/includes/module.inc @@ -265,11 +265,11 @@ function _module_build_dependencies($files) { /** * Determines whether a given module exists. * - * @param $module + * @param string $module * The name of the module (without the .module extension). * - * @return - * TRUE if the module is both installed and enabled. + * @return bool + * TRUE if the module is both installed and enabled, FALSE otherwise. */ function module_exists($module) { $list = module_list(); From 1e3e775f57f9140fc2281fb1fe02ae39d3a97fc3 Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 2 Dec 2014 10:24:53 -0800 Subject: [PATCH 09/68] Issue #2377879 by er.pushpinderrana, yakoub: hook_user_view documentation has incorrect piece --- modules/user/user.api.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/modules/user/user.api.php b/modules/user/user.api.php index e88c7a974..edc61bd3c 100644 --- a/modules/user/user.api.php +++ b/modules/user/user.api.php @@ -327,14 +327,6 @@ function hook_user_logout($account) { * The module should format its custom additions for display and add them to the * $account->content array. * - * Note that when this hook is invoked, the changes have not yet been written to - * the database, because a database transaction is still in progress. The - * transaction is not finalized until the save operation is entirely completed - * and user_save() goes out of scope. You should not rely on data in the - * database at this time as it is not updated yet. You should also note that any - * write/update database queries executed from this hook are also not committed - * immediately. Check user_save() and db_transaction() for more info. - * * @param $account * The user object on which the operation is being performed. * @param $view_mode From 76e3d53c1f55564e3d01d6a3b5f3d2d5550ee858 Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 2 Dec 2014 10:26:32 -0800 Subject: [PATCH 10/68] Issue #2208649 by joachim, er.pushpinderrana: document queue worker callback --- modules/system/system.api.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/modules/system/system.api.php b/modules/system/system.api.php index 362e32263..0af6156a4 100644 --- a/modules/system/system.api.php +++ b/modules/system/system.api.php @@ -606,8 +606,8 @@ function hook_cron() { * @return * An associative array where the key is the queue name and the value is * again an associative array. Possible keys are: - * - 'worker callback': The name of the function to call. It will be called - * with one argument, the item created via DrupalQueue::createItem(). + * - 'worker callback': A PHP callable to call that is an implementation of + * callback_queue_worker(). * - 'time': (optional) How much time Drupal should spend on calling this * worker in seconds. Defaults to 15. * - 'skip on cron': (optional) Set to TRUE to avoid being processed during @@ -643,6 +643,28 @@ function hook_cron_queue_info_alter(&$queues) { $queues['aggregator_feeds']['time'] = 90; } +/** + * Work on a single queue item. + * + * Callback for hook_queue_info(). + * + * @param $queue_item_data + * The data that was passed to DrupalQueue::createItem() when the item was + * queued. + * + * @throws \Exception + * The worker callback may throw an exception to indicate there was a problem. + * The cron process will log the exception, and leave the item in the queue to + * be processed again later. + * + * @see drupal_cron_run() + */ +function callback_queue_worker($queue_item_data) { + $node = node_load($queue_item_data); + $node->title = 'Updated title'; + $node->save(); +} + /** * Allows modules to declare their own Form API element types and specify their * default values. From f1e15c1d6579f5d656b5792746b230a67e533168 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Thu, 22 Jan 2015 00:24:59 -0500 Subject: [PATCH 11/68] Issue #2411227 by chx: Remove chx from the Drupal 7 MAINTAINERS.txt file. --- MAINTAINERS.txt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/MAINTAINERS.txt b/MAINTAINERS.txt index 36563145f..f5cf6f893 100644 --- a/MAINTAINERS.txt +++ b/MAINTAINERS.txt @@ -27,7 +27,6 @@ Ajax system - Earl Miles 'merlinofchaos' http://drupal.org/user/26979 Base system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Damien Tournoud 'DamZ' http://drupal.org/user/22211 - Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 @@ -39,7 +38,6 @@ Cache system - Nathaniel Catchpole 'catch' http://drupal.org/user/35733 Cron system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Derek Wright 'dww' http://drupal.org/user/46549 Database system @@ -55,10 +53,8 @@ Database system - Sqlite driver - Damien Tournoud 'DamZ' http://drupal.org/user/22211 - - Károly Négyesi 'chx' http://drupal.org/user/9446 Database update system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Ashok Modi 'BTMash' http://drupal.org/user/60422 Entity system @@ -71,7 +67,6 @@ File system - Aaron Winborn 'aaron' http://drupal.org/user/33420 Form system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Alex Bronstein 'effulgentsia' http://drupal.org/user/78040 - Wolfgang Ziegler 'fago' http://drupal.org/user/16747 - Daniel F. Kudwien 'sun' http://drupal.org/user/54136 @@ -105,7 +100,6 @@ Markup Menu system - Peter Wolanin 'pwolanin' http://drupal.org/user/49851 -- Károly Négyesi 'chx' http://drupal.org/user/9446 Path system - Dave Reid 'davereid' http://drupal.org/user/53892 @@ -261,7 +255,6 @@ Shortcut module Simpletest module - Jimmy Berry 'boombatower' http://drupal.org/user/214218 -- Károly Négyesi 'chx' http://drupal.org/user/9446 Statistics module - Tim Millwood 'timmillwood' http://drupal.org/user/227849 From dc3656528f68188e996826ca79c6415e0e38df64 Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 17 Feb 2015 16:29:49 -0800 Subject: [PATCH 12/68] Issue #2392543 by awm: Fix documentation for hook_taxonomy_term_view_alter --- modules/taxonomy/taxonomy.api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/taxonomy/taxonomy.api.php b/modules/taxonomy/taxonomy.api.php index b9c23dbfe..f3c5022b6 100644 --- a/modules/taxonomy/taxonomy.api.php +++ b/modules/taxonomy/taxonomy.api.php @@ -212,7 +212,7 @@ function hook_taxonomy_term_view($term, $view_mode, $langcode) { * documentation respectively for details. * * @param $build - * A renderable array representing the node content. + * A renderable array representing the term. * * @see hook_entity_view_alter() */ From 4d1840e620bfb6e619c0cdd05e61e8a25252f491 Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 17 Feb 2015 16:32:33 -0800 Subject: [PATCH 13/68] Issue #2407175 by zealfire: Documentation error in default.settings.php --- sites/default/default.settings.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 449f18867..562f99855 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -517,10 +517,10 @@ * server response time when loading 404 error pages and prevents the 404 error * from being logged in the Drupal system log. In order to prevent valid pages * such as image styles and other generated content that may match the - * '404_fast_html' regular expression from returning 404 errors, it is necessary - * to add them to the '404_fast_paths_exclude' regular expression above. Make - * sure that you understand the effects of this feature before uncommenting the - * line below. + * '404_fast_paths' regular expression from returning 404 errors, it is + * necessary to add them to the '404_fast_paths_exclude' regular expression + * above. Make sure that you understand the effects of this feature before + * uncommenting the line below. */ # drupal_fast_404(); From 9b993d50861ca924ce285c04e45d722b17db3598 Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 17 Feb 2015 16:34:25 -0800 Subject: [PATCH 14/68] Issue #1081902 by zealfire: DrupalEntityControllerInterface::load - doc needs to clarify $conditions --- includes/entity.inc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/includes/entity.inc b/includes/entity.inc index 203ed87f9..27434d048 100644 --- a/includes/entity.inc +++ b/includes/entity.inc @@ -28,7 +28,9 @@ interface DrupalEntityControllerInterface { * @param $ids * An array of entity IDs, or FALSE to load all entities. * @param $conditions - * An array of conditions in the form 'field' => $value. + * An array of conditions. Keys are field names on the entity's base table. + * Values will be compared for equality. All the comparisons will be ANDed + * together. This parameter is deprecated; use an EntityFieldQuery instead. * * @return * An array of entity objects indexed by their ids. When no results are @@ -236,7 +238,9 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface { * @param $ids * An array of entity IDs, or FALSE to load all entities. * @param $conditions - * An array of conditions in the form 'field' => $value. + * An array of conditions. Keys are field names on the entity's base table. + * Values will be compared for equality. All the comparisons will be ANDed + * together. This parameter is deprecated; use an EntityFieldQuery instead. * @param $revision_id * The ID of the revision to load, or FALSE if this query is asking for the * most current revision(s). From ea85d7c8e6f1793357841d0ba8d64f01cd3f69f4 Mon Sep 17 00:00:00 2001 From: Jennifer Hodgdon Date: Tue, 17 Feb 2015 16:37:26 -0800 Subject: [PATCH 15/68] Issue #2417983 by jacob.embree: Change docs instances of "the the" to "the" --- includes/ajax.inc | 2 +- includes/common.inc | 4 ++-- includes/file.inc | 2 +- includes/form.inc | 2 +- misc/tableselect.js | 2 +- modules/locale/locale.test | 2 +- modules/node/node.module | 2 +- modules/simpletest/tests/ajax.test | 2 +- modules/simpletest/tests/ajax_forms_test.module | 2 +- modules/simpletest/tests/database_test.test | 16 +++++++++------- modules/statistics/statistics.admin.inc | 2 +- 11 files changed, 20 insertions(+), 18 deletions(-) diff --git a/includes/ajax.inc b/includes/ajax.inc index 10877a246..6e8e277b8 100644 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -211,7 +211,7 @@ * * When returning an Ajax command array, it is often useful to have * status messages rendered along with other tasks in the command array. - * In that case the the Ajax commands array may be constructed like this: + * In that case the Ajax commands array may be constructed like this: * @code * $commands = array(); * $commands[] = ajax_command_replace(NULL, $output); diff --git a/includes/common.inc b/includes/common.inc index 20cc82be1..631d246bc 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -4448,8 +4448,8 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS * * Libraries, JavaScript, CSS and other types of custom structures are attached * to elements using the #attached property. The #attached property is an - * associative array, where the keys are the the attachment types and the values - * are the attached data. For example: + * associative array, where the keys are the attachment types and the values are + * the attached data. For example: * @code * $build['#attached'] = array( * 'js' => array(drupal_get_path('module', 'taxonomy') . '/taxonomy.js'), diff --git a/includes/file.inc b/includes/file.inc index 803661f4d..d3ac87ea0 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -1559,7 +1559,7 @@ function file_save_upload($form_field_name, $validators = array(), $destination return FALSE; } - // Add in our check of the the file name length. + // Add in our check of the file name length. $validators['file_validate_name_length'] = array(); // Call the validation functions specified by this function's caller. diff --git a/includes/form.inc b/includes/form.inc index 223c4cd68..0d358c2b4 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -938,7 +938,7 @@ function drupal_process_form($form_id, &$form, &$form_state) { // after the batch is processed. } - // Set a flag to indicate the the form has been processed and executed. + // Set a flag to indicate that the form has been processed and executed. $form_state['executed'] = TRUE; // Redirect the form based on values in $form_state. diff --git a/misc/tableselect.js b/misc/tableselect.js index fee63a9fd..8f0cd9750 100644 --- a/misc/tableselect.js +++ b/misc/tableselect.js @@ -60,7 +60,7 @@ Drupal.tableSelect = function () { }; Drupal.tableSelectRange = function (from, to, state) { - // We determine the looping mode based on the the order of from and to. + // We determine the looping mode based on the order of from and to. var mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling'; // Traverse through the sibling nodes. diff --git a/modules/locale/locale.test b/modules/locale/locale.test index 9ffec9f1a..90865872f 100644 --- a/modules/locale/locale.test +++ b/modules/locale/locale.test @@ -1202,7 +1202,7 @@ EOF; * Helper function that returns a .po file with context. */ function getPoFileWithContext() { - // Croatian (code hr) is one the the languages that have a different + // Croatian (code hr) is one of the languages that have a different // form for the full name and the abbreviated name for the month May. return <<< EOF msgid "" diff --git a/modules/node/node.module b/modules/node/node.module index dbb1a6504..0a7560bb9 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -1610,7 +1610,7 @@ function node_permission() { } /** - * Gathers the rankings from the the hook_ranking() implementations. + * Gathers the rankings from the hook_ranking() implementations. * * @param $query * A query object that has been extended with the Search DB Extender. diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test index c38a325fb..afe02306b 100644 --- a/modules/simpletest/tests/ajax.test +++ b/modules/simpletest/tests/ajax.test @@ -293,7 +293,7 @@ class AJAXCommandsTestCase extends AJAXTestCase { $this->assertCommand($commands, $expected, "'changed' AJAX command (with asterisk) issued with correct selector"); // Tests the 'css' command. - $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue."))); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the '#box' div to be blue."))); $expected = array( 'command' => 'css', 'selector' => '#css_div', diff --git a/modules/simpletest/tests/ajax_forms_test.module b/modules/simpletest/tests/ajax_forms_test.module index 260d9112e..de2fa0ba8 100644 --- a/modules/simpletest/tests/ajax_forms_test.module +++ b/modules/simpletest/tests/ajax_forms_test.module @@ -157,7 +157,7 @@ function ajax_forms_test_ajax_commands_form($form, &$form_state) { // Shows the Ajax 'css' command. $form['css_command_example'] = array( - '#value' => t("Set the the '#box' div to be blue."), + '#value' => t("Set the '#box' div to be blue."), '#type' => 'submit', '#ajax' => array( 'callback' => 'ajax_forms_test_advanced_commands_css_callback', diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index 12ddb343e..9c533bed5 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -238,7 +238,7 @@ class DatabaseConnectionTestCase extends DatabaseTestCase { // Open the default target so we have an object to compare. $db1 = Database::getConnection('default', 'default'); - // Try to close the the default connection, then open a new one. + // Try to close the default connection, then open a new one. Database::closeConnection('default', 'default'); $db2 = Database::getConnection('default', 'default'); @@ -3454,12 +3454,14 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { } /** - * Helper method for transaction unit test. This "outer layer" transaction - * starts and then encapsulates the "inner layer" transaction. This nesting - * is used to evaluate whether the the database transaction API properly - * supports nesting. By "properly supports," we mean the outer transaction - * continues to exist regardless of what functions are called and whether - * those functions start their own transactions. + * Helper method for transaction unit test. + * + * This "outer layer" transaction starts and then encapsulates the + * "inner layer" transaction. This nesting is used to evaluate whether the + * database transaction API properly supports nesting. By "properly supports," + * we mean the outer transaction continues to exist regardless of what + * functions are called and whether those functions start their own + * transactions. * * In contrast, a typical database would commit the outer transaction, start * a new transaction for the inner layer, commit the inner layer transaction, diff --git a/modules/statistics/statistics.admin.inc b/modules/statistics/statistics.admin.inc index e83a115f0..f061081e8 100644 --- a/modules/statistics/statistics.admin.inc +++ b/modules/statistics/statistics.admin.inc @@ -59,7 +59,7 @@ function statistics_recent_hits() { * statistics settings form, but is dependent on cron running. * * @return - * A render array containing information about the the top pages. + * A render array containing information about the top pages. */ function statistics_top_pages() { $header = array( From b44056d2f8e8c71d35c85ec5c2fb8f7c8a02d8a8 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Wed, 18 Mar 2015 15:20:37 -0400 Subject: [PATCH 16/68] Drupal 7.35 --- CHANGELOG.txt | 4 + includes/bootstrap.inc | 22 +++++- includes/common.inc | 46 ++++++++--- modules/simpletest/tests/bootstrap.test | 82 ++++++++++++++++++++ modules/simpletest/tests/common.test | 20 +++++ modules/simpletest/tests/system_test.module | 38 ++++++++++ modules/statistics/statistics.test | 2 +- modules/system/system.test | 43 +++++++++++ modules/user/user.module | 29 ++++++- modules/user/user.pages.inc | 4 +- modules/user/user.test | 84 +++++++++++++++++++-- 11 files changed, 346 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ef848fb11..e6ede7550 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,8 @@ +Drupal 7.35, 2015-03-18 +---------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-001. + Drupal 7.34, 2014-11-19 ---------------------- - Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-006. diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 744fc8fe7..b33f950f4 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.34'); +define('VERSION', '7.35'); /** * Core API compatibility. @@ -2497,6 +2497,26 @@ function _drupal_bootstrap_variables() { // Load bootstrap modules. require_once DRUPAL_ROOT . '/includes/module.inc'; module_load_all(TRUE); + + // Sanitize the destination parameter (which is often used for redirects) to + // prevent open redirect attacks leading to other domains. Sanitize both + // $_GET['destination'] and $_REQUEST['destination'] to protect code that + // relies on either, but do not sanitize $_POST to avoid interfering with + // unrelated form submissions. The sanitization happens here because + // url_is_external() requires the variable system to be available. + if (isset($_GET['destination']) || isset($_REQUEST['destination'])) { + require_once DRUPAL_ROOT . '/includes/common.inc'; + // If the destination is an external URL, remove it. + if (isset($_GET['destination']) && url_is_external($_GET['destination'])) { + unset($_GET['destination']); + unset($_REQUEST['destination']); + } + // If there's still something in $_REQUEST['destination'] that didn't come + // from $_GET, check it too. + if (isset($_REQUEST['destination']) && (!isset($_GET['destination']) || $_REQUEST['destination'] != $_GET['destination']) && url_is_external($_REQUEST['destination'])) { + unset($_REQUEST['destination']); + } + } } /** diff --git a/includes/common.inc b/includes/common.inc index 20cc82be1..ad2a34541 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2214,14 +2214,20 @@ function url($path = NULL, array $options = array()) { 'prefix' => '' ); + // A duplicate of the code from url_is_external() to avoid needing another + // function call, since performance inside url() is critical. if (!isset($options['external'])) { - // Return an external link if $path contains an allowed absolute URL. Only - // call the slow drupal_strip_dangerous_protocols() if $path contains a ':' - // before any / ? or #. Note: we could use url_is_external($path) here, but - // that would require another function call, and performance inside url() is - // critical. + // Return an external link if $path contains an allowed absolute URL. Avoid + // calling drupal_strip_dangerous_protocols() if there is any slash (/), + // hash (#) or question_mark (?) before the colon (:) occurrence - if any - + // as this would clearly mean it is not a URL. If the path starts with 2 + // slashes then it is always considered an external URL without an explicit + // protocol part. $colonpos = strpos($path, ':'); - $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path); + $options['external'] = (strpos($path, '//') === 0) + || ($colonpos !== FALSE + && !preg_match('![/?#]!', substr($path, 0, $colonpos)) + && drupal_strip_dangerous_protocols($path) == $path); } // Preserve the original path before altering or aliasing. @@ -2259,6 +2265,11 @@ function url($path = NULL, array $options = array()) { return $path . $options['fragment']; } + // Strip leading slashes from internal paths to prevent them becoming external + // URLs without protocol. /example.com should not be turned into + // //example.com. + $path = ltrim($path, '/'); + global $base_url, $base_secure_url, $base_insecure_url; // The base_url might be rewritten from the language rewrite in domain mode. @@ -2336,10 +2347,15 @@ function url($path = NULL, array $options = array()) { */ function url_is_external($path) { $colonpos = strpos($path, ':'); - // Avoid calling drupal_strip_dangerous_protocols() if there is any - // slash (/), hash (#) or question_mark (?) before the colon (:) - // occurrence - if any - as this would clearly mean it is not a URL. - return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path; + // Avoid calling drupal_strip_dangerous_protocols() if there is any slash (/), + // hash (#) or question_mark (?) before the colon (:) occurrence - if any - as + // this would clearly mean it is not a URL. If the path starts with 2 slashes + // then it is always considered an external URL without an explicit protocol + // part. + return (strpos($path, '//') === 0) + || ($colonpos !== FALSE + && !preg_match('![/?#]!', substr($path, 0, $colonpos)) + && drupal_strip_dangerous_protocols($path) == $path); } /** @@ -2636,7 +2652,10 @@ function drupal_deliver_html_page($page_callback_result) { // Keep old path for reference, and to allow forms to redirect to it. if (!isset($_GET['destination'])) { - $_GET['destination'] = $_GET['q']; + // Make sure that the current path is not interpreted as external URL. + if (!url_is_external($_GET['q'])) { + $_GET['destination'] = $_GET['q']; + } } $path = drupal_get_normal_path(variable_get('site_404', '')); @@ -2665,7 +2684,10 @@ function drupal_deliver_html_page($page_callback_result) { // Keep old path for reference, and to allow forms to redirect to it. if (!isset($_GET['destination'])) { - $_GET['destination'] = $_GET['q']; + // Make sure that the current path is not interpreted as external URL. + if (!url_is_external($_GET['q'])) { + $_GET['destination'] = $_GET['q']; + } } $path = drupal_get_normal_path(variable_get('site_403', '')); diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index f723c6301..14523f28c 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -546,3 +546,85 @@ class BootstrapOverrideServerVariablesTestCase extends DrupalUnitTestCase { } } } + +/** + * Tests for $_GET['destination'] and $_REQUEST['destination'] validation. + */ +class BootstrapDestinationTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'URL destination validation', + 'description' => 'Test that $_GET[\'destination\'] and $_REQUEST[\'destination\'] cannot contain external URLs.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * Tests that $_GET/$_REQUEST['destination'] only contain internal URLs. + * + * @see _drupal_bootstrap_variables() + * @see system_test_get_destination() + * @see system_test_request_destination() + */ + public function testDestination() { + $test_cases = array( + array( + 'input' => 'node', + 'output' => 'node', + 'message' => "Standard internal example node path is present in the 'destination' parameter.", + ), + array( + 'input' => '/example.com', + 'output' => '/example.com', + 'message' => 'Internal path with one leading slash is allowed.', + ), + array( + 'input' => '//example.com/test', + 'output' => '', + 'message' => 'External URL without scheme is not allowed.', + ), + array( + 'input' => 'example:test', + 'output' => 'example:test', + 'message' => 'Internal URL using a colon is allowed.', + ), + array( + 'input' => 'http://example.com', + 'output' => '', + 'message' => 'External URL is not allowed.', + ), + array( + 'input' => 'javascript:alert(0)', + 'output' => 'javascript:alert(0)', + 'message' => 'Javascript URL is allowed because it is treated as an internal URL.', + ), + ); + foreach ($test_cases as $test_case) { + // Test $_GET['destination']. + $this->drupalGet('system-test/get-destination', array('query' => array('destination' => $test_case['input']))); + $this->assertIdentical($test_case['output'], $this->drupalGetContent(), $test_case['message']); + // Test $_REQUEST['destination']. There's no form to submit to, so + // drupalPost() won't work here; this just tests a direct $_POST request + // instead. + $curl_parameters = array( + CURLOPT_URL => $this->getAbsoluteUrl('system-test/request-destination'), + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => 'destination=' . urlencode($test_case['input']), + CURLOPT_HTTPHEADER => array(), + ); + $post_output = $this->curlExec($curl_parameters); + $this->assertIdentical($test_case['output'], $post_output, $test_case['message']); + } + + // Make sure that 404 pages do not populate $_GET['destination'] with + // external URLs. + variable_set('site_404', 'system-test/get-destination'); + $this->drupalGet('http://example.com', array('external' => FALSE)); + $this->assertIdentical('', $this->drupalGetContent(), 'External URL is not allowed on 404 pages.'); + } +} diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index eebfdbe49..b8ad0cca5 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -209,7 +209,16 @@ class CommonURLUnitTest extends DrupalWebTestCase { // Test that drupal can recognize an absolute URL. Used to prevent attack vectors. $this->assertTrue(url_is_external($url), 'Correctly identified an external URL.'); + // External URL without an explicit protocol. + $url = '//drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; + $this->assertTrue(url_is_external($url), 'Correctly identified an external URL without a protocol part.'); + + // Internal URL starting with a slash. + $url = '/drupal.org'; + $this->assertFalse(url_is_external($url), 'Correctly identified an internal URL with a leading slash.'); + // Test the parsing of absolute URLs. + $url = 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; $result = array( 'path' => 'http://drupal.org/foo/bar', 'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''), @@ -349,6 +358,17 @@ class CommonURLUnitTest extends DrupalWebTestCase { $query = array($this->randomName(5) => $this->randomName(5)); $result = url($url, array('query' => $query)); $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result, 'External URL query string can be extended with a custom query string in $options.'); + + // Verify that an internal URL does not result in an external URL without + // protocol part. + $url = '/drupal.org'; + $result = url($url); + $this->assertTrue(strpos($result, '//') === FALSE, 'Internal URL does not turn into an external URL.'); + + // Verify that an external URL without protocol part is recognized as such. + $url = '//drupal.org'; + $result = url($url); + $this->assertEqual($url, $result, 'External URL without protocol is not altered.'); } } diff --git a/modules/simpletest/tests/system_test.module b/modules/simpletest/tests/system_test.module index 2eda351e9..c0eed034f 100644 --- a/modules/simpletest/tests/system_test.module +++ b/modules/simpletest/tests/system_test.module @@ -106,6 +106,20 @@ function system_test_menu() { 'type' => MENU_CALLBACK, ); + $items['system-test/get-destination'] = array( + 'title' => 'Test $_GET[\'destination\']', + 'page callback' => 'system_test_get_destination', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/request-destination'] = array( + 'title' => 'Test $_REQUEST[\'destination\']', + 'page callback' => 'system_test_request_destination', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -420,3 +434,27 @@ function system_test_authorize_init_page($page_title) { system_authorized_init('system_test_authorize_run', drupal_get_path('module', 'system_test') . '/system_test.module', array(), $page_title); drupal_goto($authorize_url); } + +/** + * Page callback to print out $_GET['destination'] for testing. + */ +function system_test_get_destination() { + if (isset($_GET['destination'])) { + print $_GET['destination']; + } + // No need to render the whole page, we are just interested in this bit of + // information. + exit; +} + +/** + * Page callback to print out $_REQUEST['destination'] for testing. + */ +function system_test_request_destination() { + if (isset($_REQUEST['destination'])) { + print $_REQUEST['destination']; + } + // No need to render the whole page, we are just interested in this bit of + // information. + exit; +} diff --git a/modules/statistics/statistics.test b/modules/statistics/statistics.test index 0498bb76b..7e038d612 100644 --- a/modules/statistics/statistics.test +++ b/modules/statistics/statistics.test @@ -414,7 +414,7 @@ class StatisticsAdminTestCase extends DrupalWebTestCase { $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); $this->drupalGet('admin/reports/visitors'); diff --git a/modules/system/system.test b/modules/system/system.test index aefbfbc46..dcc86e539 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -2825,3 +2825,46 @@ class SystemValidTokenTest extends DrupalUnitTestCase { return TRUE; } } + +/** + * Tests confirm form destinations. + */ +class ConfirmFormTest extends DrupalWebTestCase { + protected $admin_user; + + public static function getInfo() { + return array( + 'name' => 'Confirm form', + 'description' => 'Tests that the confirm form does not use external destinations.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp(); + + $this->admin_user = $this->drupalCreateUser(array('administer users')); + $this->drupalLogin($this->admin_user); + } + + /** + * Tests that the confirm form does not use external destinations. + */ + function testConfirmForm() { + $this->drupalGet('user/1/cancel'); + $this->assertCancelLinkUrl(url('user/1')); + $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'node'))); + $this->assertCancelLinkUrl(url('node')); + $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'http://example.com'))); + $this->assertCancelLinkUrl(url('user/1')); + } + + /** + * Asserts that a cancel link is present pointing to the provided URL. + */ + function assertCancelLinkUrl($url, $message = '', $group = 'Other') { + $links = $this->xpath('//a[normalize-space(text())=:label and @href=:url]', array(':label' => t('Cancel'), ':url' => $url)); + $message = ($message ? $message : format_string('Cancel link with url %url found.', array('%url' => $url))); + return $this->assertTrue(isset($links[0]), $message, $group); + } +} diff --git a/modules/user/user.module b/modules/user/user.module index 60f32a15f..bdfd36fa3 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -2335,7 +2335,7 @@ function user_external_login_register($name, $module) { */ function user_pass_reset_url($account) { $timestamp = REQUEST_TIME; - return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE)); + return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2357,7 +2357,7 @@ function user_pass_reset_url($account) { */ function user_cancel_url($account) { $timestamp = REQUEST_TIME; - return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE)); + return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2377,12 +2377,33 @@ function user_cancel_url($account) { * A UNIX timestamp, typically REQUEST_TIME. * @param int $login * The UNIX timestamp of the user's last login. + * @param int $uid + * The user ID of the user account. * * @return * A string that is safe for use in URLs and SQL statements. */ -function user_pass_rehash($password, $timestamp, $login) { - return drupal_hmac_base64($timestamp . $login, drupal_get_hash_salt() . $password); +function user_pass_rehash($password, $timestamp, $login, $uid) { + // Backwards compatibility: Try to determine a $uid if one was not passed. + // (Since $uid is a required parameter to this function, a PHP warning will + // be generated if it's not provided, which is an indication that the calling + // code should be updated. But the code below will try to generate a correct + // hash in the meantime.) + if (!isset($uid)) { + $uids = db_query_range('SELECT uid FROM {users} WHERE pass = :password AND login = :login AND uid > 0', 0, 2, array(':password' => $password, ':login' => $login))->fetchCol(); + // If exactly one user account matches the provided password and login + // timestamp, proceed with that $uid. + if (count($uids) == 1) { + $uid = reset($uids); + } + // Otherwise there is no safe hash to return, so return a random string + // that will never be treated as a valid token. + else { + return drupal_random_key(); + } + } + + return drupal_hmac_base64($timestamp . $login . $uid, drupal_get_hash_salt() . $password); } /** diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc index 8ec2348d6..6b7d38e22 100644 --- a/modules/user/user.pages.inc +++ b/modules/user/user.pages.inc @@ -126,7 +126,7 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.')); drupal_goto('user/password'); } - elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) { + elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { // First stage is a confirmation form, then login if ($action == 'login') { // Set the new user. @@ -523,7 +523,7 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') { // Basic validation of arguments. if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) { // Validate expiration and hashed password/login. - if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) { + if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { $edit = array( 'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE), ); diff --git a/modules/user/user.test b/modules/user/user.test index 03f0bbcd3..07be4c2c4 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -498,7 +498,7 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { // To attempt an expired password reset, create a password reset link as if // its request time was 60 seconds older than the allowed limit of timeout. $bogus_timestamp = REQUEST_TIME - variable_get('user_password_reset_timeout', 86400) - 60; - $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); } @@ -519,6 +519,74 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertFieldByName('name', $edit['name'], 'User name found.'); } + /** + * Make sure that users cannot forge password reset URLs of other users. + */ + function testResetImpersonation() { + // Make sure user 1 has a valid password, so it does not interfere with the + // test user accounts that are created below. + $account = user_load(1); + user_save($account, array('pass' => user_password())); + + // Create two identical user accounts except for the user name. They must + // have the same empty password, so we can't use $this->drupalCreateUser(). + $edit = array(); + $edit['name'] = $this->randomName(); + $edit['mail'] = $edit['name'] . '@example.com'; + $edit['status'] = 1; + + $user1 = user_save(drupal_anonymous_user(), $edit); + + $edit['name'] = $this->randomName(); + $user2 = user_save(drupal_anonymous_user(), $edit); + + // The password reset URL must not be valid for the second user when only + // the user ID is changed in the URL. + $reset_url = user_pass_reset_url($user1); + $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url); + $this->drupalGet($attack_reset_url); + $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + + // When legacy code calls user_pass_rehash() without providing the $uid + // parameter, neither password reset URL should be valid since it is + // impossible for the system to determine which user account the token was + // intended for. + $timestamp = REQUEST_TIME; + // Pass an explicit NULL for the $uid parameter of user_pass_rehash() + // rather than not passing it at all, to avoid triggering PHP warnings in + // the test. + $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE)); + $this->drupalGet($reset_url); + $this->assertNoText($user1->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url); + $this->drupalGet($attack_reset_url); + $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + + // To verify that user_pass_rehash() never returns a valid result in the + // above situation (even if legacy code also called it to attempt to + // validate the token, rather than just to generate the URL), check that a + // second call with the same parameters produces a different result. + $new_reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $this->assertNotEqual($reset_url_token, $new_reset_url_token); + + // However, when the duplicate account is removed, the password reset URL + // should be valid. + user_delete($user2->uid); + $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE)); + $this->drupalGet($reset_url); + $this->assertText($user1->name, 'The valid password reset page shows the user name.'); + $this->assertUrl($reset_url, array(), 'The user remains on the password reset login page.'); + $this->assertNoText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + } + } /** @@ -558,7 +626,7 @@ class UserCancelTestCase extends DrupalWebTestCase { // Attempt bogus account cancellation request confirmation. $timestamp = $account->login; - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertResponse(403, 'Bogus cancelling request rejected.'); $account = user_load($account->uid); $this->assertTrue($account->status == 1, 'User account was not canceled.'); @@ -631,14 +699,14 @@ class UserCancelTestCase extends DrupalWebTestCase { // Attempt bogus account cancellation request confirmation. $bogus_timestamp = $timestamp + 60; - $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Bogus cancelling request rejected.'); $account = user_load($account->uid); $this->assertTrue($account->status == 1, 'User account was not canceled.'); // Attempt expired account cancellation request confirmation. $bogus_timestamp = $timestamp - 86400 - 60; - $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Expired cancel account request rejected.'); $accounts = user_load_multiple(array($account->uid), array('status' => 1)); $this->assertTrue(reset($accounts), 'User account was not canceled.'); @@ -675,7 +743,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $account = user_load($account->uid, TRUE); $this->assertTrue($account->status == 0, 'User has been blocked.'); @@ -713,7 +781,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $account = user_load($account->uid, TRUE); $this->assertTrue($account->status == 0, 'User has been blocked.'); @@ -763,7 +831,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); // Confirm that user's content has been attributed to anonymous user. @@ -827,7 +895,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); // Confirm that user's content has been deleted. From 26d794fac95c9bfbdffe24cd9028d7e338717159 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 16:14:44 -0400 Subject: [PATCH 17/68] Issue #1904528 by Heine, GoddamnNoise: Language switcher (User interface text) Block generates invalid XHTML+RDFa 1.0 --- CHANGELOG.txt | 2 ++ includes/language.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b6d2297af..d90489357 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Changed the "lang" attribute on language links to "xml:lang" so it validates + as XHTML (minor markup change). - Prevented the form API from allowing arrays to be submitted for various form elements (such as textfields, textareas, and password fields). - Fixed a bug in the Contact module which caused the global user object to have diff --git a/includes/language.inc b/includes/language.inc index 803a63041..24267d8a1 100644 --- a/includes/language.inc +++ b/includes/language.inc @@ -297,7 +297,7 @@ function language_negotiation_get_switch_links($type, $path) { // Add support for WCAG 2.0's Language of Parts to add language identifiers. // http://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html foreach ($result as $langcode => $link) { - $result[$langcode]['attributes']['lang'] = $langcode; + $result[$langcode]['attributes']['xml:lang'] = $langcode; } if (!empty($result)) { From ab192786436ba867cc84ad4caca26372561845fd Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 16:31:39 -0400 Subject: [PATCH 18/68] Issue #1461732 by filijonka, Cottser, dcam, marcingy, swentel, udaksh: Fatal error when using the Comment module's "Unpublish comment containing keyword(s)" action --- CHANGELOG.txt | 2 ++ modules/comment/comment.module | 7 +++++-- modules/comment/comment.test | 37 +++++++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d90489357..113bd2f3e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed a fatal error that occurred when using the Comment module's "Unpublish + comment containing keyword(s)" action. - Changed the "lang" attribute on language links to "xml:lang" so it validates as XHTML (minor markup change). - Prevented the form API from allowing arrays to be submitted for various form diff --git a/modules/comment/comment.module b/modules/comment/comment.module index 2972474c0..6f0df6cae 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -2607,7 +2607,7 @@ function comment_unpublish_action($comment, $context = array()) { /** * Unpublishes a comment if it contains certain keywords. * - * @param $comment + * @param object $comment * Comment object to modify. * @param array $context * Array with components: @@ -2619,10 +2619,13 @@ function comment_unpublish_action($comment, $context = array()) { * @see comment_unpublish_by_keyword_action_submit() */ function comment_unpublish_by_keyword_action($comment, $context) { + $node = node_load($comment->nid); + $build = comment_view($comment, $node); + $text = drupal_render($build); foreach ($context['keywords'] as $keyword) { - $text = drupal_render($comment); if (strpos($text, $keyword) !== FALSE) { $comment->status = COMMENT_NOT_PUBLISHED; + comment_save($comment); watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject)); break; } diff --git a/modules/comment/comment.test b/modules/comment/comment.test index 9e69ba628..dc7aad3e1 100644 --- a/modules/comment/comment.test +++ b/modules/comment/comment.test @@ -13,7 +13,7 @@ class CommentHelperCase extends DrupalWebTestCase { function setUp() { parent::setUp('comment', 'search'); // Create users and test node. - $this->admin_user = $this->drupalCreateUser(array('administer content types', 'administer comments', 'administer blocks')); + $this->admin_user = $this->drupalCreateUser(array('administer content types', 'administer comments', 'administer blocks', 'administer actions')); $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'create article content', 'edit own comments')); $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->uid)); } @@ -1973,6 +1973,41 @@ class CommentActionsTestCase extends CommentHelperCase { $this->clearWatchdog(); } + /** + * Tests the unpublish comment by keyword action. + */ + public function testCommentUnpublishByKeyword() { + $this->drupalLogin($this->admin_user); + $callback = 'comment_unpublish_by_keyword_action'; + $hash = drupal_hash_base64($callback); + $comment_text = $keywords = $this->randomName(); + $edit = array( + 'actions_label' => $callback, + 'keywords' => $keywords, + ); + + $this->drupalPost("admin/config/system/actions/configure/$hash", $edit, t('Save')); + + $action = db_query("SELECT aid, type, callback, parameters, label FROM {actions} WHERE callback = :callback", array(':callback' => $callback))->fetchObject(); + + $this->assertTrue($action, 'The action could be loaded.'); + + $comment = $this->postComment($this->node, $comment_text, $this->randomName()); + + // Load the full comment so that status is available. + $comment = comment_load($comment->id); + + $this->assertTrue($comment->status == COMMENT_PUBLISHED, 'The comment status was set to published.'); + + comment_unpublish_by_keyword_action($comment, array('keywords' => array($keywords))); + + // We need to make sure that the comment has been saved with status + // unpublished. + $this->assertEqual(comment_load($comment->cid)->status, COMMENT_NOT_PUBLISHED, 'Comment was unpublished.'); + $this->assertWatchdogMessage('Unpublished comment %subject.', array('%subject' => $comment->subject), 'Found watchdog message.'); + $this->clearWatchdog(); + } + /** * Verify that a watchdog message has been entered. * From 731a540f839d9437fa9317d1e6405dd478621628 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 16:59:36 -0400 Subject: [PATCH 19/68] Issue #2293767 by tstoeckler: Allow PSR-4 test classes to be used in Drupal 7. --- CHANGELOG.txt | 2 + modules/simpletest/simpletest.module | 70 ++++++++++++------- modules/simpletest/simpletest.test | 9 ++- modules/simpletest/src/Tests/PSR4WebTest.php | 18 +++++ .../tests/psr_4_test/psr_4_test.info | 6 ++ .../tests/psr_4_test/psr_4_test.module | 1 + .../psr_4_test/src/Tests/ExampleTest.php | 18 +++++ .../src/Tests/Nested/NestedExampleTest.php | 18 +++++ 8 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 modules/simpletest/src/Tests/PSR4WebTest.php create mode 100644 modules/simpletest/tests/psr_4_test/psr_4_test.info create mode 100644 modules/simpletest/tests/psr_4_test/psr_4_test.module create mode 100644 modules/simpletest/tests/psr_4_test/src/Tests/ExampleTest.php create mode 100644 modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 113bd2f3e..534c53058 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Changed the Simpletest module to allow PSR-4 test classes to be used in + Drupal 7. - Fixed a fatal error that occurred when using the Comment module's "Unpublish comment containing keyword(s)" action. - Changed the "lang" attribute on language links to "xml:lang" so it validates diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index 3103af0e8..91f0f9065 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -328,25 +328,32 @@ function simpletest_test_get_all() { // Also discover PSR-0 test classes, if the PHP version allows it. if (version_compare(PHP_VERSION, '5.3') > 0) { - // Select all PSR-0 classes in the Tests namespace of all modules. + // Select all PSR-0 and PSR-4 classes in the Tests namespace of all + // modules. $system_list = db_query("SELECT name, filename FROM {system}")->fetchAllKeyed(); foreach ($system_list as $name => $filename) { - // Build directory in which the test files would reside. - $tests_dir = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/Drupal/' . $name . '/Tests'; - // Scan it for test files if it exists. - if (is_dir($tests_dir)) { - $files = file_scan_directory($tests_dir, '/.*\.php/'); - if (!empty($files)) { - $basedir = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/'; - foreach ($files as $file) { - // Convert the file name into the namespaced class name. - $replacements = array( - '/' => '\\', - $basedir => '', - '.php' => '', - ); - $classes[] = strtr($file->uri, $replacements); + $module_dir = DRUPAL_ROOT . '/' . dirname($filename); + // Search both the 'lib/Drupal/mymodule' directory (for PSR-0 classes) + // and the 'src' directory (for PSR-4 classes). + foreach(array('lib/Drupal/' . $name, 'src') as $subdir) { + // Build directory in which the test files would reside. + $tests_dir = $module_dir . '/' . $subdir . '/Tests'; + // Scan it for test files if it exists. + if (is_dir($tests_dir)) { + $files = file_scan_directory($tests_dir, '/.*\.php/'); + if (!empty($files)) { + foreach ($files as $file) { + // Convert the file name into the namespaced class name. + $replacements = array( + '/' => '\\', + $module_dir . '/' => '', + 'lib/' => '', + 'src/' => 'Drupal\\' . $name . '\\', + '.php' => '', + ); + $classes[] = strtr($file->uri, $replacements); + } } } } @@ -406,17 +413,20 @@ function simpletest_classloader_register() { // Only register PSR-0 class loading if we are on PHP 5.3 or higher. if (version_compare(PHP_VERSION, '5.3') > 0) { - spl_autoload_register('_simpletest_autoload_psr0'); + spl_autoload_register('_simpletest_autoload_psr4_psr0'); } } /** - * Autoload callback to find PSR-0 test classes. + * Autoload callback to find PSR-4 and PSR-0 test classes. + * + * Looks in the 'src/Tests' and in the 'lib/Drupal/mymodule/Tests' directory of + * modules for the class. * * This will only work on classes where the namespace is of the pattern * "Drupal\$extension\Tests\.." */ -function _simpletest_autoload_psr0($class) { +function _simpletest_autoload_psr4_psr0($class) { // Static cache for extension paths. // This cache is lazily filled as soon as it is needed. @@ -446,14 +456,26 @@ function _simpletest_autoload_psr0($class) { $namespace = substr($class, 0, $nspos); $classname = substr($class, $nspos + 1); - // Build the filepath where we expect the class to be defined. - $path = dirname($extensions[$extension]) . '/lib/' . - str_replace('\\', '/', $namespace) . '/' . + // Try the PSR-4 location first, and the PSR-0 location as a fallback. + // Build the PSR-4 filepath where we expect the class to be defined. + $psr4_path = dirname($extensions[$extension]) . '/src/' . + str_replace('\\', '/', substr($namespace, strlen('Drupal\\' . $extension . '\\'))) . '/' . str_replace('_', '/', $classname) . '.php'; // Include the file, if it does exist. - if (file_exists($path)) { - include $path; + if (file_exists($psr4_path)) { + include $psr4_path; + } + else { + // Build the PSR-0 filepath where we expect the class to be defined. + $psr0_path = dirname($extensions[$extension]) . '/lib/' . + str_replace('\\', '/', $namespace) . '/' . + str_replace('_', '/', $classname) . '.php'; + + // Include the file, if it does exist. + if (file_exists($psr0_path)) { + include $psr0_path; + } } } } diff --git a/modules/simpletest/simpletest.test b/modules/simpletest/simpletest.test index dde162ec7..f22ef9557 100644 --- a/modules/simpletest/simpletest.test +++ b/modules/simpletest/simpletest.test @@ -703,7 +703,9 @@ class SimpleTestDiscoveryTestCase extends DrupalWebTestCase { $classes_all = simpletest_test_get_all(); foreach (array( 'Drupal\\simpletest\\Tests\\PSR0WebTest', + 'Drupal\\simpletest\\Tests\\PSR4WebTest', 'Drupal\\psr_0_test\\Tests\\ExampleTest', + 'Drupal\\psr_4_test\\Tests\\ExampleTest', ) as $class) { $this->assert(!empty($classes_all['SimpleTest'][$class]), t('Class @class must be discovered by simpletest_test_get_all().', array('@class' => $class))); } @@ -726,15 +728,20 @@ class SimpleTestDiscoveryTestCase extends DrupalWebTestCase { // Don't expect PSR-0 tests to be discovered on older PHP versions. return; } - // This one is provided by simpletest itself via PSR-0. + // These are provided by simpletest itself via PSR-0 and PSR-4. $this->assertText('PSR0 web test'); + $this->assertText('PSR4 web test'); $this->assertText('PSR0 example test: PSR-0 in disabled modules.'); + $this->assertText('PSR4 example test: PSR-4 in disabled modules.'); $this->assertText('PSR0 example test: PSR-0 in nested subfolders.'); + $this->assertText('PSR4 example test: PSR-4 in nested subfolders.'); // Test each test individually. foreach (array( 'Drupal\\psr_0_test\\Tests\\ExampleTest', 'Drupal\\psr_0_test\\Tests\\Nested\\NestedExampleTest', + 'Drupal\\psr_4_test\\Tests\\ExampleTest', + 'Drupal\\psr_4_test\\Tests\\Nested\\NestedExampleTest', ) as $class) { $this->drupalGet('admin/config/development/testing'); $edit = array($class => TRUE); diff --git a/modules/simpletest/src/Tests/PSR4WebTest.php b/modules/simpletest/src/Tests/PSR4WebTest.php new file mode 100644 index 000000000..24c8d8999 --- /dev/null +++ b/modules/simpletest/src/Tests/PSR4WebTest.php @@ -0,0 +1,18 @@ + 'PSR4 web test', + 'description' => 'We want to assert that this PSR-4 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff --git a/modules/simpletest/tests/psr_4_test/psr_4_test.info b/modules/simpletest/tests/psr_4_test/psr_4_test.info new file mode 100644 index 000000000..3104082b6 --- /dev/null +++ b/modules/simpletest/tests/psr_4_test/psr_4_test.info @@ -0,0 +1,6 @@ +name = PSR-4 Test cases +description = Test classes to be discovered by simpletest. +core = 7.x + +hidden = TRUE +package = Testing diff --git a/modules/simpletest/tests/psr_4_test/psr_4_test.module b/modules/simpletest/tests/psr_4_test/psr_4_test.module new file mode 100644 index 000000000..b3d9bbc7f --- /dev/null +++ b/modules/simpletest/tests/psr_4_test/psr_4_test.module @@ -0,0 +1 @@ + 'PSR4 example test: PSR-4 in disabled modules.', + 'description' => 'We want to assert that this test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff --git a/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php b/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php new file mode 100644 index 000000000..ff3ac29d4 --- /dev/null +++ b/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php @@ -0,0 +1,18 @@ + 'PSR4 example test: PSR-4 in nested subfolders.', + 'description' => 'We want to assert that this PSR-4 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} From c202196bbb677d36d4244e0eaa3ca2ab223b024f Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 17:02:11 -0400 Subject: [PATCH 20/68] Issue #2439287 by jmsv23, jonathan_hunt: Fix typo in inline docs for field_sql_storage_field_storage_write(). --- modules/field/field.api.php | 2 +- .../field/modules/field_sql_storage/field_sql_storage.module | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/field/field.api.php b/modules/field/field.api.php index e8361c22e..f9370912d 100644 --- a/modules/field/field.api.php +++ b/modules/field/field.api.php @@ -1897,7 +1897,7 @@ function hook_field_storage_write($entity_type, $entity, $op, $fields) { $items = (array) $entity->{$field_name}[$langcode]; $delta_count = 0; foreach ($items as $delta => $item) { - // We now know we have someting to insert. + // We now know we have something to insert. $do_insert = TRUE; $record = array( 'entity_type' => $entity_type, diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/modules/field/modules/field_sql_storage/field_sql_storage.module index 7ab4ee5c9..c7201dd78 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -465,7 +465,7 @@ function field_sql_storage_field_storage_write($entity_type, $entity, $op, $fiel $items = (array) $entity->{$field_name}[$langcode]; $delta_count = 0; foreach ($items as $delta => $item) { - // We now know we have someting to insert. + // We now know we have something to insert. $do_insert = TRUE; $record = array( 'entity_type' => $entity_type, From 5b781afa6642536e449ce2595afffdb98f9e5d18 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 17:09:59 -0400 Subject: [PATCH 21/68] Issue #2067323 by Valentine94, geerlingguy, jessebeach, vlad.dancer: Don't show empty vertical tabs area if all vertical tabs are hidden --- misc/vertical-tabs.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/misc/vertical-tabs.js b/misc/vertical-tabs.js index ebfaa4f7f..3aa0f6f9e 100644 --- a/misc/vertical-tabs.js +++ b/misc/vertical-tabs.js @@ -134,6 +134,8 @@ Drupal.verticalTab.prototype = { tabShow: function () { // Display the tab. this.item.show(); + // Show the vertical tabs. + this.item.closest('.vertical-tabs').show(); // Update .first marker for items. We need recurse from parent to retain the // actual DOM element order as jQuery implements sortOrder, but not as public // method. @@ -164,6 +166,10 @@ Drupal.verticalTab.prototype = { if ($firstTab.length) { $firstTab.data('verticalTab').focus(); } + // Hide the vertical tabs (if no tabs remain). + else { + this.item.closest('.vertical-tabs').hide(); + } return this; } }; From f7cda605f66446c5661a99cfdb343ebb32dd0441 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 17:24:26 -0400 Subject: [PATCH 22/68] Issue #2381839 by klausi, Damien Tournoud: Changed date format for Last-Modified header breaks caching for certain Varnish/Nginx configurations. --- CHANGELOG.txt | 2 ++ includes/bootstrap.inc | 23 ++++------------------- modules/simpletest/tests/bootstrap.test | 2 ++ 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 534c53058..8b4d06ba2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Stopped sending ETag and Last-Modified headers for uncached page requests, + since they break caching for certain Varnish and Nginx configurations. - Changed the Simpletest module to allow PSR-4 test classes to be used in Drupal 7. - Fixed a fatal error that occurred when using the Comment module's "Unpublish diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index b1dd6eb1f..922fd094e 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1246,23 +1246,10 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE) * fresh page on every request. This prevents authenticated users from seeing * locally cached pages. * - * Also give each page a unique ETag. This will force clients to include both - * an If-Modified-Since header and an If-None-Match header when doing - * conditional requests for the page (required by RFC 2616, section 13.3.4), - * making the validation more robust. This is a workaround for a bug in Mozilla - * Firefox that is triggered when Drupal's caching is enabled and the user - * accesses Drupal via an HTTP proxy (see - * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated - * user requests a page, and then logs out and requests the same page again, - * Firefox may send a conditional request based on the page that was cached - * locally when the user was logged in. If this page did not have an ETag - * header, the request only contains an If-Modified-Since header. The date will - * be recent, because with authenticated users the Last-Modified header always - * refers to the time of the request. If the user accesses Drupal via a proxy - * server, and the proxy already has a cached copy of the anonymous page with an - * older Last-Modified date, the proxy may respond with 304 Not Modified, making - * the client think that the anonymous and authenticated pageviews are - * identical. + * ETag and Last-Modified headers are not set per default for authenticated + * users so that browsers do not send If-Modified-Since headers from + * authenticated user pages. drupal_serve_page_from_cache() will set appropriate + * ETag and Last-Modified headers for cached pages. * * @see drupal_page_set_cache() */ @@ -1275,9 +1262,7 @@ function drupal_page_header() { $default_headers = array( 'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT', - 'Last-Modified' => gmdate(DATE_RFC7231, REQUEST_TIME), 'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0', - 'ETag' => '"' . REQUEST_TIME . '"', ); drupal_send_headers($default_headers); } diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index 14523f28c..3489062f1 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -153,6 +153,8 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase { $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); $this->assertResponse(200, 'Conditional request returned 200 OK for authenticated user.'); $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Absense of Page was not cached.'); + $this->assertFalse($this->drupalGetHeader('ETag'), 'ETag HTTP headers are not present for logged in users.'); + $this->assertFalse($this->drupalGetHeader('Last-Modified'), 'Last-Modified HTTP headers are not present for logged in users.'); } /** From ac0f69f1dab60057909a8091a522afbdc7c86910 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 17:39:52 -0400 Subject: [PATCH 23/68] Issue #1303412 by nod_, droplet, Valentine94, KarenS, sahuni: Cannot drag in or out of 'Hidden' on the 'Manage Display' page under certain circumstances --- modules/field_ui/field_ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/field_ui/field_ui.js b/modules/field_ui/field_ui.js index 65b28d049..c2e8978db 100644 --- a/modules/field_ui/field_ui.js +++ b/modules/field_ui/field_ui.js @@ -168,7 +168,7 @@ Drupal.fieldUIOverview = { var dragObject = this; var row = dragObject.rowObject.element; var rowHandler = $(row).data('fieldUIRowHandler'); - if (rowHandler !== undefined) { + if (typeof rowHandler !== 'undefined') { var regionRow = $(row).prevAll('tr.region-message').get(0); var region = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2'); @@ -319,7 +319,7 @@ Drupal.fieldUIDisplayOverview.field.prototype = { if (currentValue == 'hidden') { // Restore the formatter back to the default formatter. Pseudo-fields do // not have default formatters, we just return to 'visible' for those. - var value = (this.defaultFormatter != undefined) ? this.defaultFormatter : 'visible'; + var value = (typeof this.defaultFormatter !== 'undefined') ? this.defaultFormatter : this.$formatSelect.find('option').val(); } break; From 9ddfc4697d91b75823bad166666e27f433517b80 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 17:47:44 -0400 Subject: [PATCH 24/68] Issue #1823306 by mkalkbrenner, p-neyens, webflo, swentel, zuuperman: Language code is missing from $context when hook_field_attach_view_alter() is invoked from field_view_field() --- CHANGELOG.txt | 2 ++ modules/field/field.module | 1 + modules/field/tests/field.test | 3 ++- modules/field/tests/field_test.module | 4 ++++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8b4d06ba2..2a12d806a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed missing language code in hook_field_attach_view_alter() when it is + invoked from field_view_field(). - Stopped sending ETag and Last-Modified headers for uncached page requests, since they break caching for certain Varnish and Nginx configurations. - Changed the Simpletest module to allow PSR-4 test classes to be used in diff --git a/modules/field/field.module b/modules/field/field.module index a593e5111..e4039786e 100644 --- a/modules/field/field.module +++ b/modules/field/field.module @@ -894,6 +894,7 @@ function field_view_field($entity_type, $entity, $field_name, $display = array() 'entity' => $entity, 'view_mode' => '_custom', 'display' => $display, + 'language' => $langcode, ); drupal_alter('field_attach_view', $result, $context); diff --git a/modules/field/tests/field.test b/modules/field/tests/field.test index b279d6a8f..adf2413ba 100644 --- a/modules/field/tests/field.test +++ b/modules/field/tests/field.test @@ -2206,11 +2206,12 @@ class FieldDisplayAPITestCase extends FieldTestCase { 'alter' => TRUE, ), ); - $output = field_view_field('test_entity', $this->entity, $this->field_name, $display); + $output = field_view_field('test_entity', $this->entity, $this->field_name, $display, LANGUAGE_NONE); $this->drupalSetContent(drupal_render($output)); $setting = $display['settings']['test_formatter_setting_multiple']; $this->assertNoText($this->label, 'Label was not displayed.'); $this->assertText('field_test_field_attach_view_alter', 'Alter fired, display passed.'); + $this->assertText('field language is ' . LANGUAGE_NONE, 'Language is placed onto the context.'); $array = array(); foreach ($this->values as $delta => $value) { $array[] = $delta . ':' . $value['value']; diff --git a/modules/field/tests/field_test.module b/modules/field/tests/field_test.module index 9daa2c305..7e9bba0d8 100644 --- a/modules/field/tests/field_test.module +++ b/modules/field/tests/field_test.module @@ -220,6 +220,10 @@ function field_test_field_attach_view_alter(&$output, $context) { if (!empty($context['display']['settings']['alter'])) { $output['test_field'][] = array('#markup' => 'field_test_field_attach_view_alter'); } + + if (isset($output['test_field'])) { + $output['test_field'][] = array('#markup' => 'field language is ' . $context['language']); + } } /** From 25ddf78e41129890dd671b9c8c4f817bfc411200 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 18:18:17 -0400 Subject: [PATCH 25/68] Issue #495930 by fietserwin: Translated strings not correctly marked in the administrative interface when the default language is not English --- includes/locale.inc | 13 +++++++++---- modules/locale/locale.admin.inc | 6 +++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/includes/locale.inc b/includes/locale.inc index 00ac18805..c7f958385 100644 --- a/includes/locale.inc +++ b/includes/locale.inc @@ -1931,7 +1931,7 @@ function _locale_translate_seek() { $groups[$string['group']], array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '
' . $string['location'] . ''), $string['context'], - array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'), + array('data' => _locale_translate_language_list($string, $limit_language), 'align' => 'center'), array('data' => l(t('edit'), "admin/config/regional/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')), array('data' => l(t('delete'), "admin/config/regional/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')), ); @@ -2126,16 +2126,21 @@ function _locale_rebuild_js($langcode = NULL) { /** * List languages in search result table */ -function _locale_translate_language_list($translation, $limit_language) { +function _locale_translate_language_list($string, $limit_language) { // Add CSS. drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); + // Include both translated and not yet translated target languages in the + // list. The source language is English for built-in strings and the default + // language for other strings. $languages = language_list(); - unset($languages['en']); + $default = language_default(); + $omit = $string['group'] == 'default' ? 'en' : $default->language; + unset($languages[$omit]); $output = ''; foreach ($languages as $langcode => $language) { if (!$limit_language || $limit_language == $langcode) { - $output .= (!empty($translation[$langcode])) ? $langcode . ' ' : "$langcode "; + $output .= (!empty($string['languages'][$langcode])) ? $langcode . ' ' : "$langcode "; } } diff --git a/modules/locale/locale.admin.inc b/modules/locale/locale.admin.inc index b736f79b7..e813962d0 100644 --- a/modules/locale/locale.admin.inc +++ b/modules/locale/locale.admin.inc @@ -1139,11 +1139,11 @@ function locale_translate_edit_form($form, &$form_state, $lid) { '#value' => $source->location ); - // Include default form controls with empty values for all languages. - // This ensures that the languages are always in the same order in forms. + // Include both translated and not yet translated target languages in the + // list. The source language is English for built-in strings and the default + // language for other strings. $languages = language_list(); $default = language_default(); - // We don't need the default language value, that value is in $source. $omit = $source->textgroup == 'default' ? 'en' : $default->language; unset($languages[($omit)]); $form['translations'] = array('#tree' => TRUE); From 2ace1905b59b779a87e3ad1a125d5bb52193aa61 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 22:46:20 -0400 Subject: [PATCH 26/68] Issue #1882774 by David_Rothstein, iva2k: User pictures are lost when system_update_7061 merges files into {file_managed}. --- CHANGELOG.txt | 2 ++ modules/user/user.install | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2a12d806a..30948b625 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused user + pictures to be lost. - Fixed missing language code in hook_field_attach_view_alter() when it is invoked from field_view_field(). - Stopped sending ETag and Last-Modified headers for uncached page requests, diff --git a/modules/user/user.install b/modules/user/user.install index 4e1a3c22d..728e00468 100644 --- a/modules/user/user.install +++ b/modules/user/user.install @@ -356,11 +356,13 @@ function user_update_dependencies() { 'filter' => 7000, ); - // user_update_7012() uses the file API, which relies on the {file_managed} - // table, so it must run after system_update_7034(), which creates that - // table. + // user_update_7012() uses the file API and inserts records into the + // {file_managed} table, so it therefore must run after system_update_7061(), + // which inserts files with specific IDs into the table and therefore relies + // on the table being empty (otherwise it would accidentally overwrite + // existing records). $dependencies['user'][7012] = array( - 'system' => 7034, + 'system' => 7061, ); // user_update_7013() uses the file usage API, which relies on the From 9dafd622f00485a1889bfd22bcf1291e2aed647b Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Sun, 29 Mar 2015 22:56:35 -0400 Subject: [PATCH 27/68] Issue #1404050 by JamesOakley, David_Rothstein: system_update_7061 breaks private files by leaving one too many forward slashes in protocol of migrated URIs --- CHANGELOG.txt | 2 ++ modules/system/system.install | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 30948b625..26e05739c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused private + files to be inaccessible. - Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused user pictures to be lost. - Fixed missing language code in hook_field_attach_view_alter() when it is diff --git a/modules/system/system.install b/modules/system/system.install index 43c7383a3..64c989a61 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -2854,7 +2854,14 @@ function system_update_7061(&$sandbox) { // We will convert filepaths to URI using the default scheme // and stripping off the existing file directory path. $file['uri'] = $scheme . preg_replace('!^' . preg_quote($basename) . '!', '', $file['filepath']); - $file['uri'] = file_stream_wrapper_uri_normalize($file['uri']); + // Normalize the URI but don't call file_stream_wrapper_uri_normalize() + // directly, since that is a higher-level API function which invokes + // hooks while validating the scheme, and those will not work during + // the upgrade. Instead, use a simpler version that just assumes the + // scheme from above is already valid. + if (($file_uri_scheme = file_uri_scheme($file['uri'])) && ($file_uri_target = file_uri_target($file['uri']))) { + $file['uri'] = $file_uri_scheme . '://' . $file_uri_target; + } unset($file['filepath']); // Insert into the file_managed table. // Each fid should only be stored once in file_managed. From ea48131869663312ab598d281197bc96bc8c1ac7 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 00:29:14 -0400 Subject: [PATCH 28/68] Issue #2170453 by Darren Oh, mikeryan, Fabianx: Ignore case in code registry lookups --- includes/bootstrap.inc | 11 ++++--- modules/simpletest/tests/bootstrap.test | 29 +++++++++++++++++++ .../drupal_autoload_test.info | 8 +++++ .../drupal_autoload_test.module | 6 ++++ .../drupal_autoload_test_class.inc | 11 +++++++ .../drupal_autoload_test_interface.inc | 11 +++++++ 6 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info create mode 100644 modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module create mode 100644 modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc create mode 100644 modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_interface.inc diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 922fd094e..6a0ff7bc9 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -3167,10 +3167,13 @@ function _registry_check_code($type, $name = NULL) { // This function may get called when the default database is not active, but // there is no reason we'd ever want to not use the default database for // this query. - $file = Database::getConnection('default', 'default')->query("SELECT filename FROM {registry} WHERE name = :name AND type = :type", array( - ':name' => $name, - ':type' => $type, - )) + $file = Database::getConnection('default', 'default') + ->select('registry', 'r', array('target' => 'default')) + ->fields('r', array('filename')) + // Use LIKE here to make the query case-insensitive. + ->condition('r.name', db_like($name), 'LIKE') + ->condition('r.type', $type) + ->execute() ->fetchField(); // Flag that we've run a lookup query and need to update the cache. diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index 3489062f1..ece1cd9e9 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -288,6 +288,35 @@ class BootstrapVariableTestCase extends DrupalWebTestCase { } +/** + * Tests the auto-loading behavior of the code registry. + */ +class BootstrapAutoloadTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Code registry', + 'description' => 'Test that the code registry functions correctly.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('drupal_autoload_test'); + } + + /** + * Tests that autoloader name matching is not case sensitive. + */ + function testAutoloadCase() { + // Test interface autoloader. + $this->assertTrue(drupal_autoload_interface('drupalautoloadtestinterface'), 'drupal_autoload_interface() recognizes DrupalAutoloadTestInterface in lower case.'); + // Test class autoloader. + $this->assertTrue(drupal_autoload_class('drupalautoloadtestclass'), 'drupal_autoload_class() recognizes DrupalAutoloadTestClass in lower case.'); + } + +} + /** * Test hook_boot() and hook_exit(). */ diff --git a/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info new file mode 100644 index 000000000..4660de367 --- /dev/null +++ b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info @@ -0,0 +1,8 @@ +name = "Drupal code registry test" +description = "Support module for testing the code registry." +files[] = drupal_autoload_test_interface.inc +files[] = drupal_autoload_test_class.inc +package = Testing +version = VERSION +core = 7.x +hidden = TRUE diff --git a/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module new file mode 100644 index 000000000..37aa94eb8 --- /dev/null +++ b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module @@ -0,0 +1,6 @@ + Date: Mon, 30 Mar 2015 00:46:00 -0400 Subject: [PATCH 29/68] Issue #779482 by mikeytown2, sun, dalin, cam8001, segi, alexpott, Boobaa, Sweetchuck, jbrown, quicksketch: Installation failure when opcode cache is enabled --- CHANGELOG.txt | 1 + includes/bootstrap.inc | 31 +++++++++++++++++++++++++++++++ includes/install.inc | 7 +++++++ 3 files changed, 39 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 26e05739c..4060fcba6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,7 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed installation failures when an opcode cache is enabled. - Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused private files to be inaccessible. - Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused user diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 6a0ff7bc9..ac6fb143f 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -3511,3 +3511,34 @@ function drupal_check_memory_limit($required, $memory_limit = NULL) { // - The memory limit is greater than the memory required for the operation. return ((!$memory_limit) || ($memory_limit == -1) || (parse_size($memory_limit) >= parse_size($required))); } + +/** + * Invalidates a PHP file from any active opcode caches. + * + * If the opcode cache does not support the invalidation of individual files, + * the entire cache will be flushed. + * + * @param string $filepath + * The absolute path of the PHP file to invalidate. + */ +function drupal_clear_opcode_cache($filepath) { + if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50300) { + // Below PHP 5.3, clearstatcache does not accept any function parameters. + clearstatcache(); + } + else { + clearstatcache(TRUE, $filepath); + } + + // Zend OPcache. + if (function_exists('opcache_invalidate')) { + opcache_invalidate($filepath, TRUE); + } + // APC. + if (function_exists('apc_delete_file')) { + // apc_delete_file() throws a PHP warning in case the specified file was + // not compiled yet. + // @see http://php.net/apc-delete-file + @apc_delete_file($filepath); + } +} diff --git a/includes/install.inc b/includes/install.inc index f13ee8ac2..2b55589f8 100644 --- a/includes/install.inc +++ b/includes/install.inc @@ -653,6 +653,13 @@ function drupal_rewrite_settings($settings = array(), $prefix = '') { if ($fp && fwrite($fp, $buffer) === FALSE) { throw new Exception(st('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file))); } + else { + // The existing settings.php file might have been included already. In + // case an opcode cache is enabled, the rewritten contents of the file + // will not be reflected in this process. Ensure to invalidate the file + // in case an opcode cache is enabled. + drupal_clear_opcode_cache(DRUPAL_ROOT . '/' . $settings_file); + } } else { throw new Exception(st('Failed to open %settings. Verify the file permissions.', array('%settings' => $default_settings))); From 5a1ad44daed5ac2d53eb4332a683549b55579570 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 01:18:18 -0400 Subject: [PATCH 30/68] Issue #375062 by cs_shadow, David_Rothstein, mondrake, juampy, theunraveler, hswong3i, smk-ka, fietserwin: "imagecolorsforindex() Color index nnn out of range in GDToolkit" message sometimes appears --- .../image-test-transparent-out-of-range.gif | Bin 0 -> 183 bytes modules/simpletest/tests/image.test | 44 ++++++++++++++-- modules/system/image.gd.inc | 49 ++++++++++++++---- 3 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 modules/simpletest/files/image-test-transparent-out-of-range.gif diff --git a/modules/simpletest/files/image-test-transparent-out-of-range.gif b/modules/simpletest/files/image-test-transparent-out-of-range.gif new file mode 100644 index 0000000000000000000000000000000000000000..a54df7a93ce2346e19414536a3a8eb555f64f8d1 GIT binary patch literal 183 zcmZ?wbhEHb)L;-{IK%)3|AFLxAOXbxK{N;`{$ycfU|?g=0dYWT8JKGb=t5n!+_BT}-q<=e+CBR^E^YIlLQph@XE_%@DZj;qn)Ttdkecd;6*8 z&7vbKTMxVyio3Ti!AH!1F|}w0cii&h&$lQ1y?XKKNAtb1$K0P(m*wSGW;TYIbQ;uG YWoxN4^!3Y5Oq@KWciMF6B2ETt0P&+sLI3~& literal 0 HcmV?d00001 diff --git a/modules/simpletest/tests/image.test b/modules/simpletest/tests/image.test index dc95a6e2f..849702216 100644 --- a/modules/simpletest/tests/image.test +++ b/modules/simpletest/tests/image.test @@ -261,6 +261,7 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { */ function testManipulations() { // If GD isn't available don't bother testing this. + module_load_include('inc', 'system', 'image.gd'); if (!function_exists('image_gd_check_settings') || !image_gd_check_settings()) { $this->pass(t('Image manipulations for the GD toolkit were skipped because the GD toolkit is not available.')); return; @@ -379,7 +380,7 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { array_fill(0, 3, 76) + array(3 => 0), array_fill(0, 3, 149) + array(3 => 0), array_fill(0, 3, 29) + array(3 => 0), - array_fill(0, 3, 0) + array(3 => 127) + array_fill(0, 3, 225) + array(3 => 127) ), ), ); @@ -394,11 +395,14 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { continue 2; } - // Transparent GIFs and the imagefilter function don't work together. - // There is a todo in image.gd.inc to correct this. + // All images should be converted to truecolor when loaded. + $image_truecolor = imageistruecolor($image->resource); + $this->assertTrue($image_truecolor, format_string('Image %file after load is a truecolor image.', array('%file' => $file))); + if ($image->info['extension'] == 'gif') { if ($op == 'desaturate') { - $values['corners'][3] = $this->white; + // Transparent GIFs and the imagefilter function don't work together. + $values['corners'][3][3] = 0; } } @@ -451,7 +455,8 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { $directory = file_default_scheme() . '://imagetests'; file_prepare_directory($directory, FILE_CREATE_DIRECTORY); - image_save($image, $directory . '/' . $op . '.' . $image->info['extension']); + $file_path = $directory . '/' . $op . '.' . $image->info['extension']; + image_save($image, $file_path); $this->assertTrue($correct_dimensions_real, format_string('Image %file after %action action has proper dimensions.', array('%file' => $file, '%action' => $op))); $this->assertTrue($correct_dimensions_object, format_string('Image %file object after %action action is reporting the proper height and width values.', array('%file' => $file, '%action' => $op))); @@ -460,8 +465,37 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { $this->assertTrue($correct_colors, format_string('Image %file object after %action action has the correct color placement.', array('%file' => $file, '%action' => $op))); } } + + // Check that saved image reloads without raising PHP errors. + $image_reloaded = image_load($file_path); } + } + /** + * Tests loading an image whose transparent color index is out of range. + */ + function testTransparentColorOutOfRange() { + // This image was generated by taking an initial image with a palette size + // of 6 colors, and setting the transparent color index to 6 (one higher + // than the largest allowed index), as follows: + // @code + // $image = imagecreatefromgif('modules/simpletest/files/image-test.gif'); + // imagecolortransparent($image, 6); + // imagegif($image, 'modules/simpletest/files/image-test-transparent-out-of-range.gif'); + // @endcode + // This allows us to test that an image with an out-of-range color index + // can be loaded correctly. + $file = 'image-test-transparent-out-of-range.gif'; + $image = image_load(drupal_get_path('module', 'simpletest') . '/files/' . $file); + + if (!$image) { + $this->fail(format_string('Could not load image %file.', array('%file' => $file))); + } + else { + // All images should be converted to truecolor when loaded. + $image_truecolor = imageistruecolor($image->resource); + $this->assertTrue($image_truecolor, format_string('Image %file after load is a truecolor image.', array('%file' => $file))); + } } } diff --git a/modules/system/image.gd.inc b/modules/system/image.gd.inc index d9035e4c6..913b0de51 100644 --- a/modules/system/image.gd.inc +++ b/modules/system/image.gd.inc @@ -229,7 +229,24 @@ function image_gd_desaturate(stdClass $image) { function image_gd_load(stdClass $image) { $extension = str_replace('jpg', 'jpeg', $image->info['extension']); $function = 'imagecreatefrom' . $extension; - return (function_exists($function) && $image->resource = $function($image->source)); + if (function_exists($function) && $image->resource = $function($image->source)) { + if (imageistruecolor($image->resource)) { + return TRUE; + } + else { + // Convert indexed images to truecolor, copying the image to a new + // truecolor resource, so that filters work correctly and don't result + // in unnecessary dither. + $resource = image_gd_create_tmp($image, $image->info['width'], $image->info['height']); + if ($resource) { + imagecopy($resource, $image->resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource)); + imagedestroy($image->resource); + $image->resource = $resource; + } + } + return (bool) $image->resource; + } + return FALSE; } /** @@ -297,17 +314,31 @@ function image_gd_create_tmp(stdClass $image, $width, $height) { $res = imagecreatetruecolor($width, $height); if ($image->info['extension'] == 'gif') { - // Grab transparent color index from image resource. + // Find out if a transparent color is set, will return -1 if no + // transparent color has been defined in the image. $transparent = imagecolortransparent($image->resource); if ($transparent >= 0) { - // The original must have a transparent color, allocate to the new image. - $transparent_color = imagecolorsforindex($image->resource, $transparent); - $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); - - // Flood with our new transparent color. - imagefill($res, 0, 0, $transparent); - imagecolortransparent($res, $transparent); + // Find out the number of colors in the image palette. It will be 0 for + // truecolor images. + $palette_size = imagecolorstotal($image->resource); + if ($palette_size == 0 || $transparent < $palette_size) { + // Set the transparent color in the new resource, either if it is a + // truecolor image or if the transparent color is part of the palette. + // Since the index of the transparency color is a property of the + // image rather than of the palette, it is possible that an image + // could be created with this index set outside the palette size (see + // http://stackoverflow.com/a/3898007). + $transparent_color = imagecolorsforindex($image->resource, $transparent); + $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + + // Flood with our new transparent color. + imagefill($res, 0, 0, $transparent); + imagecolortransparent($res, $transparent); + } + else { + imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255)); + } } } elseif ($image->info['extension'] == 'png') { From 44b538695d561d34238c68b487604df1b6474de0 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 01:21:59 -0400 Subject: [PATCH 31/68] Issue #2428915 by pfrenssen: Remove mention of non-existing function in conf_path() documentation --- includes/bootstrap.inc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index ac6fb143f..6d60ed1f0 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -529,9 +529,8 @@ function timer_stop($name) { * Returns the appropriate configuration directory. * * Returns the configuration path based on the site's hostname, port, and - * pathname. Uses find_conf_path() to find the current configuration directory. - * See default.settings.php for examples on how the URL is converted to a - * directory. + * pathname. See default.settings.php for examples on how the URL is converted + * to a directory. * * @param bool $require_settings * Only configuration directories with an existing settings.php file From f56b706c3d3c9e774740f16ab8572d4fa538fa73 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 01:57:28 -0400 Subject: [PATCH 32/68] Issue #365241 by bcn, paulmarbach, xjm, chuckdeal97, skruf: Add select event to autocomplete feature --- misc/autocomplete.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/misc/autocomplete.js b/misc/autocomplete.js index 8f7ac6011..567908170 100644 --- a/misc/autocomplete.js +++ b/misc/autocomplete.js @@ -114,6 +114,7 @@ Drupal.jsAC.prototype.onkeyup = function (input, e) { */ Drupal.jsAC.prototype.select = function (node) { this.input.value = $(node).data('autocompleteValue'); + $(this.input).trigger('autocompleteSelect', [node]); }; /** @@ -167,7 +168,7 @@ Drupal.jsAC.prototype.unhighlight = function (node) { Drupal.jsAC.prototype.hidePopup = function (keycode) { // Select item if the right key or mousebutton was pressed. if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) { - this.input.value = $(this.selected).data('autocompleteValue'); + this.select(this.selected); } // Hide popup. var popup = this.popup; @@ -220,7 +221,7 @@ Drupal.jsAC.prototype.found = function (matches) { for (key in matches) { $('
  • ') .html($('
    ').html(matches[key])) - .mousedown(function () { ac.select(this); }) + .mousedown(function () { ac.hidePopup(this); }) .mouseover(function () { ac.highlight(this); }) .mouseout(function () { ac.unhighlight(this); }) .data('autocompleteValue', key) From 5a17a54c48e59a2798dd68a8d1e939149523dab9 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 02:09:35 -0400 Subject: [PATCH 33/68] Issue #2394517 by opdavies: Add a function to check if a user has a certain role --- CHANGELOG.txt | 2 ++ modules/user/user.module | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4060fcba6..3414ff833 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Added a user_has_role() function to check whether a user has a particular + role (API addition). - Fixed installation failures when an opcode cache is enabled. - Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused private files to be inaccessible. diff --git a/modules/user/user.module b/modules/user/user.module index bdfd36fa3..d74ed2f40 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -848,6 +848,26 @@ function user_is_blocked($name) { ->execute()->fetchObject(); } +/** + * Checks if a user has a role. + * + * @param int $rid + * A role ID. + * + * @param object|null $account + * (optional) A user account. Defaults to the current user. + * + * @return bool + * TRUE if the user has the role, or FALSE if not. + */ +function user_has_role($rid, $account = NULL) { + if (!$account) { + $account = $GLOBALS['user']; + } + + return isset($account->roles[$rid]); +} + /** * Implements hook_permission(). */ From b386d9211922a466485ba2e2ac83bbce2d3f7db5 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 02:12:13 -0400 Subject: [PATCH 34/68] Issue #2371759 by Valentine94, angel.angelio: The docblock for user_help() should read "Implements hook_help()." --- modules/user/user.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/user/user.module b/modules/user/user.module index d74ed2f40..9637a7165 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -32,7 +32,7 @@ define('USER_REGISTER_VISITORS', 1); define('USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL', 2); /** - * Implement hook_help(). + * Implements hook_help(). */ function user_help($path, $arg) { global $user; From cedcc04cf8d0432e8a57f3d965a604601252af5f Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 14:45:45 -0400 Subject: [PATCH 35/68] Issue #2342243 by martin107, serundeputy: Rename a variable in theme_system_modules_fieldset() to avoid colliding index variable names in a nested foreach loop --- modules/system/system.admin.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc index 22c202c3f..0f525c6cf 100644 --- a/modules/system/system.admin.inc +++ b/modules/system/system.admin.inc @@ -2646,8 +2646,8 @@ function theme_system_modules_fieldset($variables) { } $row[] = array('data' => $description, 'class' => array('description')); // Display links (such as help or permissions) in their own columns. - foreach (array('help', 'permissions', 'configure') as $key) { - $row[] = array('data' => drupal_render($module['links'][$key]), 'class' => array($key)); + foreach (array('help', 'permissions', 'configure') as $link_type) { + $row[] = array('data' => drupal_render($module['links'][$link_type]), 'class' => array($link_type)); } $rows[] = $row; } From 9164413df67a78362fcc04ed3c8ed934ea603e2d Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 15:03:39 -0400 Subject: [PATCH 36/68] Issue #1883058 by cilefen, aendrew: Default Drupal "drop" favicon needs a retina-quality version --- misc/favicon.ico | Bin 1150 -> 5430 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/misc/favicon.ico b/misc/favicon.ico index 3417ec244e8dc4be38ac8a6da044533ac6790773..61f840fa85d35e7d1a458d2ace49a4def7f867c1 100644 GIT binary patch literal 5430 zcmb_g2~?Cv5-wnbXo&ELaf!Yd9vBb;@kR6CspB#s2&l+L6b~2$FM=!QAV)xsmtSMn;PEAtK*NWYdU@REdT{PMF$)h@ecHMFF}?1yg*Y-KYE_ z_L)+*FWBm)STw7|+hv-s)yHPz&CJZ8t*uSPhT01R0tm4W^~yj7lr2ScXe`C~h=pR{TDNg_Hy7=!<7w9!r0zASVM-Ykj1_WDX}sgYr2 ziZv70u|Pn{YQWqm8>}jb!+j!)n=kB*Ygzk|_^&~LKde%$jl_5AHP(KQzo~2_JLSG2 ze}(K~-!p8gK8Pp6$Ex^9^MGqkw$m+GTy86 z1-R7E&@kfsaBx~8DfO&9*J6;w)|C_&( ze6tPH*XuJmot0C5AbdJrJtf6G8^&ScGPwQTJ7D=L`1<2uYWPAJyf2h}ziK_zgUM}e z$?;!X=H=!6=^89>vL7Gh2(|RSNfQF@BK8#XP|O^xI3U8)xnX!LZ6z7E!Qi;%xuGb} z2_bC_M)q@x8y4+!9qcbkaKfz6nf0@6KAphsD=wGImq{$)z zA=9K-M5sdao}Q!+)`flwTsA*nJ>snj%xU#>Jw`hIlvUc8A{@iq#fo{*z>6iRD%wvT zHloS#T;Z_3xE+l* z-A@PgXyp>wk0~O7m{e8p3*rPtEGCzi$4tbw+T$vIXk%bt09(o1 z;B=g^%6*)_5s;o=dT#dxnqWgs8Kc|X`G|Q#O1B7#xcH?z1QlFdZu@-37kK+b9Nw$k zi?@G^AAEyx^=SL3FJOxMi@Rd4zY(T+z0Jgw8!M>b;+G@{PN?%U8d(83%gIVHu(@;p z7C{LYzbIir?r5-vv;Fb!FXn*7&;IsHDHlI2+&N-6AET9Lv3v6Qo=6OJ6>;%>Ry*^H z$y<7~SnBMlUv@txE(i4Sa-G4&U*zn}Bk#d`>g?ZyZ0{PyI*1&h&ehdblZ)@{>*O~Q$6>M$!xq3@o+gYN4xc`JW=_?0f7En?+9ILf#S{bF{@eIyyjU`Es%eIr%p; zGZO*=0vHMk4B%y@gk*PjJz-*?KfRp~nYlQZI_s5Upm$p8CJR|}Q{&)w85dh!+t=4u zbIH6y-ksKj*wu4D{T2&W7x@YdlB%)*-Hk%m@AM$j;7&^c~Us zyxm)W@9FMjVi@RuhAo#5goRkU2V6pt}t*2KAr3R>!4$~HHdX}U=rO+w2T&C;e`Z2&d%z;+3cWiiw5~l zr2k2<=YZswwz(gH{}?S8&+4=MP2sctKK;mGY3qNkVs)k?^G;4#XDGLsX=!L_X$k5m zGBR>h^UvZc^KHS)iY{j_!_fIqu`4-goVvAp6EeE~>dIWJcfEh`HLs=LcS%V}uRXp$ zi}CE)vtVs)J%kq6Sz9{US)0Y;~eoV*! literal 1150 zcmaiyZAg<*6vzLoASkdgs3;+LtJ`0I|$iie={W%mHjov3%ade~IZzB39o@fzFtUR_5In&HCv_`3Y~!lu_0C zgV-Q}gi=hu??y5vOh-V1IV+A)e?tMa7?Y^)v6gIw33GwD;7UMyL1qRD(Bip*JS{;5%=#OC@e{OmXf8tp70{(YPLK)rRj z)ND9FT{W3x)yC13dzhv@9|N`8Z9Dn))=W6M@<4*keHr0+$a`nM)BD~B^vCxrlgA^^F5cB(NLqBrbc^d zth0z->SM%nIoaJT8O*ZRQlIStm7i4n;C$05_&FcRKRXY)mp|XQ-ZH*}^K~lpgliUx?;i)19zrSSE7GuX*m*VJdUcojN}|R4MygShfvMsoC^#?p z=jOz6-`rRFJ@bj))u*r;GRZnE?ELJ#oDFAr++p0WnwcC7`hBxB-mU+S7y88ZKF#$~ zzA_E#1{_-VtY?V&4V%OoZD&C2eGXQoW2+P+m21?+-5F`mCr38C)k^G=(kzs-U)tzl f{f>{Mn^^%&ECpUH28v{nP0$Ehf${GKg4ck*14y&W From d67fd28f14c9a5e4e596a75749b4e6fb4c80da3d Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 16:38:49 -0400 Subject: [PATCH 37/68] Issue #1441950 by hefox: Node types removed from hook_node_info with base = 'node_content' cannot be deleted --- modules/node/node.module | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/node/node.module b/modules/node/node.module index 0a7560bb9..ebac07902 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -739,11 +739,9 @@ function _node_types_build($rebuild = FALSE) { $type_db = $type_object->type; // Original disabled value. $disabled = $type_object->disabled; - // Check for node types from disabled modules and mark their types for removal. - // Types defined by the node module in the database (rather than by a separate - // module using hook_node_info) have a base value of 'node_content'. The isset() - // check prevents errors on old (pre-Drupal 7) databases. - if (isset($type_object->base) && $type_object->base != 'node_content' && empty($_node_types->types[$type_db])) { + // Check for node types either from disabled modules or otherwise not defined + // and mark as disabled. + if (empty($type_object->custom) && empty($_node_types->types[$type_db])) { $type_object->disabled = TRUE; } if (isset($_node_types->types[$type_db])) { From c0f687ed41e2bd171b422485ef2362eddb2ce918 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 16:49:06 -0400 Subject: [PATCH 38/68] Issue #2001308 by stefan.r, David_Rothstein, marthinal, helmo: Node preview removes file values from node edit form for non-displayed items --- CHANGELOG.txt | 3 +++ modules/file/tests/file.test | 12 ++++++++++++ modules/node/node.pages.inc | 35 +++++++++++++++++++---------------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3414ff833..f0ddec41a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,9 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed a bug which caused previewing a node to remove elements from the node + being edited. With this fix, calling node_preview() will no longer modify the + passed-in node object (minor API change). - Added a user_has_role() function to check whether a user has a particular role (API addition). - Fixed installation failures when an opcode cache is enabled. diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test index 0f6a578d9..e2c5737f4 100644 --- a/modules/file/tests/file.test +++ b/modules/file/tests/file.test @@ -877,6 +877,7 @@ class FileFieldDisplayTestCase extends FileFieldTestCase { $field_settings = array( 'display_field' => '1', 'display_default' => '1', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); $instance_settings = array( 'description_field' => '1', @@ -917,6 +918,17 @@ class FileFieldDisplayTestCase extends FileFieldTestCase { $this->assertNoRaw($default_output, 'Field is hidden when "display" option is unchecked.'); + // Test that fields appear as expected during the preview. + // Add a second file. + $name = 'files[' . $field_name . '_' . LANGUAGE_NONE . '_1]'; + $edit[$name] = drupal_realpath($test_file->uri); + + // Uncheck the display checkboxes and go to the preview. + $edit[$field_name . '[' . LANGUAGE_NONE . '][0][display]'] = FALSE; + $edit[$field_name . '[' . LANGUAGE_NONE . '][1][display]'] = FALSE; + $this->drupalPost('node/' . $nid . '/edit', $edit, t('Preview')); + $this->assertRaw($field_name . '[' . LANGUAGE_NONE . '][0][display]', 'First file appears as expected.'); + $this->assertRaw($field_name . '[' . LANGUAGE_NONE . '][1][display]', 'Second file appears as expected.'); } } diff --git a/modules/node/node.pages.inc b/modules/node/node.pages.inc index 626746362..cc3908e3c 100644 --- a/modules/node/node.pages.inc +++ b/modules/node/node.pages.inc @@ -371,35 +371,38 @@ function node_form_build_preview($form, &$form_state) { * @see node_form_build_preview() */ function node_preview($node) { - if (node_access('create', $node) || node_access('update', $node)) { - _field_invoke_multiple('load', 'node', array($node->nid => $node)); + // Clone the node before previewing it to prevent the node itself from being + // modified. + $cloned_node = clone $node; + if (node_access('create', $cloned_node) || node_access('update', $cloned_node)) { + _field_invoke_multiple('load', 'node', array($cloned_node->nid => $cloned_node)); // Load the user's name when needed. - if (isset($node->name)) { + if (isset($cloned_node->name)) { // The use of isset() is mandatory in the context of user IDs, because // user ID 0 denotes the anonymous user. - if ($user = user_load_by_name($node->name)) { - $node->uid = $user->uid; - $node->picture = $user->picture; + if ($user = user_load_by_name($cloned_node->name)) { + $cloned_node->uid = $user->uid; + $cloned_node->picture = $user->picture; } else { - $node->uid = 0; // anonymous user + $cloned_node->uid = 0; // anonymous user } } - elseif ($node->uid) { - $user = user_load($node->uid); - $node->name = $user->name; - $node->picture = $user->picture; + elseif ($cloned_node->uid) { + $user = user_load($cloned_node->uid); + $cloned_node->name = $user->name; + $cloned_node->picture = $user->picture; } - $node->changed = REQUEST_TIME; - $nodes = array($node->nid => $node); + $cloned_node->changed = REQUEST_TIME; + $nodes = array($cloned_node->nid => $cloned_node); field_attach_prepare_view('node', $nodes, 'full'); // Display a preview of the node. if (!form_get_errors()) { - $node->in_preview = TRUE; - $output = theme('node_preview', array('node' => $node)); - unset($node->in_preview); + $cloned_node->in_preview = TRUE; + $output = theme('node_preview', array('node' => $cloned_node)); + unset($cloned_node->in_preview); } drupal_set_title(t('Preview'), PASS_THROUGH); From 86974af08b793a424f4303ba10a6e68a5be2dab2 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 17:17:14 -0400 Subject: [PATCH 39/68] Issue #889338 by dawehner, das-peter, yched, heddn, frankcarey: Add support for Xdebug in DrupalWebTestCase --- CHANGELOG.txt | 1 + modules/simpletest/drupal_web_test_case.php | 26 ++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f0ddec41a..5fa03d09f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,7 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Added basic support for Xdebug when running automated tests. - Fixed a bug which caused previewing a node to remove elements from the node being edited. With this fix, calling node_preview() will no longer modify the passed-in node object (minor API change). diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 8ecddb50b..271efff3a 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1769,14 +1769,24 @@ protected function curlInitialize() { protected function curlExec($curl_options, $redirect = FALSE) { $this->curlInitialize(); - // cURL incorrectly handles URLs with a fragment by including the - // fragment in the request to the server, causing some web servers - // to reject the request citing "400 - Bad Request". To prevent - // this, we strip the fragment from the request. - // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. - if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { - $original_url = $curl_options[CURLOPT_URL]; - $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + if (!empty($curl_options[CURLOPT_URL])) { + // Forward XDebug activation if present. + if (isset($_COOKIE['XDEBUG_SESSION'])) { + $options = drupal_parse_url($curl_options[CURLOPT_URL]); + $options += array('query' => array()); + $options['query'] += array('XDEBUG_SESSION_START' => $_COOKIE['XDEBUG_SESSION']); + $curl_options[CURLOPT_URL] = url($options['path'], $options); + } + + // cURL incorrectly handles URLs with a fragment by including the + // fragment in the request to the server, causing some web servers + // to reject the request citing "400 - Bad Request". To prevent + // this, we strip the fragment from the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (strpos($curl_options[CURLOPT_URL], '#')) { + $original_url = $curl_options[CURLOPT_URL]; + $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + } } $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; From 0a9d5311c576a077af33678b148dff811325085d Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 17:39:12 -0400 Subject: [PATCH 40/68] Issue #413270 by Jody Lynn, Daniel Korte: Block settings for theme menu title getting double escaped --- CHANGELOG.txt | 2 ++ modules/block/block.module | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5fa03d09f..840a662ad 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed double-escaping of theme names in the Block module administrative + interface (minor string change). - Added basic support for Xdebug when running automated tests. - Fixed a bug which caused previewing a node to remove elements from the node being edited. With this fix, calling node_preview() will no longer modify the diff --git a/modules/block/block.module b/modules/block/block.module index b6a733267..0cda4e239 100644 --- a/modules/block/block.module +++ b/modules/block/block.module @@ -66,7 +66,7 @@ function block_help($path, $arg) { $demo_theme = !empty($arg[4]) ? $arg[4] : variable_get('theme_default', 'bartik'); $themes = list_themes(); $output = '

    ' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the Save blocks button at the bottom of the page. Click the configure link next to each block to configure its specific title and visibility settings.') . '

    '; - $output .= '

    ' . l(t('Demonstrate block regions (@theme)', array('@theme' => $themes[$demo_theme]->info['name'])), 'admin/structure/block/demo/' . $demo_theme) . '

    '; + $output .= '

    ' . l(t('Demonstrate block regions (!theme)', array('!theme' => $themes[$demo_theme]->info['name'])), 'admin/structure/block/demo/' . $demo_theme) . '

    '; return $output; } } @@ -143,7 +143,7 @@ function block_menu() { ); foreach (list_themes() as $key => $theme) { $items['admin/structure/block/list/' . $key] = array( - 'title' => check_plain($theme->info['name']), + 'title' => $theme->info['name'], 'page arguments' => array($key), 'type' => $key == $default_theme ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK, 'weight' => $key == $default_theme ? -10 : 0, @@ -162,7 +162,7 @@ function block_menu() { ); } $items['admin/structure/block/demo/' . $key] = array( - 'title' => check_plain($theme->info['name']), + 'title' => $theme->info['name'], 'page callback' => 'block_admin_demo', 'page arguments' => array($key), 'type' => MENU_CALLBACK, From bcb8761d36eb0297d8675f6af17957f9accc5b7b Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 17:45:25 -0400 Subject: [PATCH 41/68] Issue #1995058 by TravisCarden, acbramley, vbouchet: Tableselect "select all" checkbox should be checked on page load if all checkboxes are ticked --- misc/tableselect.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/misc/tableselect.js b/misc/tableselect.js index 8f0cd9750..09bac1390 100644 --- a/misc/tableselect.js +++ b/misc/tableselect.js @@ -57,6 +57,10 @@ Drupal.tableSelect = function () { // Keep track of the last checked checkbox. lastChecked = e.target; }); + + // If all checkboxes are checked on page load, make sure the select-all one + // is checked too, otherwise keep unchecked. + updateSelectAll((checkboxes.length == $(checkboxes).filter(':checked').length)); }; Drupal.tableSelectRange = function (from, to, state) { From eaa79a12af2f739213e05c95edd4a0686bdc391b Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:20:30 -0400 Subject: [PATCH 42/68] Issue #1934498 by attiks, David_Rothstein, KhaledBlah, tstoeckler, julien_acti, helmo, effulgentsia, Jelle_S, jcisio: Allow the image style 'itok' token to be suppressed in image derivative URLs --- modules/image/image.module | 18 +++++++++++++++--- modules/image/image.test | 9 +++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/modules/image/image.module b/modules/image/image.module index a2a0f416a..fac8de955 100644 --- a/modules/image/image.module +++ b/modules/image/image.module @@ -1027,7 +1027,15 @@ function image_style_url($style_name, $path) { // The token query is added even if the 'image_allow_insecure_derivatives' // variable is TRUE, so that the emitted links remain valid if it is changed // back to the default FALSE. - $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, $original_uri)); + // However, sites which need to prevent the token query from being emitted at + // all can additionally set the 'image_suppress_itok_output' variable to TRUE + // to achieve that (if both are set, the security token will neither be + // emitted in the image derivative URL nor checked for in + // image_style_deliver()). + $token_query = array(); + if (!variable_get('image_suppress_itok_output', FALSE)) { + $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, $original_uri)); + } // If not using clean URLs, the image derivative callback is only available // with the query string. If the file does not exist, use url() to ensure @@ -1039,8 +1047,12 @@ function image_style_url($style_name, $path) { } $file_url = file_create_url($uri); - // Append the query string with the token. - return $file_url . (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); + // Append the query string with the token, if necessary. + if ($token_query) { + $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); + } + + return $file_url; } /** diff --git a/modules/image/image.test b/modules/image/image.test index 2387314c5..359197948 100644 --- a/modules/image/image.test +++ b/modules/image/image.test @@ -330,6 +330,15 @@ class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase { $this->drupalGet($nested_url); $this->assertResponse(200, 'Image was accessible when a correct token was provided in the URL.'); + // Suppress the security token in the URL, then get the URL of a file. Check + // that the security token is not present in the URL but that the image is + // still accessible. + variable_set('image_suppress_itok_output', TRUE); + $generate_url = image_style_url($this->style_name, $original_uri); + $this->assertIdentical(strpos($generate_url, IMAGE_DERIVATIVE_TOKEN . '='), FALSE, 'The security token does not appear in the image style URL.'); + $this->drupalGet($generate_url); + $this->assertResponse(200, 'Image was accessible at the URL with a missing token.'); + // Check that requesting a nonexistent image does not create any new // directories in the file system. $directory = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/' . $this->randomName(); From c17ccc02ae279900653affd9691e372554df1991 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:26:51 -0400 Subject: [PATCH 43/68] Added issue #1934498 to CHANGELOG.txt. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 840a662ad..27e8b7475 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,9 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Added an 'image_suppress_itok_output' variable to allow sites already using + the existing 'image_allow_insecure_derivatives' variable to also prevent + security tokens from appearing in image derivative URLs. - Fixed double-escaping of theme names in the Block module administrative interface (minor string change). - Added basic support for Xdebug when running automated tests. From a610b977aab78ebdacf35033c2d8695d83733795 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:35:29 -0400 Subject: [PATCH 44/68] Issue #713462 by jwilson3, Paul B, casey, sivaji@knackforge.com, dcam: Content added via drupal_add_region_content() is not added to pages --- CHANGELOG.txt | 2 ++ includes/theme.inc | 3 +++ modules/simpletest/tests/theme.test | 9 +++++++++ modules/simpletest/tests/theme_test.module | 13 +++++++++++++ 4 files changed, 27 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 27e8b7475..a48b6e99d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed the drupal_add_region_content() function so that it actually adds + content to the page. - Added an 'image_suppress_itok_output' variable to allow sites already using the existing 'image_allow_insecure_derivatives' variable to also prevent security tokens from appearing in image derivative URLs. diff --git a/includes/theme.inc b/includes/theme.inc index ed34b8289..1833beeb8 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -2622,6 +2622,9 @@ function template_preprocess_page(&$variables) { if (!isset($variables['page'][$region_key])) { $variables['page'][$region_key] = array(); } + if ($region_content = drupal_get_region_content($region_key)) { + $variables['page'][$region_key][]['#markup'] = $region_content; + } } // Set up layout variable. diff --git a/modules/simpletest/tests/theme.test b/modules/simpletest/tests/theme.test index f1a743e00..f5ddfa9b0 100644 --- a/modules/simpletest/tests/theme.test +++ b/modules/simpletest/tests/theme.test @@ -155,6 +155,15 @@ class ThemeTestCase extends DrupalWebTestCase { $this->assertNotEqual(theme_get_setting('subtheme_override', 'test_basetheme'), theme_get_setting('subtheme_override', 'test_subtheme'), 'Base theme\'s default settings values can be overridden by subtheme.'); $this->assertIdentical(theme_get_setting('basetheme_only', 'test_subtheme'), 'base theme value', 'Base theme\'s default settings values are inherited by subtheme.'); } + + /** + * Test the drupal_add_region_content() function. + */ + function testDrupalAddRegionContent() { + $this->drupalGet('theme-test/drupal-add-region-content'); + $this->assertText('Hello'); + $this->assertText('World'); + } } /** diff --git a/modules/simpletest/tests/theme_test.module b/modules/simpletest/tests/theme_test.module index 61a12bb70..948d8175a 100644 --- a/modules/simpletest/tests/theme_test.module +++ b/modules/simpletest/tests/theme_test.module @@ -53,6 +53,11 @@ function theme_test_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['theme-test/drupal-add-region-content'] = array( + 'page callback' => '_theme_test_drupal_add_region_content', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -126,6 +131,14 @@ function _theme_test_suggestion() { return theme(array('theme_test__suggestion', 'theme_test'), array()); } +/** + * Page callback, calls drupal_add_region_content. + */ +function _theme_test_drupal_add_region_content() { + drupal_add_region_content('content', 'World'); + return 'Hello'; +} + /** * Theme function for testing theme('theme_test_foo'). */ From 437ceb6bb1a9241805b2e2acf0b6600fcd444c35 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:39:37 -0400 Subject: [PATCH 45/68] Issue #2453389 by rpayanm, joshi.rohit100: hook_view() does not document $langcode --- modules/node/node.api.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/node/node.api.php b/modules/node/node.api.php index a83dee943..9a4d09591 100644 --- a/modules/node/node.api.php +++ b/modules/node/node.api.php @@ -1299,6 +1299,10 @@ function hook_validate($node, $form, &$form_state) { * The node to be displayed, as returned by node_load(). * @param $view_mode * View mode, e.g. 'full', 'teaser', ... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * * @return * The passed $node parameter should be modified as necessary and returned so * it can be properly presented. Nodes are prepared for display by assembling @@ -1312,7 +1316,7 @@ function hook_validate($node, $form, &$form_state) { * * @ingroup node_api_hooks */ -function hook_view($node, $view_mode) { +function hook_view($node, $view_mode, $langcode = NULL) { if ($view_mode == 'full' && node_is_page($node)) { $breadcrumb = array(); $breadcrumb[] = l(t('Home'), NULL); From e765be0bad0015c69e45367ea518159932f3afcb Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:42:39 -0400 Subject: [PATCH 46/68] Issue #2453311 by TravisCarden: Fix a couple more "the the"s in the codebase --- includes/bootstrap.inc | 2 +- includes/database/query.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 6d60ed1f0..cb7a5b7d1 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -2637,7 +2637,7 @@ function drupal_installation_attempted() { * * This would include implementations of hook_install(), which could run * during the Drupal installation phase, and might also be run during - * non-installation time, such as while installing the module from the the + * non-installation time, such as while installing the module from the * module administration page. * * Example usage: diff --git a/includes/database/query.inc b/includes/database/query.inc index 8af91c2d7..c9c5a8328 100644 --- a/includes/database/query.inc +++ b/includes/database/query.inc @@ -1694,7 +1694,7 @@ class DatabaseCondition implements QueryConditionInterface, Countable { * Implements Countable::count(). * * Returns the size of this conditional. The size of the conditional is the - * size of its conditional array minus one, because one element is the the + * size of its conditional array minus one, because one element is the * conjunction. */ public function count() { From c377b88b8e9809ab117dd79901f01515ff085385 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:47:52 -0400 Subject: [PATCH 47/68] Issue #2453321 by TravisCarden: Typo in @see reference: drupal_decode_exception() should be _drupal_decode_exception() --- includes/bootstrap.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index cb7a5b7d1..0e8446280 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1643,14 +1643,14 @@ function request_uri() { * information about the passed-in exception is used. * @param $variables * Array of variables to replace in the message on display. Defaults to the - * return value of drupal_decode_exception(). + * return value of _drupal_decode_exception(). * @param $severity * The severity of the message, as per RFC 3164. * @param $link * A link to associate with the message. * * @see watchdog() - * @see drupal_decode_exception() + * @see _drupal_decode_exception() */ function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) { From a577cc98e33aa3f6630446625930123694cbbad1 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:49:19 -0400 Subject: [PATCH 48/68] Issue #2446657 by rpayanm, er.pushpinderrana: Remove dead link from robots.txt --- robots.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/robots.txt b/robots.txt index 6f20eaf37..ff9e28687 100644 --- a/robots.txt +++ b/robots.txt @@ -12,9 +12,6 @@ # # For more information about the robots.txt standard, see: # http://www.robotstxt.org/robotstxt.html -# -# For syntax checking, see: -# http://www.frobee.com/robots-txt-check User-agent: * Crawl-delay: 10 From 517f137d4aa1e3681f46a4767ef0e5bcefa7ea40 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 18:53:00 -0400 Subject: [PATCH 49/68] Issue #1018618 by manfer, joshi.rohit100: Wrong assertions in block.test --- modules/block/block.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/block/block.test b/modules/block/block.test index 99c81dc6e..f20e3c878 100644 --- a/modules/block/block.test +++ b/modules/block/block.test @@ -75,7 +75,7 @@ class BlockTestCase extends DrupalWebTestCase { $bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField(); // Check to see if the custom block was created by checking that it's in the database. - $this->assertNotNull($bid, 'Custom block found in database'); + $this->assertTrue($bid, 'Custom block found in database'); // Check that block_block_view() returns the correct title and content. $data = block_block_view($bid); @@ -305,7 +305,7 @@ class BlockTestCase extends DrupalWebTestCase { ))->fetchField(); // Check to see if the block was created by checking that it's in the database. - $this->assertNotNull($bid, 'Block found in database'); + $this->assertTrue($bid, 'Block found in database'); // Check whether the block can be moved to all available regions. foreach ($this->regions as $region) { From e8a38c6175619fea2e3b1bbcc39c6a4cf35af5d6 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 19:03:15 -0400 Subject: [PATCH 50/68] Issue #2283717 by joshi.rohit100, amitgoyal, g3r4, er.pushpinderrana: Remove user_access function calls on hook_permission functions so the Permissions page consistently links to other admin pages for all users --- CHANGELOG.txt | 3 +++ modules/filter/filter.module | 4 +--- modules/node/node.module | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a48b6e99d..a8be58cdc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,9 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Changed permission descriptions throughout Drupal core to consistently link + to relevant administrative pages, regardless of whether the user viewing the + Permissions page can view the page being linked to (minor UI change). - Fixed the drupal_add_region_content() function so that it actually adds content to the page. - Added an 'image_suppress_itok_output' variable to allow sites already using diff --git a/modules/filter/filter.module b/modules/filter/filter.module index 5c2b8c97f..74621f108 100644 --- a/modules/filter/filter.module +++ b/modules/filter/filter.module @@ -348,9 +348,7 @@ function filter_permission() { foreach (filter_formats() as $format) { $permission = filter_permission_name($format); if (!empty($permission)) { - // Only link to the text format configuration page if the user who is - // viewing this will have access to that page. - $format_name_replacement = user_access('administer filters') ? l($format->name, 'admin/config/content/formats/' . $format->format) : drupal_placeholder($format->name); + $format_name_replacement = l($format->name, 'admin/config/content/formats/' . $format->format); $perms[$permission] = array( 'title' => t("Use the !text_format text format", array('!text_format' => $format_name_replacement,)), 'description' => drupal_placeholder(t('Warning: This permission may have security implications depending on how the text format is configured.')), diff --git a/modules/node/node.module b/modules/node/node.module index ebac07902..a9c1d23e8 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -1578,9 +1578,7 @@ function node_permission() { ), 'access content overview' => array( 'title' => t('Access the content overview page'), - 'description' => user_access('access content overview') - ? t('Get an overview of all content.', array('@url' => url('admin/content'))) - : t('Get an overview of all content.'), + 'description' => t('Get an overview of all content.', array('@url' => url('admin/content'))), ), 'access content' => array( 'title' => t('View published content'), From 860c0600143d56c7c083804d3dd0726010a6945c Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 19:39:05 -0400 Subject: [PATCH 51/68] Issue #2425259 by catch, sidharrell, nlisgo, Josh Waihi, Fabianx, Berdir, martin107: Router rebuild lock_wait() condition can result in rebuild later in the request (race condition) --- includes/menu.inc | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/includes/menu.inc b/includes/menu.inc index 2bfa39f66..8e26b6dee 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -456,7 +456,9 @@ function menu_get_item($path = NULL, $router_item = NULL) { // Rebuild if we know it's needed, or if the menu masks are missing which // occurs rarely, likely due to a race condition of multiple rebuilds. if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) { - menu_rebuild(); + if (_menu_check_rebuild()) { + menu_rebuild(); + } } $original_map = arg(NULL, $path); @@ -2693,6 +2695,21 @@ function menu_reset_static_cache() { drupal_static_reset('menu_link_get_preferred'); } +/** + * Checks whether a menu_rebuild() is necessary. + */ +function _menu_check_rebuild() { + // To absolutely ensure that the menu rebuild is required, re-load the + // variables in case they were set by another process. + $variables = variable_initialize(); + if (empty($variables['menu_rebuild_needed']) && !empty($variables['menu_masks'])) { + unset($GLOBALS['conf']['menu_rebuild_needed']); + $GLOBALS['conf']['menu_masks'] = $variables['menu_masks']; + return FALSE; + } + return TRUE; +} + /** * Populates the database tables used by various menu functions. * @@ -2713,6 +2730,14 @@ function menu_rebuild() { // We choose to block here since otherwise the router item may not // be available in menu_execute_active_handler() resulting in a 404. lock_wait('menu_rebuild'); + + if (_menu_check_rebuild()) { + // If we get here and menu_masks was not set, then it is possible a menu + // is being reloaded, or that the process rebuilding the menu was unable + // to complete successfully. A missing menu_masks variable could result + // in a 404, so re-run the function. + return menu_rebuild(); + } return FALSE; } @@ -2737,6 +2762,12 @@ function menu_rebuild() { $transaction->rollback(); watchdog_exception('menu', $e); } + // Explicitly commit the transaction now; this ensures that the database + // operations during the menu rebuild are committed before the lock is made + // available again, since locks may not always reside in the same database + // connection. The lock is acquired outside of the transaction so should also + // be released outside of it. + unset($transaction); lock_release('menu_rebuild'); return TRUE; From b0705c036c84649a0cd2930391e1f0ed8e573009 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 21:09:19 -0400 Subject: [PATCH 52/68] Issue #2462223 by helmo: Typo in comment in node_access_test.module --- modules/node/tests/node_access_test.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/node/tests/node_access_test.module b/modules/node/tests/node_access_test.module index ec35c41f1..7932c552d 100644 --- a/modules/node/tests/node_access_test.module +++ b/modules/node/tests/node_access_test.module @@ -211,7 +211,7 @@ function node_access_test_node_insert($node) { } /** - * Implements hook_nodeapi_update(). + * Implements hook_node_update(). */ function node_access_test_node_update($node) { _node_access_test_node_write($node); From d6ae9e6dc393366f79cb54420fb808f20ed3df9c Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 21:12:20 -0400 Subject: [PATCH 53/68] Issue #2386037 by gobinathm: Incorrect foreign key tables in users.install --- CHANGELOG.txt | 2 ++ modules/user/user.install | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a8be58cdc..62993ae5a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Fixed incorrect foreign keys in the User module's role_permission and + users_roles database tables. - Changed permission descriptions throughout Drupal core to consistently link to relevant administrative pages, regardless of whether the user viewing the Permissions page can view the page being linked to (minor UI change). diff --git a/modules/user/user.install b/modules/user/user.install index 728e00468..b573e72d3 100644 --- a/modules/user/user.install +++ b/modules/user/user.install @@ -81,7 +81,7 @@ function user_schema() { ), 'foreign keys' => array( 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -278,7 +278,7 @@ function user_schema() { 'columns' => array('uid' => 'uid'), ), 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), From 277ae50e8be4adb6a74aada6f341b2fdc38d6b20 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 21:23:12 -0400 Subject: [PATCH 54/68] Issue #1483736 by stefan.r, bfcam, jrigby: field_attach_update deletes file fields (content & file) in entity regardless of if they are included in the entity object --- modules/file/file.field.inc | 6 ++++++ modules/file/tests/file.test | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/modules/file/file.field.inc b/modules/file/file.field.inc index b26d7e457..794f16e67 100644 --- a/modules/file/file.field.inc +++ b/modules/file/file.field.inc @@ -252,6 +252,12 @@ function file_field_insert($entity_type, $entity, $field, $instance, $langcode, * Checks for files that have been removed from the object. */ function file_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { + // Check whether the field is defined on the object. + if (!isset($entity->{$field['field_name']})) { + // We cannot check for removed files if the field is not defined. + return; + } + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); // On new revisions, all files are considered to be a new usage and no diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test index e2c5737f4..33d7afd1b 100644 --- a/modules/file/tests/file.test +++ b/modules/file/tests/file.test @@ -474,6 +474,15 @@ class FileFieldWidgetTestCase extends FileFieldTestCase { $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; $this->assertFileExists($node_file, 'New file saved to disk on node creation.'); + // Test that running field_attach_update() leaves the file intact. + $field = new stdClass(); + $field->type = $type_name; + $field->nid = $nid; + field_attach_update('node', $field); + $node = node_load($nid); + $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $this->assertFileExists($node_file, 'New file still saved to disk on field update.'); + // Ensure the file can be downloaded. $this->drupalGet(file_create_url($node_file->uri)); $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); From 032954a14e12fec4a6f48ede2c784d7ee04c589b Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 21:26:50 -0400 Subject: [PATCH 55/68] Issue #1051872 by boombatower, jdillick, marvil07: Add documentation concerning modified property flag on node_type_save() --- modules/node/node.module | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/node/node.module b/modules/node/node.module index a9c1d23e8..fd848e246 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -506,7 +506,8 @@ function node_type_load($name) { * - custom: TRUE or FALSE indicating whether this type is defined by a module * (FALSE) or by a user (TRUE) via Add Content Type. * - modified: TRUE or FALSE indicating whether this type has been modified by - * an administrator. Currently not used in any way. + * an administrator. When modifying an existing node type, set to TRUE, or + * the change will be ignored on node_types_rebuild(). * - locked: TRUE or FALSE indicating whether the administrator can change the * machine name of this type. * - disabled: TRUE or FALSE indicating whether this type has been disabled. From 1dbe1088ba1b94ce681f45c6526c73b671a190d8 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 21:45:28 -0400 Subject: [PATCH 56/68] Issue #1828530 by cam8001: Remove unused $default_method variable in user_cancel_confirm_form() --- modules/user/user.pages.inc | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc index 6b7d38e22..f21bd134d 100644 --- a/modules/user/user.pages.inc +++ b/modules/user/user.pages.inc @@ -359,7 +359,6 @@ function user_cancel_confirm_form($form, &$form_state, $account) { $form['_account'] = array('#type' => 'value', '#value' => $account); // Display account cancellation method selection, if allowed. - $default_method = variable_get('user_cancel_method', 'user_cancel_block'); $admin_access = user_access('administer users'); $can_select_method = $admin_access || user_access('select account cancellation method'); $form['user_cancel_method'] = array( From 32efb5c811d38a8b25d30c14ef89044d04440e62 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 21:50:31 -0400 Subject: [PATCH 57/68] Issue #197641 by herom, good_man, elcuco: Drag and drop does not work correctly on RTL languages --- misc/tabledrag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/tabledrag.js b/misc/tabledrag.js index fed674ca9..3cc270194 100644 --- a/misc/tabledrag.js +++ b/misc/tabledrag.js @@ -500,7 +500,7 @@ Drupal.tableDrag.prototype.dragRow = function (event, self) { if (self.indentEnabled) { var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x; // Set the number of indentations the mouse has been moved left or right. - var indentDiff = Math.round(xDiff / self.indentAmount * self.rtl); + var indentDiff = Math.round(xDiff / self.indentAmount); // Indent the row with our estimated diff, which may be further // restricted according to the rows around this row. var indentChange = self.rowObject.indent(indentDiff); From 3329a70175eb772ee89568ec3423572e48012518 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 21:57:40 -0400 Subject: [PATCH 58/68] Issue #1201452 by mgifford, Heine, ircmaxell: Improve security on newer versions of PHP by setting an additional charset DSN parameter when connecting to MySQL via PDO --- includes/database/mysql/database.inc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/database/mysql/database.inc b/includes/database/mysql/database.inc index 4907a39dd..0b84f2719 100644 --- a/includes/database/mysql/database.inc +++ b/includes/database/mysql/database.inc @@ -36,6 +36,10 @@ class DatabaseConnection_mysql extends DatabaseConnection { // Default to TCP connection on port 3306. $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']); } + // Character set is added to dsn to ensure PDO uses the proper character + // set when escaping. This has security implications. See + // https://www.drupal.org/node/1201452 for further discussion. + $dsn .= ';charset=utf8'; $dsn .= ';dbname=' . $connection_options['database']; // Allow PDO options to be overridden. $connection_options += array( From b1001811d95d5b4dc7ca4916fc74f773106ac9f0 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 22:03:05 -0400 Subject: [PATCH 59/68] Issue #2432619 by vbouchet, jhodgdon, joachim: block_load() should state it's not suitable for general consumption --- modules/block/block.module | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/block/block.module b/modules/block/block.module index 0cda4e239..48c80d766 100644 --- a/modules/block/block.module +++ b/modules/block/block.module @@ -692,6 +692,9 @@ function block_list($region) { /** * Loads a block object from the database. * + * This function returns the first block matching the module and delta + * parameters, so it should not be used for theme-specific functionality. + * * @param $module * Name of the module that implements the block to load. * @param $delta From 63065623fba4087ba574b64efce054ae5a5b0683 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 22:12:31 -0400 Subject: [PATCH 60/68] Issue #1946240 by hampercm, eiriksm, David_Rothstein, rpayanm, rszrama, Yaron Tal, dgtlife, madhusudanmca, er.pushpinderrana, Cottser: Remove the hardcoded 0 index in theme_status_messages() --- includes/theme.inc | 2 +- modules/simpletest/tests/system_test.module | 21 +++++++++++++++ modules/system/system.test | 29 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/includes/theme.inc b/includes/theme.inc index 1833beeb8..8d5348dfc 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -1691,7 +1691,7 @@ function theme_status_messages($variables) { $output .= " \n"; } else { - $output .= $messages[0]; + $output .= reset($messages); } $output .= "\n"; } diff --git a/modules/simpletest/tests/system_test.module b/modules/simpletest/tests/system_test.module index c0eed034f..8c4432996 100644 --- a/modules/simpletest/tests/system_test.module +++ b/modules/simpletest/tests/system_test.module @@ -78,6 +78,13 @@ function system_test_menu() { 'type' => MENU_CALLBACK, ); + $items['system-test/drupal-set-message'] = array( + 'title' => 'Set messages with drupal_set_message()', + 'page callback' => 'system_test_drupal_set_message', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['system-test/main-content-handling'] = array( 'title' => 'Test main content handling', 'page callback' => 'system_test_main_content_fallback', @@ -435,6 +442,20 @@ function system_test_authorize_init_page($page_title) { drupal_goto($authorize_url); } +/** + * Sets two messages and removes the first one before the messages are displayed. + */ +function system_test_drupal_set_message() { + // Set two messages. + drupal_set_message('First message (removed).'); + drupal_set_message('Second message (not removed).'); + + // Remove the first. + unset($_SESSION['messages']['status'][0]); + + return ''; +} + /** * Page callback to print out $_GET['destination'] for testing. */ diff --git a/modules/system/system.test b/modules/system/system.test index dcc86e539..3e26bae8c 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -2826,6 +2826,35 @@ class SystemValidTokenTest extends DrupalUnitTestCase { } } +/** + * Tests drupal_set_message() and related functions. + */ +class DrupalSetMessageTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Messages', + 'description' => 'Tests that messages can be displayed using drupal_set_message().', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * Tests setting messages and removing one before it is displayed. + */ + function testSetRemoveMessages() { + // The page at system-test/drupal-set-message sets two messages and then + // removes the first before it is displayed. + $this->drupalGet('system-test/drupal-set-message'); + $this->assertNoText('First message (removed).'); + $this->assertText('Second message (not removed).'); + } +} + /** * Tests confirm form destinations. */ From f41ecaf25d7a38923e026ef45a74dffaf58f9479 Mon Sep 17 00:00:00 2001 From: David Rothstein Date: Mon, 30 Mar 2015 22:28:39 -0400 Subject: [PATCH 61/68] Issue #1279226 by attiks, ericduran, Wim Leers, sun, David_Rothstein, nod_: Allow sites and modules to skip loading jQuery and Drupal JavaScript libraries on pages where they won't be used --- CHANGELOG.txt | 4 + includes/common.inc | 40 +++++++-- modules/simpletest/tests/common.test | 130 +++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 62993ae5a..5e299f5cb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,10 @@ Drupal 7.36, xxxx-xx-xx (development version) ----------------------- +- Added a 'javascript_always_use_jquery' variable which can be set to FALSE by + sites that may not need jQuery loaded on all pages, and a 'requires_jquery' + option to drupal_add_js() which modules can set to FALSE when adding + JavaScript files that have no dependency on jQuery. - Fixed incorrect foreign keys in the User module's role_permission and users_roles database tables. - Changed permission descriptions throughout Drupal core to consistently link diff --git a/includes/common.inc b/includes/common.inc index 0fac77201..0437ec1a4 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -4162,6 +4162,13 @@ function drupal_region_class($region) { * else being the same, JavaScript added by a call to drupal_add_js() that * happened later in the page request gets added to the page after one for * which drupal_add_js() happened earlier in the page request. + * - requires_jquery: Set this to FALSE if the JavaScript you are adding does + * not have a dependency on jQuery. Defaults to TRUE, except for JavaScript + * settings where it defaults to FALSE. This is used on sites that have the + * 'javascript_always_use_jquery' variable set to FALSE; on those sites, if + * all the JavaScript added to the page by drupal_add_js() does not have a + * dependency on jQuery, then for improved front-end performance Drupal + * will not add jQuery and related libraries and settings to the page. * - defer: If set to TRUE, the defer attribute is set on the