diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5942a1c2d7..f82c8c638f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +Drupal 7.98, 2023-06-07 +----------------------- +- Various security improvements +- Various bug fixes, optimizations and improvements + Drupal 7.97, 2023-04-21 ----------------------- - Fix PHP 5.x regression caused by SA-CORE-2023-005 diff --git a/cron.php b/cron.php index c6ce5317e8..1f32e6f1f4 100644 --- a/cron.php +++ b/cron.php @@ -13,12 +13,12 @@ include_once DRUPAL_ROOT . '/includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); -if (!isset($_GET['cron_key']) || variable_get('cron_key', 'drupal') != $_GET['cron_key']) { - watchdog('cron', 'Cron could not run because an invalid key was used.', array(), WATCHDOG_NOTICE); - drupal_access_denied(); -} -elseif (variable_get('maintenance_mode', 0)) { +if (variable_get('maintenance_mode', 0)) { watchdog('cron', 'Cron could not run because the site is in maintenance mode.', array(), WATCHDOG_NOTICE); + drupal_site_offline(); +} +elseif (!isset($_GET['cron_key']) || variable_get('cron_key', 'drupal') != $_GET['cron_key']) { + watchdog('cron', 'Cron could not run because an invalid key was used.', array(), WATCHDOG_NOTICE); drupal_access_denied(); } else { diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index f6c93b9c0d..767753c96e 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.97'); +define('VERSION', '7.98'); /** * Core API compatibility. @@ -2328,15 +2328,30 @@ function drupal_base64_encode($string) { /** * Returns a string of highly randomized bytes (over the full 8-bit range). * - * This function is better than simply calling mt_rand() or any other built-in - * PHP function because it can return a long string of bytes (compared to < 4 - * bytes normally from mt_rand()) and uses the best available pseudo-random - * source. + * On PHP 7 and later, this function is a wrapper around the built-in PHP + * function random_bytes(). If that function does not exist or cannot find an + * appropriate source of randomness, this function is better than simply calling + * mt_rand() or any other built-in PHP function because it can return a long + * string of bytes (compared to < 4 bytes normally from mt_rand()) and uses the + * best available pseudo-random source. * - * @param $count + * @param int $count * The number of characters (bytes) to return in the string. + * + * @return string + * A randomly generated string. */ function drupal_random_bytes($count) { + if (function_exists('random_bytes')) { + try { + return random_bytes($count); + } + catch (Exception $e) { + // An appropriate source of randomness could not be found. Fall back to a + // less secure implementation. + } + } + // $random_state does not use drupal_static as it stores random bytes. static $random_state, $bytes, $has_openssl; diff --git a/includes/common.inc b/includes/common.inc index 29e3d317de..485eccd690 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -7380,6 +7380,13 @@ function _drupal_schema_initialize(&$schema, $module, $remove_descriptions = TRU unset($field['description']); } } + // Set the type key for all fields where it is not set (mostly when using + // datatabase specific data types). + foreach ($table['fields'] as &$field) { + if (!isset($field['type'])) { + $field['type'] = NULL; + } + } } } diff --git a/includes/file.inc b/includes/file.inc index 2dd7a7ffa4..d4a4c4c72f 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -69,6 +69,13 @@ define('FILE_EXISTS_ERROR', 2); */ define('FILE_STATUS_PERMANENT', 1); +/** + * A pipe-separated list of insecure extensions. + * + * @see file_munge_filename(), file_save_upload() + */ +define('FILE_INSECURE_EXTENSIONS', 'php|phar|pl|py|cgi|asp|js|phtml'); + /** * Provides Drupal stream wrapper registry. * @@ -1184,9 +1191,8 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) { $whitelist = array_unique(explode(' ', strtolower(trim($extensions)))); - // Remove unsafe extensions from the list of allowed extensions. The list is - // copied from file_save_upload(). - $whitelist = array_diff($whitelist, explode('|', 'php|phar|pl|py|cgi|asp|js')); + // Remove unsafe extensions from the list of allowed extensions. + $whitelist = array_diff($whitelist, explode('|', FILE_INSECURE_EXTENSIONS)); // Split the filename up by periods. The first part becomes the basename // the last part the final extension. @@ -1566,7 +1572,7 @@ function file_save_upload($form_field_name, $validators = array(), $destination // rename filename.php.foo and filename.php to filename.php_.foo_.txt and // filename.php_.txt, respectively). Don't rename if 'allow_insecure_uploads' // evaluates to TRUE. - if (preg_match('/\.(php|phar|pl|py|cgi|asp|js)(\.|$)/i', $file->filename)) { + if (preg_match('/\.(' . FILE_INSECURE_EXTENSIONS . ')(\.|$)/i', $file->filename)) { // If the file will be rejected anyway due to a disallowed extension, it // should not be renamed; rather, we'll let file_validate_extensions() // reject it below. @@ -1758,7 +1764,7 @@ function file_validate(stdClass &$file, $validators = array()) { // malicious extension. Contributed and custom code that calls this method // needs to take similar steps if they need to permit files with malicious // extensions to be uploaded. - if (empty($errors) && !variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|phar|pl|py|cgi|asp|js)(\.|$)/i', $file->filename)) { + if (empty($errors) && !variable_get('allow_insecure_uploads', 0) && preg_match('/\.(' . FILE_INSECURE_EXTENSIONS . ')(\.|$)/i', $file->filename)) { $errors[] = t('For security reasons, your upload has been rejected.'); } diff --git a/includes/form.inc b/includes/form.inc index fe52b0afaa..d2a6c5ec28 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -1682,7 +1682,10 @@ function form_clear_error() { } /** - * Returns an associative array of all errors. + * Returns an associative array of all errors if any. + * + * @return array|null + * The form errors if any, NULL otherwise. */ function form_get_errors() { $form = form_set_error(); @@ -2307,8 +2310,8 @@ function form_state_values_clean(&$form_state) { * A keyed array containing the current state of the form. * * @return - * The data that will appear in the $form_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_image_button_value($form, $input, $form_state) { if ($input !== FALSE) { @@ -2353,8 +2356,8 @@ function form_type_image_button_value($form, $input, $form_state) { * the element's default value should be returned. * * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_checkbox_value($element, $input = FALSE) { if ($input === FALSE) { @@ -2394,8 +2397,8 @@ function form_type_checkbox_value($element, $input = FALSE) { * the element's default value should be returned. * * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_checkboxes_value($element, $input = FALSE) { if ($input === FALSE) { @@ -2435,8 +2438,8 @@ function form_type_checkboxes_value($element, $input = FALSE) { * the element's default value should be returned. * * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_tableselect_value($element, $input = FALSE) { // If $element['#multiple'] == FALSE, then radio buttons are displayed and @@ -2471,8 +2474,8 @@ function form_type_tableselect_value($element, $input = FALSE) { * element's default value is returned. Defaults to FALSE. * * @return - * The data that will appear in the $element_state['values'] collection for - * this element. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_radios_value(&$element, $input = FALSE) { if ($input !== FALSE) { @@ -2510,8 +2513,8 @@ function form_type_radios_value(&$element, $input = FALSE) { * the element's default value should be returned. * * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_password_confirm_value($element, $input = FALSE) { if ($input === FALSE) { @@ -2541,8 +2544,8 @@ function form_type_password_confirm_value($element, $input = FALSE) { * the element's default value should be returned. * * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_select_value($element, $input = FALSE) { if ($input !== FALSE) { @@ -2578,12 +2581,12 @@ function form_type_select_value($element, $input = FALSE) { * @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. + * 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. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_textarea_value($element, $input = FALSE) { if ($input !== FALSE && $input !== NULL) { @@ -2603,8 +2606,8 @@ function form_type_textarea_value($element, $input = FALSE) { * the element's default value should be returned. * * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_textfield_value($element, $input = FALSE) { if ($input !== FALSE && $input !== NULL) { @@ -2627,8 +2630,8 @@ function form_type_textfield_value($element, $input = FALSE) { * the element's default value should be returned. * * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. + * The data that will appear in $form_state['values'] for this element, or + * nothing to use the default. */ function form_type_token_value($element, $input = FALSE) { if ($input !== FALSE) { @@ -3377,6 +3380,30 @@ function form_process_actions($element, &$form_state) { return $element; } +/** + * Processes a form button element. + * + * @param $element + * An associative array containing the properties and children of the + * form button. + * @param $form_state + * The $form_state array for the form this element belongs to. + * + * @return + * The processed element. + */ +function form_process_button($element, &$form_state) { + // We normally want to add drupal.form-single-submit so that the double submit + // protection can be added to the site, however, with the addition of + // javascript_always_use_jquery, this would make most pages with a login + // block or a search form have jquery always added, changing what people who + // set the javascript_always_use_jquery variable to FALSE would have expected. + if (variable_get('javascript_always_use_jquery', TRUE) && variable_get('javascript_use_double_submit_protection', TRUE)) { + $element['#attached']['library'][] = array('system', 'drupal.form-single-submit'); + } + return $element; +} + /** * Processes a container element. * diff --git a/includes/locale.inc b/includes/locale.inc index 48dbdce988..9758055e61 100644 --- a/includes/locale.inc +++ b/includes/locale.inc @@ -1093,7 +1093,7 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL * * @param $report * Report array summarizing the number of changes done in the form: - * array(inserts, updates, deletes). + * array(additions, deletes, skips, updates). * @param $langcode * Language code to import string into. * @param $context diff --git a/includes/session.inc b/includes/session.inc index 1f3c1773ba..a174290409 100644 --- a/includes/session.inc +++ b/includes/session.inc @@ -87,19 +87,21 @@ function _drupal_session_read($sid) { // Otherwise, if the session is still active, we have a record of the // client's session in the database. If it's HTTPS then we are either have // a HTTPS session or we are about to log in so we check the sessions table - // for an anonymous session with the non-HTTPS-only cookie. + // for an anonymous session with the non-HTTPS-only cookie. The session ID + // that is in the user's cookie is hashed before being stored in the database + // as a security measure. Thus, we have to hash it to match the database. if ($is_https) { - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => $sid))->fetchObject(); + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => drupal_session_id($sid)))->fetchObject(); if (!$user) { if (isset($_COOKIE[$insecure_session_name])) { $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array( - ':sid' => $_COOKIE[$insecure_session_name])) + ':sid' => drupal_session_id($_COOKIE[$insecure_session_name]))) ->fetchObject(); } } } else { - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => drupal_session_id($sid)))->fetchObject(); } // We found the client's session record and they are an authenticated, @@ -185,17 +187,18 @@ function _drupal_session_write($sid, $value) { // Use the session ID as 'sid' and an empty string as 'ssid' by default. // _drupal_session_read() does not allow empty strings so that's a safe // default. - $key = array('sid' => $sid, 'ssid' => ''); + $key = array('sid' => drupal_session_id($sid), 'ssid' => ''); // On HTTPS connections, use the session ID as both 'sid' and 'ssid'. if ($is_https) { - $key['ssid'] = $sid; + $key['ssid'] = drupal_session_id($sid); // The "secure pages" setting allows a site to simultaneously use both // secure and insecure session cookies. If enabled and both cookies are - // presented then use both keys. + // presented then use both keys. The session ID from the cookie is + // hashed before being stored in the database as a security measure. if (variable_get('https', FALSE)) { $insecure_session_name = substr(session_name(), 1); if (isset($_COOKIE[$insecure_session_name])) { - $key['sid'] = $_COOKIE[$insecure_session_name]; + $key['sid'] = drupal_session_id($_COOKIE[$insecure_session_name]); } } } @@ -416,18 +419,18 @@ function drupal_session_regenerate() { 'httponly' => $params['httponly'], ); drupal_setcookie(session_name(), session_id(), $options); - $fields = array('sid' => session_id()); + $fields = array('sid' => drupal_session_id(session_id())); if ($is_https) { - $fields['ssid'] = session_id(); + $fields['ssid'] = drupal_session_id(session_id()); // If the "secure pages" setting is enabled, use the newly-created // insecure session identifier as the regenerated sid. if (variable_get('https', FALSE)) { - $fields['sid'] = $session_id; + $fields['sid'] = drupal_session_id($session_id); } } db_update('sessions') ->fields($fields) - ->condition($is_https ? 'ssid' : 'sid', $old_session_id) + ->condition($is_https ? 'ssid' : 'sid', drupal_session_id($old_session_id)) ->execute(); } elseif (isset($old_insecure_session_id)) { @@ -435,8 +438,8 @@ function drupal_session_regenerate() { // secure site but a session was active on the insecure site, update the // insecure session with the new session identifiers. db_update('sessions') - ->fields(array('sid' => $session_id, 'ssid' => session_id())) - ->condition('sid', $old_insecure_session_id) + ->fields(array('sid' => drupal_session_id($session_id), 'ssid' => drupal_session_id(session_id()))) + ->condition('sid', drupal_session_id($old_insecure_session_id)) ->execute(); } else { @@ -488,7 +491,7 @@ function _drupal_session_destroy($sid) { // Delete session data. db_delete('sessions') - ->condition($is_https ? 'ssid' : 'sid', $sid) + ->condition($is_https ? 'ssid' : 'sid', drupal_session_id($sid)) ->execute(); // Reset $_SESSION and $user to prevent a new session from being started @@ -598,3 +601,22 @@ function drupal_save_session($status = NULL) { } return $save_session; } + +/** + * Session ids are hashed by default before being stored in the database. + * + * This should only be done if any existing sessions have been updated, as + * reflected by the hash_session_ids variable. + * + * @param $id + * A session id. + * + * @return + * The session id which may have been hashed. + */ +function drupal_session_id($id) { + if (variable_get('hashed_session_ids_supported', FALSE) && !variable_get('do_not_hash_session_ids', FALSE)) { + $id = drupal_hash_base64($id); + } + return $id; +} diff --git a/misc/form-single-submit.js b/misc/form-single-submit.js new file mode 100644 index 0000000000..52e3002b97 --- /dev/null +++ b/misc/form-single-submit.js @@ -0,0 +1,60 @@ +(function ($) { + +/** + * Prevents consecutive form submissions of identical form values. + * + * Repetitive form submissions that would submit the identical form values are + * prevented, unless the form values are different from the previously + * submitted values. + * + * This is a simplified re-implementation of a user-agent behavior that should + * be natively supported by major web browsers, but at this time, only Firefox + * has a built-in protection. + * + * A form value-based approach ensures that the constraint is triggered for + * consecutive, identical form submissions only. Compared to that, a form + * button-based approach would (1) rely on [visible] buttons to exist where + * technically not required and (2) require more complex state management if + * there are multiple buttons in a form. + * + * This implementation is based on form-level submit events only and relies on + * jQuery's serialize() method to determine submitted form values. As such, the + * following limitations exist: + * + * - Event handlers on form buttons that preventDefault() do not receive a + * double-submit protection. That is deemed to be fine, since such button + * events typically trigger reversible client-side or server-side operations + * that are local to the context of a form only. + * - Changed values in advanced form controls, such as file inputs, are not part + * of the form values being compared between consecutive form submits (due to + * limitations of jQuery.serialize()). That is deemed to be acceptable, + * because if the user forgot to attach a file, then the size of HTTP payload + * will most likely be small enough to be fully passed to the server endpoint + * within (milli)seconds. If a user mistakenly attached a wrong file and is + * technically versed enough to cancel the form submission (and HTTP payload) + * in order to attach a different file, then that edge-case is not supported + * here. + * + * Lastly, all forms submitted via HTTP GET are idempotent by definition of HTTP + * standards, so excluded in this implementation. + */ +Drupal.behaviors.formSingleSubmit = { + attach: function () { + function onFormSubmit (e) { + var $form = $(e.currentTarget); + var formValues = $form.serialize(); + var previousValues = $form.attr('data-drupal-form-submit-last'); + if (previousValues === formValues) { + e.preventDefault(); + } + else { + $form.attr('data-drupal-form-submit-last', formValues); + } + } + + $('body').once('form-single-submit') + .delegate('form:not([method~="GET"])', 'submit.singleSubmit', onFormSubmit); + } +}; + +})(jQuery); diff --git a/modules/block/block.js b/modules/block/block.js index 721dedf123..69d49f206d 100644 --- a/modules/block/block.js +++ b/modules/block/block.js @@ -118,10 +118,18 @@ Drupal.behaviors.blockDrag = { tableDrag.rowObject = new tableDrag.row(row); // Find the correct region and insert the row as the last in the region. - table.find('.region-' + select[0].value + '-message').nextUntil('.region-message').last().before(row); - + tableDrag.rowObject = new tableDrag.row(row[0]); + var region_message = table.find('.region-' + select[0].value + '-message'); + var region_items = region_message.nextUntil('.region-message, .region-title'); + if (region_items.length) { + region_items.last().after(row); + } + // We found that region_message is the last row. + else { + region_message.after(row); + } // Modify empty regions with added or removed fields. - checkEmptyRegions(table, row); + checkEmptyRegions(table, tableDrag.rowObject); // Remove focus from selectbox. select.get(0).blur(); }); diff --git a/modules/comment/comment.api.php b/modules/comment/comment.api.php index 05912655b1..5df84bf6ab 100644 --- a/modules/comment/comment.api.php +++ b/modules/comment/comment.api.php @@ -105,7 +105,7 @@ function hook_comment_view_alter(&$build) { } /** - * The comment is being published by the moderator. + * Act on a comment that is being saved in a published state. * * @param $comment * Passes in the comment the action is being performed on. @@ -117,7 +117,7 @@ function hook_comment_publish($comment) { } /** - * The comment is being unpublished by the moderator. + * Act on a comment that is being saved in an unpublished state. * * @param $comment * Passes in the comment the action is being performed on. diff --git a/modules/comment/comment.module b/modules/comment/comment.module index 4fb165b14e..ad414f81af 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -730,10 +730,16 @@ function comment_node_page_additions($node) { $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); if ($cids = comment_get_thread($node, $mode, $comments_per_page)) { $comments = comment_load_multiple($cids); - comment_prepare_thread($comments); - $build = comment_view_multiple($comments, $node); - $build['pager']['#theme'] = 'pager'; - $additions['comments'] = $build; + if ($comments) { + comment_prepare_thread($comments); + $build = comment_view_multiple($comments, $node); + $build['pager']['#theme'] = 'pager'; + $additions['comments'] = $build; + } + elseif ($node->comment_count > 0) { + // Comment count has got out of line, update it. + db_query("UPDATE {node_comment_statistics} SET comment_count = 0 WHERE nid = :nid", array(':nid' => $node->nid)); + } } } @@ -1360,9 +1366,11 @@ function comment_node_update_index($node) { $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) { $comments = comment_load_multiple($cids); - comment_prepare_thread($comments); - $build = comment_view_multiple($comments, $node); - return drupal_render($build); + if ($comments) { + comment_prepare_thread($comments); + $build = comment_view_multiple($comments, $node); + return drupal_render($build); + } } } return ''; diff --git a/modules/comment/comment.test b/modules/comment/comment.test index 118b367184..0abdca57d3 100644 --- a/modules/comment/comment.test +++ b/modules/comment/comment.test @@ -2331,6 +2331,60 @@ class CommentNodeChangesTestCase extends CommentHelperCase { } } +/** + * Tests the behavior of comments when the comment author is deleted. + */ +class CommentAuthorDeletionTestCase extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment author deletion', + 'description' => 'Test the behavior of comments when the comment author is deleted.', + 'group' => 'Comment', + ); + } + + /** + * Tests that comments are correctly deleted when their author is deleted. + */ + function testAuthorDeletion() { + // Create a comment as the admin user. + $this->drupalLogin($this->admin_user); + $comment = $this->postComment($this->node, $this->randomName()); + $this->assertTrue($this->commentExists($comment), t('Comment is displayed initially.')); + $this->drupalLogout(); + + // Delete the admin user, and check that the node which displays the + // comment can still be viewed, but the comment itself does not appear + // there. + user_delete($this->admin_user->uid); + $this->drupalGet('node/' . $this->node->nid); + $this->assertResponse(200, t('Node page is accessible after the comment author is deleted.')); + $this->assertFalse($this->commentExists($comment), t('Comment is not displayed after the comment author is deleted.')); + } + + /** + * Test comment author deletion while the comment module is disabled. + */ + function testAuthorDeletionCommentModuleDisabled() { + // Create a comment as the admin user. + $this->drupalLogin($this->admin_user); + $comment = $this->postComment($this->node, $this->randomName()); + $this->assertTrue($this->commentExists($comment), t('Comment is displayed initially.')); + $this->drupalLogout(); + + // Delete the admin user while the comment module is disabled, and ensure + // that the missing comment author is still handled correctly (the node + // itself should be displayed, but the comment should not be displayed on + // it). + module_disable(array('comment')); + user_delete($this->admin_user->uid); + module_enable(array('comment')); + $this->drupalGet('node/' . $this->node->nid); + $this->assertResponse(200, t('Node page is accessible after the comment author is deleted.')); + $this->assertFalse($this->commentExists($comment), t('Comment is not displayed after the comment author is deleted.')); + } +} + /** * Tests uninstalling the comment module. */ diff --git a/modules/field/modules/list/list.module b/modules/field/modules/list/list.module index 47544be681..aae0458235 100644 --- a/modules/field/modules/list/list.module +++ b/modules/field/modules/list/list.module @@ -67,7 +67,7 @@ function list_field_settings_form($field, $instance, $has_data) { $form['allowed_values'] = array( '#type' => 'textarea', '#title' => t('Allowed values list'), - '#default_value' => empty($settings['allowed_values_function']) ? list_allowed_values_string($settings['allowed_values']) : array(), + '#default_value' => empty($settings['allowed_values_function']) ? list_allowed_values_string($settings['allowed_values']) : '', '#rows' => 10, '#element_validate' => array('list_allowed_values_setting_validate'), '#field_has_data' => $has_data, diff --git a/modules/field/modules/text/text.module b/modules/field/modules/text/text.module index d64eef9a68..c895d2da5a 100644 --- a/modules/field/modules/text/text.module +++ b/modules/field/modules/text/text.module @@ -318,6 +318,9 @@ function _text_sanitize($instance, $langcode, $item, $column) { if (isset($item["safe_$column"])) { return $item["safe_$column"]; } + if ($item[$column] === '') { + return ''; + } return $instance['settings']['text_processing'] ? check_markup($item[$column], $item['format'], $langcode) : check_plain($item[$column]); } diff --git a/modules/forum/forum.module b/modules/forum/forum.module index 0baddd4ee2..458ca43b6d 100644 --- a/modules/forum/forum.module +++ b/modules/forum/forum.module @@ -263,7 +263,7 @@ function _forum_node_check_node_type($node) { * Implements hook_node_view(). */ function forum_node_view($node, $view_mode) { - if (_forum_node_check_node_type($node)) { + if (!empty($node->forum_tid) && _forum_node_check_node_type($node)) { if ($view_mode == 'full' && node_is_page($node)) { $vid = variable_get('forum_nav_vocabulary', 0); $vocabulary = taxonomy_vocabulary_load($vid); diff --git a/modules/forum/forum.test b/modules/forum/forum.test index 0a5989fa44..a6f9eb182f 100644 --- a/modules/forum/forum.test +++ b/modules/forum/forum.test @@ -210,6 +210,46 @@ class ForumTestCase extends DrupalWebTestCase { $node->uid = 1; $node->taxonomy_forums[LANGUAGE_NONE][0]['tid'] = $this->root_forum['tid']; node_save($node); + + // Verify that adding taxonomy_forums reference field to another content + // type does not trigger any errors. + // Create new content type. + $type_name = 'test_' . strtolower($this->randomName()); + $type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name)); + // Create field instance on the bundle. + $vocabulary = taxonomy_vocabulary_load(variable_get('forum_nav_vocabulary', 0)); + $instance = array( + 'field_name' => 'taxonomy_forums', + 'entity_type' => 'node', + 'label' => $vocabulary->name, + 'bundle' => $type->type, + 'required' => FALSE, + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + ), + ); + field_create_instance($instance); + // Create node and access node detail page. + $settings = array( + 'type' => $type->type, + 'title' => $type_name, + 'taxonomy_forums' => array(LANGUAGE_NONE => array()), + ); + $node1 = $this->drupalCreateNode($settings); + $this->drupalGet('node/' . $node1->nid); + $this->assertResponse(200); + // Remove the field instance. + field_delete_instance($instance); } /** diff --git a/modules/image/image.admin.inc b/modules/image/image.admin.inc index 0fbdc0b747..3d0a38ca67 100644 --- a/modules/image/image.admin.inc +++ b/modules/image/image.admin.inc @@ -252,7 +252,7 @@ function image_style_add_form($form, &$form_state) { '#size' => '64', '#required' => TRUE, '#machine_name' => array( - 'exists' => 'image_style_load', + 'exists' => 'image_style_add_form_name_exists', 'source' => array('label'), 'replace_pattern' => '[^0-9a-z_\-]', 'error' => t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for style names.'), @@ -267,6 +267,19 @@ function image_style_add_form($form, &$form_state) { return $form; } +/** + * Check if the proposed machine name is already taken. + * + * @param string $name + * An image style machine name. + * + * @return bool + * TRUE if the image style machine name already exists, FALSE otherwise. + */ +function image_style_add_form_name_exists($name) { + return (bool) image_style_load($name); +} + /** * Submit handler for adding a new image style. */ diff --git a/modules/image/image.test b/modules/image/image.test index 422a34eb73..7a8c687c8c 100644 --- a/modules/image/image.test +++ b/modules/image/image.test @@ -850,6 +850,25 @@ class ImageAdminStylesUnitTest extends ImageFieldTestCase { $effects = array_values($style['effects']); $this->assertEqual($effects[0]['name'], 'image_scale', 'The default effect still exists in the reverted style.'); $this->assertFalse(array_key_exists(1, $effects), 'The added effect has been removed in the reverted style.'); + + // Verify that a new style with the same name as the default one could not + // be created. + $edit = array( + 'name' => $style_name, + 'label' => $style_label, + ); + // The default style is not overriden. + $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style')); + $this->assertNoRaw(t('Style %name was created.', array('%name' => $style_label)), 'Image style successfully created.'); + // Override the default. + $this->drupalPost($edit_path, array(), t('Override defaults')); + $this->assertRaw(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $style_label)), 'Default image style may be overridden.'); + // The default style is overriden. + $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style')); + $this->assertNoRaw(t('Style %name was created.', array('%name' => $style_label)), 'Image style successfully created.'); + // Revert the image style. + $this->drupalPost($revert_path, array(), t('Revert')); + drupal_static_reset('image_styles'); } /** diff --git a/modules/locale/locale.admin.inc b/modules/locale/locale.admin.inc index acf6eb2ebe..fb0900375d 100644 --- a/modules/locale/locale.admin.inc +++ b/modules/locale/locale.admin.inc @@ -279,7 +279,7 @@ function _locale_languages_common_controls(&$form, $language = NULL) { '#required' => TRUE, '#default_value' => @$language->language, '#disabled' => (isset($language->language)), - '#description' => t('RFC 4646 compliant language identifier. Language codes typically use a country code, and optionally, a script or regional variant name. Examples: "en", "en-US" and "zh-Hant".', array('@rfc4646' => 'http://www.ietf.org/rfc/rfc4646.txt')), + '#description' => t('Use language codes as defined by the W3C for interoperability. Examples: "en", "en-gb" and "zh-hant".', array('@w3ctags' => 'http://www.w3.org/International/articles/language-tags/')), ); } $form['name'] = array('#type' => 'textfield', diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index 966b82f6a0..6bc580c9bf 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -554,13 +554,34 @@ class CommonXssUnitTest extends DrupalUnitTestCase { */ function testBadProtocolStripping() { // Ensure that check_url() strips out harmful protocols, and encodes for - // HTML. Ensure drupal_strip_dangerous_protocols() can be used to return a - // plain-text string stripped of harmful protocols. + // HTML. $url = 'javascript:http://www.example.com/?x=1&y=2'; - $expected_plain = 'http://www.example.com/?x=1&y=2'; $expected_html = 'http://www.example.com/?x=1&y=2'; $this->assertIdentical(check_url($url), $expected_html, 'check_url() filters a URL and encodes it for HTML.'); - $this->assertIdentical(drupal_strip_dangerous_protocols($url), $expected_plain, 'drupal_strip_dangerous_protocols() filters a URL and returns plain text.'); + + // Ensure that drupal_strip_dangerous_protocols() can be used to return a + // plain-text string stripped of harmful protocols. + $data = array( + 'javascript:http://www.example.com/?x=1&y=2' => 'http://www.example.com/?x=1&y=2', + 'foo://disallowed.com' => '//disallowed.com', + 'http://example.com' => 'http://example.com', + 'https://example.com' => 'https://example.com', + 'www.example.com' => 'www.example.com', + 'mailto:person2@example.com' => 'mailto:person2@example.com', + 'person2@example.com' => 'person2@example.com', + 'ftp://example.com' => 'ftp://example.com', + 'sftp://secure.host' => 'sftp://secure.host', + 'ssh://odd.geek' => 'ssh://odd.geek', + 'news://example.net' => 'news://example.net', + 'telnet://example' => 'telnet://example', + 'irc://example.host' => 'irc://example.host', + 'webcal://calendar' => 'webcal://calendar', + 'rtsp://127.0.0.1' => 'rtsp://127.0.0.1', + 'tel:111111111' => 'tel:111111111', + ); + foreach ($data as $url => $expected_plain) { + $this->assertIdentical(drupal_strip_dangerous_protocols($url), $expected_plain, 'drupal_strip_dangerous_protocols() filters a URL and returns plain text.'); + } } } @@ -1765,6 +1786,29 @@ class JavaScriptTestCase extends DrupalWebTestCase { variable_del('javascript_always_use_jquery'); } + /** + * Tests the double submit form protection and + * 'javascript_use_double_submit_protection' variable. + */ + function testDoubleSubmitFormProtection() { + // The default front page of the site should have the double submit + // protection enabled as there is a login block. + $this->drupalGet(''); + $this->assertRaw('misc/form-single-submit.js', 'Default behavior: Double submit protection is enabled.'); + + // The default front page should have the double submit protection disabled + // when the 'javascript_always_use_jquery' variable is set to FALSE or the + // 'javascript_use_double_submit_protection' variable is set to FALSE. + variable_set('javascript_always_use_jquery', FALSE); + $this->drupalGet(''); + $this->assertNoRaw('misc/form-single-submit.js', 'When "javascript_always_use_jquery" is FALSE: Double submit protection is disabled.'); + variable_del('javascript_always_use_jquery'); + variable_set('javascript_use_double_submit_protection', FALSE); + $this->drupalGet(''); + $this->assertNoRaw('misc/form-single-submit.js', 'When "javascript_use_double_submit_protection" is FALSE: Double submit protection is disabled.'); + variable_del('javascript_use_double_submit_protection'); + } + /** * Test drupal_add_js() sets preproccess to false when cache is set to false. */ @@ -2682,6 +2726,12 @@ class DrupalDataApiTest extends DrupalWebTestCase { // Update the record. $update_result = drupal_write_record('node_access', $node_access, array('nid', 'gid', 'realm')); $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a record is updated with drupal_write_record() for a table with a multi-field primary key.'); + + // Insert a new record to the table with a database specific data type. + $person = new stdClass(); + $person->age = 25; + $insert_result = drupal_write_record('test_db_specific_datatype', $person); + $this->assertTrue($insert_result == SAVED_NEW, 'Correct value returned when a record is inserted with drupal_write_record() for a table with a database specific data type.'); } } diff --git a/modules/simpletest/tests/database_test.install b/modules/simpletest/tests/database_test.install index ad19430d8b..d7e8903c2e 100644 --- a/modules/simpletest/tests/database_test.install +++ b/modules/simpletest/tests/database_test.install @@ -276,5 +276,25 @@ function database_test_schema() { $schema['TEST_UPPERCASE'] = $schema['test']; + $schema['test_db_specific_datatype'] = array( + 'description' => 'Schema with database specific data type.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'age' => array( + 'description' => "The person's age.", + 'mysql_type' => 'tinyint', + 'pgsql_type' => 'smallint', + 'sqlite_type' => 'tinyint', + 'not null' => FALSE, + 'default' => 0, + ), + ), + 'primary key' => array('id'), + ); + return $schema; } diff --git a/modules/simpletest/tests/session.test b/modules/simpletest/tests/session.test index 796767f6d1..e21d14a141 100644 --- a/modules/simpletest/tests/session.test +++ b/modules/simpletest/tests/session.test @@ -246,6 +246,68 @@ class SessionTestCase extends DrupalWebTestCase { $this->assertResponse(403, 'An empty session ID is not allowed.'); } + /** + * Test hashing of session ids in the database. + */ + function testHashedSessionIds() { + $user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($user); + $this->drupalGet('session-test/is-logged-in'); + $this->assertResponse(200, 'User is logged in.'); + + $this->drupalGet('session-test/id'); + $matches = array(); + preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); + $this->assertTrue(!empty($matches[1]) , 'Found session ID after logging in.'); + $session_id = $matches[1]; + + $this->drupalGet('session-test/id-from-cookie'); + $matches = array(); + preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); + $this->assertTrue(!empty($matches[1]) , 'Found session ID from cookie.'); + $cookie_session_id = $matches[1]; + + $this->assertEqual($session_id, $cookie_session_id, 'Session id and cookie session id are the same.'); + + $sql = 'SELECT s.sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid'; + $db_session = db_query($sql, array(':uid' => $user->uid))->fetchObject(); + + $this->assertNotEqual($db_session->sid, $cookie_session_id, 'Session id in the database is not the same as in the session cookie.'); + $this->assertEqual($db_session->sid, drupal_hash_base64($cookie_session_id), 'Session id in the database is the cookie session id hashed.'); + } + + /** + * Test opt-out of hashing of session ids in the database. + */ + function testHashedSessionIdsOptOut() { + variable_set('do_not_hash_session_ids', TRUE); + + $user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($user); + $this->drupalGet('session-test/is-logged-in'); + $this->assertResponse(200, 'User is logged in.'); + + $this->drupalGet('session-test/id'); + $matches = array(); + preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); + $this->assertTrue(!empty($matches[1]) , 'Found session ID after logging in.'); + $session_id = $matches[1]; + + $this->drupalGet('session-test/id-from-cookie'); + $matches = array(); + preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); + $this->assertTrue(!empty($matches[1]) , 'Found session ID from cookie.'); + $cookie_session_id = $matches[1]; + + $this->assertEqual($session_id, $cookie_session_id, 'Session id and cookie session id are the same.'); + + $sql = 'SELECT s.sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid'; + $db_session = db_query($sql, array(':uid' => $user->uid))->fetchObject(); + + $this->assertEqual($db_session->sid, $cookie_session_id, 'Session id in the database is the same as in the session cookie.'); + $this->assertNotEqual($db_session->sid, drupal_hash_base64($cookie_session_id), 'Session id in the database is not the cookie session id hashed.'); + } + /** * Test absence of SameSite attribute on session cookies by default. */ @@ -740,8 +802,8 @@ class SessionHttpsTestCase extends DrupalWebTestCase { */ protected function assertSessionIds($sid, $ssid, $assertion_text) { $args = array( - ':sid' => $sid, - ':ssid' => $ssid, + ':sid' => drupal_session_id($sid), + ':ssid' => !empty($ssid) ? drupal_session_id($ssid) : '', ); return $this->assertTrue(db_query('SELECT timestamp FROM {sessions} WHERE sid = :sid AND ssid = :ssid', $args)->fetchField(), $assertion_text); } diff --git a/modules/simpletest/tests/taxonomy_test.module b/modules/simpletest/tests/taxonomy_test.module index 16b571ecdc..bb9a2977f0 100644 --- a/modules/simpletest/tests/taxonomy_test.module +++ b/modules/simpletest/tests/taxonomy_test.module @@ -140,6 +140,35 @@ function taxonomy_test_query_taxonomy_term_access_alter(QueryAlterableInterface } } +/** + * Implements hook_block_info(). + */ +function taxonomy_test_block_info() { + $blocks['test_block_form'] = array( + 'info' => t('Test block with form'), + ); + return $blocks; +} + +/** + * Implements hook_block_view(). + */ +function taxonomy_test_block_view($delta = 0) { + $form = drupal_get_form('taxonomy_test_simple_form'); + return array('subject' => 'Simple form', 'content' => drupal_render($form)); +} + +/** + * Form builder for testing submission on taxonomy terms overview page. + */ +function taxonomy_test_simple_form($form, &$form_state) { + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + return $form; +} + /** * Test controller class for taxonomy terms. * diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc index f7f7cdb6a9..664e21a733 100644 --- a/modules/system/system.admin.inc +++ b/modules/system/system.admin.inc @@ -2309,7 +2309,7 @@ function system_clean_url_settings($form, &$form_state) { $form_state['redirect'] = url('admin/config/search/clean-urls'); $form['clean_url_description'] = array( '#type' => 'markup', - '#markup' => '

' . t('Use URLs like example.com/user instead of example.com/?q=user.'), + '#markup' => '

' . t('Use URLs like example.com/user instead of example.com/?q=user.') . '

', ); // Explain why the user is seeing this page and what to expect after // clicking the 'Run the clean URL test' button. @@ -2385,7 +2385,8 @@ function system_run_cron() { * Menu callback: return information about PHP. */ function system_php() { - phpinfo(~ (INFO_VARIABLES | INFO_ENVIRONMENT)); + $phpinfo_flags = variable_get('sa_core_2023_004_phpinfo_flags', ~(INFO_VARIABLES | INFO_ENVIRONMENT)); + phpinfo($phpinfo_flags); drupal_exit(); } diff --git a/modules/system/system.install b/modules/system/system.install index 16a4877a26..2af5004f69 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -683,6 +683,9 @@ function system_install() { // Populate the cron key variable. $cron_key = drupal_random_key(); variable_set('cron_key', $cron_key); + + // This variable indicates that the database is ready for hashed session ids. + variable_set('hashed_session_ids_supported', TRUE); } /** @@ -1623,13 +1626,13 @@ function system_schema() { 'not null' => TRUE, ), 'sid' => array( - 'description' => "A session ID. The value is generated by Drupal's session handlers.", + 'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.", 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, ), 'ssid' => array( - 'description' => "Secure session ID. The value is generated by Drupal's session handlers.", + 'description' => "Secure session ID (hashed). The value is generated by Drupal's session handlers.", 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, @@ -3372,6 +3375,54 @@ function system_update_7085() { variable_del('block_interest_cohort'); } +/** + * Prepare the schema and data of the sessions table for hashed session ids. + */ +function system_update_7086() { + // Update the session ID fields' description. + $spec = array( + 'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.", + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ); + db_drop_primary_key('sessions'); + db_change_field('sessions', 'sid', 'sid', $spec, array('primary key' => array('sid', 'ssid'))); + // Updates the secure session ID field's description. + $spec = array( + 'description' => "Secure session ID (hashed). The value is generated by Drupal's session handlers.", + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ); + db_drop_primary_key('sessions'); + db_change_field('sessions', 'ssid', 'ssid', $spec, array('primary key' => array('sid', 'ssid'))); + + // Update all existing sessions. + if (!variable_get('do_not_hash_session_ids', FALSE)) { + $sessions = db_query('SELECT sid, ssid FROM {sessions}'); + while ($session = $sessions->fetchAssoc()) { + $query = db_update('sessions'); + $fields = array(); + if (!empty($session['sid'])) { + $fields['sid'] = drupal_hash_base64($session['sid']); + $query->condition('sid', $session['sid']); + } + if (!empty($session['ssid'])) { + $fields['ssid'] = drupal_hash_base64($session['ssid']); + $query->condition('ssid', $session['ssid']); + } + $query + ->fields($fields) + ->execute(); + } + } + + // This variable indicates that the database is ready for hashed session ids. + variable_set('hashed_session_ids_supported', TRUE); +} + /** * @} End of "defgroup updates-7.x-extra". * The next series of updates should start at 8000. diff --git a/modules/system/system.module b/modules/system/system.module index af71f29dd3..dc6ccfec17 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -335,7 +335,7 @@ function system_element_info() { '#button_type' => 'submit', '#executes_submit_callback' => TRUE, '#limit_validation_errors' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('ajax_process_form', 'form_process_button'), '#theme_wrappers' => array('button'), ); $types['button'] = array( @@ -344,7 +344,7 @@ function system_element_info() { '#button_type' => 'submit', '#executes_submit_callback' => FALSE, '#limit_validation_errors' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('ajax_process_form', 'form_process_button'), '#theme_wrappers' => array('button'), ); $types['image_button'] = array( @@ -352,7 +352,7 @@ function system_element_info() { '#button_type' => 'submit', '#executes_submit_callback' => TRUE, '#limit_validation_errors' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('ajax_process_form', 'form_process_button'), '#return_value' => TRUE, '#has_garbage_value' => TRUE, '#src' => NULL, @@ -1139,6 +1139,15 @@ function system_library() { ), ); + // Drupal's form single submit library. + $libraries['drupal.form-single-submit'] = array( + 'title' => 'Drupal form single submit library', + 'version' => VERSION, + 'js' => array( + 'misc/form-single-submit.js' => array('group' => JS_LIBRARY, 'weight' => 1), + ), + ); + // Drupal's states library. $libraries['drupal.states'] = array( 'title' => 'Drupal states', diff --git a/modules/system/system.tar.inc b/modules/system/system.tar.inc index 0b26f9cb76..fd012e657e 100644 --- a/modules/system/system.tar.inc +++ b/modules/system/system.tar.inc @@ -1460,6 +1460,8 @@ class Archive_Tar $v_magic = 'ustar '; $v_version = ' '; + $v_uname = ''; + $v_gname = ''; if (function_exists('posix_getpwuid')) { $userinfo = posix_getpwuid($v_info[4]); diff --git a/modules/system/system.test b/modules/system/system.test index 6655bfa388..518892d63f 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -1277,6 +1277,41 @@ class SiteMaintenanceTestCase extends DrupalWebTestCase { // Log in with temporary login link. $this->drupalPost($path, array(), t('Log in')); $this->assertText($user_message); + $this->drupalLogout(); + } + + /** + * Verify access to cron.php with custom 403 page during maintenance mode. + */ + function testCronSiteMaintenance() { + global $base_url; + + // Set custom 403 page. + $this->drupalLogin($this->admin_user); + $edit = array( + 'title' => $this->randomName(10), + 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomName(100)))), + ); + $node = $this->drupalCreateNode($edit); + + // Use a custom 403 page. + $this->drupalPost('admin/config/system/site-information', array('site_403' => 'node/' . $node->nid), t('Save configuration')); + + // Turn on maintenance mode. + $edit = array( + 'maintenance_mode' => 1, + ); + $this->drupalPost('admin/config/development/maintenance', $edit, t('Save configuration')); + $this->drupalLogout(); + + // Access cron.php without valid cron key. + $this->drupalGet($base_url . '/cron.php', array('external' => TRUE)); + $this->assertResponse(503); + + // Access cron.php with valid cron key. + $key = variable_get('cron_key', 'drupal'); + $this->drupalGet($base_url . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->assertResponse(503); } } @@ -2870,6 +2905,20 @@ class SystemAdminTestCase extends DrupalWebTestCase { } } + /** + * Test the phpinfo() page. + */ + function testAdminPhpInfo() { + $this->drupalGet('admin/reports/status/php'); + $this->assertText('PHP'); + $this->assertNoText('_COOKIE['); + + variable_set('sa_core_2023_004_phpinfo_flags', INFO_ALL); + $this->drupalGet('admin/reports/status/php'); + $this->assertText('PHP'); + $this->assertText('_COOKIE['); + } + /** * Test compact mode. */ diff --git a/modules/taxonomy/taxonomy.admin.inc b/modules/taxonomy/taxonomy.admin.inc index 1bf3d2d986..46f871f3f7 100644 --- a/modules/taxonomy/taxonomy.admin.inc +++ b/modules/taxonomy/taxonomy.admin.inc @@ -349,7 +349,7 @@ function taxonomy_overview_terms($form, &$form_state, $vocabulary) { $current_page = array_merge($order, $current_page); // Update our form with the new order. foreach ($current_page as $key => $term) { // Verify this is a term for the current page and set at the current depth. - if (is_array($form_state['input'][$key]) && is_numeric($form_state['input'][$key]['tid'])) { + if (isset($form_state['input'][$key]) && is_array($form_state['input'][$key]) && is_numeric($form_state['input'][$key]['tid'])) { $current_page[$key]->depth = $form_state['input'][$key]['depth']; } else { diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module index d94e165ad6..cde4d70621 100644 --- a/modules/taxonomy/taxonomy.module +++ b/modules/taxonomy/taxonomy.module @@ -1312,7 +1312,7 @@ class TaxonomyVocabularyController extends DrupalDefaultEntityController { * @param $tids * An array of taxonomy term IDs. * @param $conditions - * (deprecated) An associative array of conditions on the {taxonomy_term} + * (deprecated) An associative array of conditions on the {taxonomy_term_data} * table, where the keys are the database fields and the values are the * values those fields must have. Instead, it is preferable to use * EntityFieldQuery to retrieve a list of entity IDs loadable by @@ -1684,7 +1684,7 @@ function taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, } } if ($tids) { - $terms = taxonomy_term_load_multiple($tids); + $terms = taxonomy_term_load_multiple(array_filter($tids)); // Iterate through the fieldable entities again to attach the loaded term data. foreach ($entities as $id => $entity) { diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test index f4f78213f2..610c17377a 100644 --- a/modules/taxonomy/taxonomy.test +++ b/modules/taxonomy/taxonomy.test @@ -1044,6 +1044,31 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase { variable_set('taxonomy_nodes_test_query_node_access_alter', FALSE); } + /** + * Test multiple forms on taxonomy terms overview page. + */ + function testTaxonomyTermsOverviewPage() { + // Enable block with custom form on taxonomy terms overview page. + module_enable(array('taxonomy_test')); + + $this->drupalLogout(); + $admin_user1 = $this->drupalCreateUser(array('administer taxonomy', 'administer blocks', 'bypass node access')); + $this->drupalLogin($admin_user1); + + $this->createTerm($this->vocabulary); + $this->createTerm($this->vocabulary); + + $edit = array(); + $edit['blocks[taxonomy_test_test_block_form][region]'] = 'header'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + + $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name); + $this->assertText('Simple form', 'Block successfully being displayed on the page.'); + + // Try to submit the custom form to verify there are no errors. + $this->drupalPost(NULL, array(), t('Submit')); + } + } /** @@ -1673,6 +1698,13 @@ class TaxonomyTermFieldTestCase extends TaxonomyWebTestCase { $entity = field_test_entity_test_load($entity->ftid); field_test_entity_save($entity); $this->pass('Empty term ID does not trigger autocreate.'); + + // Try also NULL value. + $entity->{$this->field_name}[$langcode][0]['tid'] = NULL; + field_test_entity_save($entity); + $this->pass('NULL term ID does not trigger autocreate.'); + field_attach_prepare_view('test_entity', array($entity->ftid => $entity), 'full'); + $this->pass('NULL term ID prepared to display.'); } } diff --git a/modules/update/update.fetch.inc b/modules/update/update.fetch.inc index 428cace6b0..718da48a7f 100644 --- a/modules/update/update.fetch.inc +++ b/modules/update/update.fetch.inc @@ -155,6 +155,9 @@ function _update_process_fetch_task($project) { if (empty($fail[$fetch_url_base]) || $fail[$fetch_url_base] < $max_fetch_attempts) { $xml = drupal_http_request($url); + if (isset($xml->error)) { + watchdog('update', 'Error %errorcode (%message) occurred when trying to fetch available update data for the project %project.', array('%errorcode' => $xml->code, '%message' => $xml->error, '%project' => $project_name), WATCHDOG_ERROR); + } if (!isset($xml->error) && isset($xml->data)) { $data = $xml->data; } diff --git a/modules/update/update.module b/modules/update/update.module index cbd68514f9..fdd00680f4 100644 --- a/modules/update/update.module +++ b/modules/update/update.module @@ -14,7 +14,7 @@ /** * URL to check for updates, if a given project doesn't define its own. */ -define('UPDATE_DEFAULT_URL', 'http://updates.drupal.org/release-history'); +define('UPDATE_DEFAULT_URL', 'https://updates.drupal.org/release-history'); // These are internally used constants for this code, do not modify. diff --git a/modules/update/update.test b/modules/update/update.test index 5ce5bb88b1..eba500ed2f 100644 --- a/modules/update/update.test +++ b/modules/update/update.test @@ -231,6 +231,8 @@ class UpdateCoreTestCase extends UpdateTestHelper { // Ensure that no "Warning: SimpleXMLElement..." parse errors are found. $this->assertNoText('SimpleXMLElement'); $this->assertUniqueText(t('Failed to get available update data for one project.')); + $update_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'update'))->fetchField(); + $this->assertEqual('Error %errorcode (%message) occurred when trying to fetch available update data for the project %project.', $update_log, 'Failed update logged'); } /** diff --git a/modules/user/user.admin.inc b/modules/user/user.admin.inc index 7a6adf2f2e..be5607ead7 100644 --- a/modules/user/user.admin.inc +++ b/modules/user/user.admin.inc @@ -266,6 +266,9 @@ function user_admin_account_submit($form, &$form_state) { } } +/** + * Form validation handler for the user_admin_account() form. + */ function user_admin_account_validate($form, &$form_state) { $form_state['values']['accounts'] = array_filter($form_state['values']['accounts']); if (count($form_state['values']['accounts']) == 0) { diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 785c3b6617..8410b72090 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -837,3 +837,47 @@ * directory tree. */ # $conf['file_sa_core_2023_005_schemes'] = array('porcelain'); + +/** + * Configuration for phpinfo() admin status report. + * + * Drupal's admin UI includes a report at admin/reports/status/php which shows + * the output of phpinfo(). The full output can contain sensitive information + * so by default Drupal removes some sections. + * + * This behaviour can be configured by setting this variable to a different + * value corresponding to the flags parameter of phpinfo(). + * + * If you need to expose more information in the report - for example to debug a + * problem - consider doing so temporarily. + * + * @see https://www.php.net/manual/function.phpinfo.php + */ +# $conf['sa_core_2023_004_phpinfo_flags'] = ~(INFO_VARIABLES | INFO_ENVIRONMENT); + +/** + * Session IDs are hashed by default before being stored in the database. This + * reduces the risk of sessions being hijacked if the database is compromised. + * + * This variable allows opting out of this security improvement. + */ +# $conf['do_not_hash_session_ids'] = TRUE; + +/** + * URL for update information. + * + * Drupal's update module can check for the availability of updates. By default + * https is used for this check. If for any reason your site cannot use https + * you can change this variable to fallback to http. It is recommended to fix + * the problem with SSL/TLS rather than use http which provides no security. + */ +# $conf['update_fetch_url'] = 'https://updates.drupal.org/release-history'; + +/** + * Opt out of double submit protection. + * + * By default Drupal will prevent consecutive form submissions of identical form + * values. Set this variable to FALSE in order to opt out of this + * prevention and revert to the original behaviour. + */ +# $conf['javascript_use_double_submit_protection'] = FALSE; diff --git a/update.php b/update.php index b05d47e228..6c6df295bc 100644 --- a/update.php +++ b/update.php @@ -96,7 +96,7 @@ function update_script_selection_form($form, &$form_state) { $module_update_key = $data['module'] . '_updates'; if (isset($form['start'][$module_update_key]['#items'][$data['number']])) { $text = $data['missing_dependencies'] ? 'This update will been skipped due to the following missing dependencies: ' . implode(', ', $data['missing_dependencies']) . '' : "This update will be skipped due to an error in the module's code."; - $form['start'][$module_update_key]['#items'][$data['number']] .= '
' . $text . '
'; + $form['start'][$module_update_key]['#items'][$data['number']] .= '
' . $text . '
'; } // Move the module containing this update to the top of the list. $form['start'] = array($module_update_key => $form['start'][$module_update_key]) + $form['start'];