diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e1503e4f47..da509c4c6b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,23 +10,35 @@ ################ ################ -# Includes +# Workflow # -# Additional configuration can be provided through includes. -# One advantage of include files is that if they are updated upstream, the -# changes affect all pipelines using that include. +# Define conditions for when the pipeline will run. +# For example: +# * On commit +# * On merge request +# * On manual trigger +# * etc. +# https://docs.gitlab.com/ee/ci/jobs/job_control.html#specify-when-jobs-run-with-rules # -# Includes can be overriden by re-declaring anything provided in an include, -# here in gitlab-ci.yml -# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values +# Pipelines can also be configured to run on a schedule,though they still must meet the conditions defined in Workflow and Rules. This can be used, for example, to do nightly regression testing: +# https://gitlab.com/help/ci/pipelines/schedules ################ -include: - - project: $_GITLAB_TEMPLATES_REPO - ref: $_GITLAB_TEMPLATES_REF - file: - - '/includes/include.drupalci.variables.yml' - - '/includes/include.drupalci.workflows.yml' +workflow: + rules: + # These 3 rules from https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml + # Run on merge requests + - if: $CI_PIPELINE_SOURCE == 'merge_request_event' + # Run when called from an upstream pipeline https://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html?tab=Multi-project+pipeline#use-rules-to-control-downstream-pipeline-jobs + - if: $CI_PIPELINE_SOURCE == 'pipeline' + # Run on commits. + - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ROOT_NAMESPACE == "project" + # The last rule above blocks manual and scheduled pipelines on non-default branch. The rule below allows them: + - if: $CI_PIPELINE_SOURCE == "schedule" && $CI_PROJECT_ROOT_NAMESPACE == "project" + # Run if triggered from Web using 'Run Pipelines' + - if: $CI_PIPELINE_SOURCE == "web" + # Run if triggered from WebIDE + - if: $CI_PIPELINE_SOURCE == "webide" ################ # Variables @@ -42,9 +54,11 @@ include: ################ variables: + _CONFIG_DOCKERHUB_ROOT: "drupalci" _TARGET_PHP: "8.1" CONCURRENCY: 15 GIT_DEPTH: "3" + COMPOSER_ALLOW_SUPERUSER: 1 ################ # Stages diff --git a/.htaccess b/.htaccess index 7dd1d14c9f..7cf5756a88 100644 --- a/.htaccess +++ b/.htaccess @@ -149,6 +149,13 @@ DirectoryIndex index.php index.html index.htm # Various header fixes. + # Disable content sniffing for all responses, since it's an attack vector. + # This header is also set in drupal_deliver_html_page(), which depending on + # Apache configuration might get placed in the 'onsuccess' table. To prevent + # header duplication, unset that one prior to setting in the 'always' table. + # See "To circumvent this limitation..." in + # https://httpd.apache.org/docs/current/mod/mod_headers.html. + Header onsuccess unset X-Content-Type-Options # Disable content sniffing, since it's an attack vector. Header always set X-Content-Type-Options nosniff # Disable Proxy header, since it's an attack vector. diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 73c7f8ff16..d312a68dad 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +Drupal 7.101, 2024-06-05 +----------------------- +- Various security improvements +- Various bug fixes, optimizations and improvements + Drupal 7.100, 2024-03-06 ------------------------ - Security improvements diff --git a/includes/ajax.inc b/includes/ajax.inc index db53e31b26..b511ad358e 100644 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -323,7 +323,7 @@ function ajax_render($commands = array()) { function ajax_get_form() { $form_state = form_state_defaults(); - $form_build_id = $_POST['form_build_id']; + $form_build_id = (isset($_POST['form_build_id']) ? $_POST['form_build_id'] : ''); // Get the form from the cache. $form = form_get_cache($form_build_id, $form_state); diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 42449f8eb1..bbbdcb01c8 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.100'); +define('VERSION', '7.101'); /** * Core API compatibility. @@ -2310,7 +2310,8 @@ function drupal_block_denied($ip) { * The number of random bytes to fetch and base64 encode. * * @return string - * The base64 encoded result will have a length of up to 4 * $byte_count. + * A base-64 encoded string, with + replaced with -, / with _ and any = + * padding characters removed. */ function drupal_random_key($byte_count = 32) { return drupal_base64_encode(drupal_random_bytes($byte_count)); diff --git a/includes/common.inc b/includes/common.inc index 3f54119fd2..69831b6137 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -5561,8 +5561,27 @@ function drupal_cron_run() { DrupalQueue::get($queue_name)->createQueue(); } + $module_previous = ''; + + // If detailed logging isn't enabled, don't log individual execution times. + $time_logging_enabled = variable_get('cron_detailed_logging', DRUPAL_CRON_DETAILED_LOGGING); + // Iterate through the modules calling their cron handlers (if any): foreach (module_implements('cron') as $module) { + if ($time_logging_enabled) { + if (!$module_previous) { + watchdog('cron', 'Starting execution of @module_cron().', array('@module' => $module)); + } + else { + watchdog('cron', 'Starting execution of @module_cron(), execution of @module_previous_cron() took @time.', array( + '@module' => $module, + '@module_previous' => $module_previous, + '@time' => timer_read('cron_' . $module_previous) . 'ms', + )); + } + timer_start('cron_' . $module); + } + // Do not let an exception thrown by one module disturb another. try { module_invoke($module, 'cron'); @@ -5570,6 +5589,20 @@ function drupal_cron_run() { catch (Exception $e) { watchdog_exception('cron', $e); } + + if ($time_logging_enabled) { + timer_stop('cron_' . $module); + $module_previous = $module; + } + } + + if ($time_logging_enabled) { + if ($module_previous) { + watchdog('cron', 'Execution of @module_previous_cron() took @time.', array( + '@module_previous' => $module_previous, + '@time' => timer_read('cron_' . $module_previous) . 'ms', + )); + } } // Record cron time. @@ -8229,7 +8262,10 @@ function entity_get_controller($entity_type) { $controllers = &drupal_static(__FUNCTION__, array()); if (!isset($controllers[$entity_type])) { $type_info = entity_get_info($entity_type); - $class = $type_info['controller class']; + // Explicitly fail for malformed entities missing a valid controller class. + if (!isset($type_info['controller class']) || !class_exists($class = $type_info['controller class'])) { + throw new EntityMalformedException(t('Missing or non-existent controller class on entity of type @entity_type.', array('@entity_type' => $entity_type))); + } $controllers[$entity_type] = new $class($entity_type); } return $controllers[$entity_type]; diff --git a/includes/errors.inc b/includes/errors.inc index 4b1f028018..7ab84a708d 100644 --- a/includes/errors.inc +++ b/includes/errors.inc @@ -216,7 +216,7 @@ function _drupal_log_error($error, $fatal = FALSE) { if ($fatal) { // When called from CLI, simply output a plain text message. print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n"; - exit; + exit(1); } } diff --git a/includes/file.inc b/includes/file.inc index 7b4c2cd268..e18ffa2e36 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -2086,7 +2086,7 @@ function file_download() { $target = implode('/', $args); $uri = $scheme . '://' . $target; $uri = file_uri_normalize_dot_segments($uri); - if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) { + if (file_stream_wrapper_valid_scheme($scheme) && is_file($uri)) { $headers = file_download_headers($uri); if (count($headers)) { file_transfer($uri, $headers); diff --git a/includes/unicode.inc b/includes/unicode.inc index f41eb8f8ce..14f9251b27 100644 --- a/includes/unicode.inc +++ b/includes/unicode.inc @@ -478,6 +478,9 @@ function decode_entities($text) { */ function drupal_strlen($text) { global $multibyte; + if (is_null($text)) { + return 0; + } if ($multibyte == UNICODE_MULTIBYTE) { return mb_strlen($text); } diff --git a/includes/updater.inc b/includes/updater.inc index d86e3b6319..749cd94cb4 100644 --- a/includes/updater.inc +++ b/includes/updater.inc @@ -245,9 +245,6 @@ class Updater { // Make sure the installation parent directory exists and is writable. $this->prepareInstallDirectory($filetransfer, $args['install_dir']); - // Note: If the project is installed in sites/all, it will not be - // deleted. It will be installed in sites/default as that will override - // the sites/all reference and not break other sites which are using it. if (is_dir($args['install_dir'] . '/' . $this->name)) { // Remove the existing installed file. $filetransfer->removeDirectory($args['install_dir'] . '/' . $this->name); diff --git a/modules/dblog/dblog.test b/modules/dblog/dblog.test index 9c266656fd..c7e5367288 100644 --- a/modules/dblog/dblog.test +++ b/modules/dblog/dblog.test @@ -128,12 +128,30 @@ class DBLogTestCase extends DrupalWebTestCase { $count = db_query('SELECT COUNT(wid) FROM {watchdog}')->fetchField(); $this->assertTrue($count > $row_limit, format_string('Dblog row count of @count exceeds row limit of @limit', array('@count' => $count, '@limit' => $row_limit))); + // Get last ID to compare against; log entries get deleted, so we can't + // reliably add the number of newly created log entries to the current count + // to measure number of log entries created by cron. + $last_id = db_query('SELECT MAX(wid) FROM {watchdog}')->fetchField(); + // Run a cron job. $this->cronRun(); - // Verify that the database log row count equals the row limit plus one - // because cron adds a record after it runs. - $count = db_query('SELECT COUNT(wid) FROM {watchdog}')->fetchField(); - $this->assertTrue($count == $row_limit + 1, format_string('Dblog row count of @count equals row limit of @limit plus one', array('@count' => $count, '@limit' => $row_limit))); + + // Get last ID after cron was run. + $current_id = db_query('SELECT MAX(wid) FROM {watchdog}')->fetchField(); + + // Only one final "cron is finished" message should be logged. + $this->assertEqual($current_id - $last_id, 1, format_string('Cron added @count of @expected new log entries', array('@count' => $current_id - $last_id, '@expected' => 1))); + + // Test enabling of detailed cron logging. + // Get the number of enabled modules. Cron adds a log entry for each module. + $module_count = count(module_implements('cron')); + variable_set('cron_detailed_logging', 1); + $last_id = db_query('SELECT MAX(wid) FROM {watchdog}')->fetchField(); + $this->cronRun(); + $current_id = db_query('SELECT MAX(wid) FROM {watchdog}')->fetchField(); + + // The number of log entries created. + $this->assertEqual($current_id - $last_id, $module_count + 2, format_string('Cron added @count of @expected new log entries', array('@count' => $current_id - $last_id, '@expected' => $module_count + 2))); } /** diff --git a/modules/node/node.admin.inc b/modules/node/node.admin.inc index eead4ea988..69b4939701 100644 --- a/modules/node/node.admin.inc +++ b/modules/node/node.admin.inc @@ -165,12 +165,7 @@ function node_filter_form() { ); foreach ($session as $filter) { list($type, $value) = $filter; - if ($type == 'term') { - // Load term name from DB rather than search and parse options array. - $value = module_invoke('taxonomy', 'term_load', $value); - $value = $value->name; - } - elseif ($type == 'language') { + if ($type == 'language') { $value = $value == LANGUAGE_NONE ? t('Language neutral') : module_invoke('locale', 'language_name', $value); } else { diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 08b8dfeb0e..1c7129b813 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -560,23 +560,22 @@ public function run(array $methods = array()) { 'function' => $class . '->' . $method . '()', ); $completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller); - $this->setUp(); - if ($this->setup) { - try { + try { + $this->setUp(); + if ($this->setup) { $this->$method(); - // Finish up. + $this->tearDown(); } - catch (Throwable $e) { - $this->exceptionHandler($e); - } - catch (Exception $e) { - // Cater for older PHP versions. - $this->exceptionHandler($e); + else { + $this->fail(t("The test cannot be executed because it has not been set up properly.")); } - $this->tearDown(); } - else { - $this->fail(t("The test cannot be executed because it has not been set up properly.")); + catch (Throwable $e) { + $this->exceptionHandler($e); + } + catch (Exception $e) { + // Cater for older PHP versions. + $this->exceptionHandler($e); } // Remove the completion check record. DrupalTestCase::deleteAssert($completion_check_id); diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test index d271e0417a..c79babc925 100644 --- a/modules/simpletest/tests/ajax.test +++ b/modules/simpletest/tests/ajax.test @@ -618,4 +618,16 @@ class AJAXElementValidation extends AJAXTestCase { $this->assertNoText(t('Error message'), "No error message in resultant JSON"); $this->assertText('ajax_forms_test_validation_form_callback invoked', 'The correct callback was invoked'); } + + /** + * Try to open default Ajax callback without passing required data. + */ + function testAJAXPathWithoutData() { + $this->drupalGet('system/ajax'); + $query_parameters = array( + ':type' => 'php', + ':severity' => WATCHDOG_WARNING, + ); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND severity = :severity', $query_parameters)->fetchField(), 0, 'No warning message appears in the logs.'); + } } diff --git a/modules/simpletest/tests/entity_crud.test b/modules/simpletest/tests/entity_crud.test index c7d66509a2..3159161c4f 100644 --- a/modules/simpletest/tests/entity_crud.test +++ b/modules/simpletest/tests/entity_crud.test @@ -58,4 +58,33 @@ class EntityLoadTestCase extends DrupalWebTestCase { $nodes_loaded = entity_load('node', array('1.', '2')); $this->assertEqual(count($nodes_loaded), 1); } + + /** + * Tests the controller class loading functionality on non-existing entity + * types and on entities without valid controller class. + */ + public function testEntityLoadInvalidControllerClass() { + // Ensure that loading a non-existing entity type will throw an + // EntityMalformedException. + try { + entity_load('test', array('1')); + $this->fail(t('Cannot load a controller class on non-existing entity type.')); + } + catch (EntityMalformedException $e) { + $this->pass(t('Cannot load a controller class on non-existing entity type.')); + } + + // Ensure that loading an entity without valid controller class will throw + // an EntityMalformedException. + module_enable(array('entity_crud_hook_test')); + variable_set('entity_crud_hook_test_alter_controller_class', TRUE); + try { + entity_load('node', array('1')); + $this->fail(t('Cannot load a missing or non-existent controller class.')); + } + catch (EntityMalformedException $e) { + $this->pass(t('Cannot load a missing or non-existent controller class.')); + } + variable_set('entity_crud_hook_test_alter_controller_class', FALSE); + } } diff --git a/modules/simpletest/tests/entity_crud_hook_test.module b/modules/simpletest/tests/entity_crud_hook_test.module index d25dff1b3c..eebebc8c5d 100644 --- a/modules/simpletest/tests/entity_crud_hook_test.module +++ b/modules/simpletest/tests/entity_crud_hook_test.module @@ -249,3 +249,13 @@ function entity_crud_hook_test_taxonomy_vocabulary_delete() { function entity_crud_hook_test_user_delete() { $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); } + +/** + * Implements hook_entity_info_alter(). + */ +function entity_crud_hook_test_entity_info_alter(&$entity_info) { + if (variable_get('entity_crud_hook_test_alter_controller_class', FALSE)) { + // Set the controller class for nodes to NULL. + $entity_info['node']['controller class'] = NULL; + } +} diff --git a/modules/simpletest/tests/file.test b/modules/simpletest/tests/file.test index 8121b80334..80d1dd3839 100644 --- a/modules/simpletest/tests/file.test +++ b/modules/simpletest/tests/file.test @@ -2616,11 +2616,16 @@ class FileDownloadTest extends FileTestCase { $url = file_create_url($file->uri); // Set file_test access header to allow the download. + file_test_reset(); file_test_set_return('download', array('x-foo' => 'Bar')); $this->drupalGet($url); $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-foo'], 'Bar', 'Found header set by file_test module on private download.'); $this->assertResponse(200, 'Correctly allowed access to a file when file_test provides headers.'); + // Ensure hook_file_download is fired correctly. + $hooks_results = file_test_get_all_calls(); + $file_uri = !empty($hooks_results['download']) ? reset($hooks_results['download'][0]) : ''; + $this->assertEqual($file->uri, $file_uri); // Test that the file transferred correctly. $this->assertEqual($contents, $this->content, 'Contents of the file are correct.'); @@ -2631,9 +2636,23 @@ class FileDownloadTest extends FileTestCase { $this->assertResponse(403, 'Correctly denied access to a file when file_test sets the header to -1.'); // Try non-existent file. + file_test_reset(); $url = file_create_url('private://' . $this->randomName()); $this->drupalHead($url); $this->assertResponse(404, 'Correctly returned 404 response for a non-existent file.'); + // Assert that hook_file_download is not called. + $hooks_results = file_test_get_all_calls(); + $hook_download_results = isset($hooks_results['download']) ? $hooks_results['download'] : NULL; + $this->assertEqual(array(), $hook_download_results); + + // Try requesting the private file url without a file specified. + file_test_reset(); + $this->drupalGet('system/files'); + $this->assertResponse(404, 'Correctly returned 404 response for a private file url without a file specified.'); + // Assert that hook_file_download is not called. + $hooks_results = file_test_get_all_calls(); + $hook_download_results = isset($hooks_results['download']) ? $hooks_results['download'] : NULL; + $this->assertEqual(array(), $hook_download_results); } /** diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index c8197fb088..396a5261eb 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -768,6 +768,109 @@ class FormValidationTestCase extends DrupalWebTestCase { } } +/** + * Tests validation of additional Form API properties. + * + * Limited to maxlength validation at present. + */ +class FormsElementsValidationTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Form element validation - misc', + 'description' => 'Tests miscellaneous form element validation mechanisms.', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Tests #maxlength validation. + */ + public function testMaxlengthValidation() { + $max_length = 5; + // The field types that support #maxlength. + $form = array( + 'textfield' => array( + '#type' => 'textfield', + '#title' => 'Textfield', + '#required' => FALSE, + '#maxlength' => $max_length, + ), + 'password' => array( + '#type' => 'password', + '#title' => 'Password', + '#maxlength' => $max_length, + ), + ); + + $edit = array( + 'textfield' => $this->randomString($max_length + 1), + 'password' => $this->randomString($max_length + 1), + ); + list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, $edit); + $this->assertFalse(empty($errors), 'Form with overly long inputs returned errors.'); + $this->assertTrue(isset($errors['textfield']) && strpos($errors['textfield'], 'cannot be longer than') !== FALSE, 'Long input error in textfield.'); + $this->assertTrue(isset($errors['password']) && strpos($errors['password'], 'cannot be longer than') !== FALSE, 'Long input error in password.'); + + // This test for NULL inputs cannot be performed using the drupalPost() method. + $edit['textfield'] = NULL; + $edit['password'] = NULL; + list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, $edit); + $this->assertTrue(empty($errors), 'Form with NULL inputs did not return errors.'); + + $edit['textfield'] = $this->randomString($max_length); + $edit['password'] = $this->randomString($max_length); + list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, $edit); + $this->assertTrue(empty($errors), 'Form with maxlength inputs did not return errors.'); + + } + + /** + * Helper function for the option check test to submit a form while collecting errors. + * + * Copied from FormsElementsTableSelectFunctionalTest. + * + * @param $form_element + * A form element to test. + * @param $edit + * An array containing post data. + * + * @return + * An array containing the processed form, the form_state and any errors. + */ + private function formSubmitHelper($form, $edit) { + $form_id = $this->randomName(); + $form_state = form_state_defaults(); + + $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); + + $form_state['input'] = $edit; + $form_state['input']['form_id'] = $form_id; + + // The form token CSRF protection should not interfere with this test, + // so we bypass it by marking this test form as programmed. + $form_state['programmed'] = TRUE; + + drupal_prepare_form($form_id, $form, $form_state); + + drupal_process_form($form_id, $form, $form_state); + + $errors = form_get_errors(); + + // Clear errors and messages. + drupal_get_messages(); + form_clear_error(); + + // Return the processed form together with form_state and errors + // to allow the caller lowlevel access to the form. + return array($form, $form_state, $errors); + } + +} + /** * Test form element labels, required markers and associated output. */ diff --git a/modules/simpletest/tests/unicode.test b/modules/simpletest/tests/unicode.test index bd96553abb..d0e631d8f7 100644 --- a/modules/simpletest/tests/unicode.test +++ b/modules/simpletest/tests/unicode.test @@ -118,10 +118,12 @@ class UnicodeUnitTest extends DrupalUnitTestCase { $testcase = array( 'tHe QUIcK bRoWn' => 15, 'ÜBER-åwesome' => 12, + 'NULL' => 0, ); foreach ($testcase as $input => $output) { - $this->assertEqual(drupal_strlen($input), $output, format_string('%input length is %output', array('%input' => $input, '%output' => $output))); + $tested_value = ($input === 'NULL' ? NULL : $input); + $this->assertEqual(drupal_strlen($tested_value), $output, format_string('%input length is %output', array('%input' => $input, '%output' => $output))); } } diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc index 664e21a733..9ddecc27f4 100644 --- a/modules/system/system.admin.inc +++ b/modules/system/system.admin.inc @@ -1645,6 +1645,12 @@ function system_cron_settings() { '#default_value' => variable_get('cron_safe_threshold', DRUPAL_CRON_DEFAULT_THRESHOLD), '#options' => array(0 => t('Never')) + drupal_map_assoc(array(3600, 10800, 21600, 43200, 86400, 604800), 'format_interval'), ); + $form['cron']['cron_detailed_logging'] = array( + '#type' => 'checkbox', + '#title' => t('Detailed cron logging'), + '#default_value' => variable_get('cron_detailed_logging', DRUPAL_CRON_DETAILED_LOGGING), + '#description' => t('Run times of individual cron jobs will be written to watchdog'), + ); return system_settings_form($form); } diff --git a/modules/system/system.module b/modules/system/system.module index dc6ccfec17..9d7422f53c 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -15,6 +15,11 @@ define('DRUPAL_MAXIMUM_TEMP_FILE_AGE', 21600); */ define('DRUPAL_CRON_DEFAULT_THRESHOLD', 10800); +/** + * Detailed cron logging disabled by default. + */ +define('DRUPAL_CRON_DETAILED_LOGGING', 0); + /** * New users will be set to the default time zone at registration. */ diff --git a/modules/system/system.tar.inc b/modules/system/system.tar.inc index fd012e657e..700e8a346b 100644 --- a/modules/system/system.tar.inc +++ b/modules/system/system.tar.inc @@ -341,7 +341,7 @@ class Archive_Tar * single string with names separated by a single * blank space. * - * @return true on success, false on error. + * @return bool true on success, false on error. * @see createModify() */ public function create($p_filelist) @@ -361,7 +361,7 @@ class Archive_Tar * single string with names separated by a single * blank space. * - * @return true on success, false on error. + * @return bool true on success, false on error. * @see createModify() * @access public */ @@ -504,7 +504,7 @@ class Archive_Tar * each element in the list, when * relevant. * - * @return true on success, false on error. + * @return bool true on success, false on error. */ public function addModify($p_filelist, $p_add_dir, $p_remove_dir = '') { @@ -557,7 +557,7 @@ class Archive_Tar * gid => the group ID of the file * (default = 0 = root) * - * @return true on success, false on error. + * @return bool true on success, false on error. */ public function addString($p_filename, $p_string, $p_datetime = false, $p_params = array()) { @@ -683,7 +683,7 @@ class Archive_Tar * @param boolean $p_preserve Preserve user/group ownership of files * @param boolean $p_symlinks Allow symlinks. * - * @return true on success, false on error. + * @return bool true on success, false on error. * @see extractModify() */ public function extractList($p_filelist, $p_path = '', $p_remove_path = '', $p_preserve = false, $p_symlinks = true) @@ -721,7 +721,7 @@ class Archive_Tar * list of parameters, in the format attribute code + attribute values : * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ','); * - * @return true on success, false on error. + * @return bool true on success, false on error. */ public function setAttribute() { @@ -2178,7 +2178,7 @@ class Archive_Tar if ($v_extract_file) { if ($v_header['typeflag'] == "5") { if (!@file_exists($v_header['filename'])) { - if (!@mkdir($v_header['filename'], 0777)) { + if (!@mkdir($v_header['filename'], 0775)) { $this->_error( 'Unable to create directory {' . $v_header['filename'] . '}' @@ -2511,7 +2511,7 @@ class Archive_Tar return false; } - if (!@mkdir($p_dir, 0777)) { + if (!@mkdir($p_dir, 0775)) { $this->_error("Unable to create directory '$p_dir'"); return false; } diff --git a/modules/user/user.api.php b/modules/user/user.api.php index b9dc95f15d..65d460f963 100644 --- a/modules/user/user.api.php +++ b/modules/user/user.api.php @@ -302,12 +302,12 @@ function hook_user_update(&$edit, $account, $category) { /** * The user just logged in. * - * @param $edit - * The array of form values submitted by the user. + * @param $form_state + * A keyed array containing the current state of the login form. * @param $account * The user object on which the operation was just performed. */ -function hook_user_login(&$edit, $account) { +function hook_user_login(&$form_state, $account) { // If the user has a NULL time zone, notify them to set a time zone. if (!$account->timezone && variable_get('configurable_timezones', 1) && variable_get('empty_timezone_message', 0)) { drupal_set_message(t('Configure your account time zone setting.', array('@user-edit' => url("user/$account->uid/edit", array('query' => drupal_get_destination(), 'fragment' => 'edit-timezone'))))); diff --git a/modules/user/user.module b/modules/user/user.module index 145fc480c5..9103f5e5f2 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -1283,7 +1283,12 @@ function user_account_form_validate($form, &$form_state) { elseif ((bool) db_select('users')->fields('users', array('uid'))->condition('uid', $account->uid, '<>')->condition('mail', db_like($form_state['values']['mail']), 'LIKE')->range(0, 1)->execute()->fetchField()) { // Format error message dependent on whether the user is logged in or not. if ($GLOBALS['user']->uid) { - form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $form_state['values']['mail']))); + // Do not fail the validation if user has not changed e-mail address. + // This means that multiple accounts with the same e-mail address exist + // and the logged-in user is one of them. + if ((isset($account->mail) && $account->mail != $mail) || !isset($account->mail)) { + form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $form_state['values']['mail']))); + } } else { form_set_error('mail', t('The e-mail address %email is already registered. Have you forgotten your password?', array('%email' => $form_state['values']['mail'], '@password' => url('user/password')))); @@ -2312,12 +2317,12 @@ function user_authenticate($name, $password) { * The function records a watchdog message about the new session, saves the * login timestamp, calls hook_user_login(), and generates a new session. * - * @param array $edit - * The array of form values submitted by the user. + * @param array $form_state + * A keyed array containing the current state of the login form. * * @see hook_user_login() */ -function user_login_finalize(&$edit = array()) { +function user_login_finalize(&$form_state = array()) { global $user; watchdog('user', 'Session opened for %name.', array('%name' => $user->name)); // Update the user table timestamp noting user has logged in. @@ -2329,11 +2334,12 @@ function user_login_finalize(&$edit = array()) { ->execute(); // Regenerate the session ID to prevent against session fixation attacks. - // This is called before hook_user in case one of those functions fails - // or incorrectly does a redirect which would leave the old session in place. + // This is called before hook_user_login() in case one of those functions + // fails or incorrectly does a redirect which would leave the old session in + // place. drupal_session_regenerate(); - user_module_invoke('login', $edit, $user); + user_module_invoke('login', $form_state, $user); } /** diff --git a/modules/user/user.test b/modules/user/user.test index f3e1002a4c..aad1f57937 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -144,10 +144,7 @@ class UserRegistrationTestCase extends DrupalWebTestCase { variable_set('configurable_timezones', 1); variable_set('date_default_timezone', 'Europe/Brussels'); - // Check that the account information fieldset's options are not displayed - // is a fieldset if there is not more than one fieldset in the form. $this->drupalGet('user/register'); - $this->assertNoRaw('
Account information', 'Account settings fieldset was hidden.'); $edit = array(); $edit['name'] = $name = $this->randomName(); @@ -2343,6 +2340,42 @@ class UserEditTestCase extends DrupalWebTestCase { $this->drupalLogin($user1); $this->drupalLogout(); } + + /** + * Tests that the user can edit the account when another user with the same + * e-mail address exists. + */ + public function testUserEditDuplicateEmail() { + // Create two regular users. + $user1 = $this->drupalCreateUser(array('change own username')); + $user2 = $this->drupalCreateUser(array('change own username')); + + // Change the e-mail address of the user2 to have the same e-mail address + // as the user1. + db_update('users') + ->fields(array('mail' => $user1->mail)) + ->condition('uid', $user2->uid) + ->execute(); + + $this->drupalLogin($user2); + $edit['name'] = $user2->name; + $this->drupalPost("user/" . $user2->uid . "/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $this->drupalLogout(); + + // Change the e-mail address of the user2 to have the same e-mail address + // as the user1, except that the first letter will be uppercase. + db_update('users') + ->fields(array('mail' => ucfirst($user1->mail))) + ->condition('uid', $user2->uid) + ->execute(); + + $this->drupalLogin($user2); + $edit['name'] = $user2->name; + $this->drupalPost("user/" . $user2->uid . "/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $this->drupalLogout(); + } } /** diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 1cf7f8a1b7..fb6040c6ff 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -886,3 +886,12 @@ * prevention and revert to the original behaviour. */ # $conf['javascript_use_double_submit_protection'] = FALSE; + +/** + * Cron logging. + * + * Optionally drupal_cron_run() can log each execution of hook_cron() together + * with the execution time. This is disabled by default to reduce log noise. Set + * this variable to TRUE in order to enable the additional logging. + */ +# $conf['cron_logging_enabled'] = TRUE;