diff --git a/.htaccess b/.htaccess index 8500b50a8fa..7dd1d14c9fc 100644 --- a/.htaccess +++ b/.htaccess @@ -3,7 +3,7 @@ # # Protect files and directories from prying eyes. - + Require all denied diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 82bef689943..8e654d7d79f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +Drupal 7.92, 2022-09-07 +----------------------- +- Improved support for PHP 8.1 +- Various security hardenings +- Various bug fixes, optimizations and improvements + Drupal 7.91, 2022-07-20 ----------------------- - Fixed security issues: diff --git a/MAINTAINERS.txt b/MAINTAINERS.txt index cbc9f51a85a..cade09353c7 100644 --- a/MAINTAINERS.txt +++ b/MAINTAINERS.txt @@ -13,6 +13,7 @@ The branch maintainers for Drupal 7 are: - Dries Buytaert 'dries' https://www.drupal.org/u/dries - Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx - Drew Webber 'mcdruid' https://www.drupal.org/u/mcdruid +- (provisional) Juraj Nemec 'poker10' https://www.drupal.org/u/poker10 Component maintainers diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 1dd34e81c8a..8a44bfa2058 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.91'); +define('VERSION', '7.92'); /** * Core API compatibility. @@ -2373,7 +2373,7 @@ function drupal_random_bytes($count) { // the microtime() - is prepended rather than appended. This is to avoid // directly leaking $random_state via the $output stream, which could // allow for trivial prediction of further "random" numbers. - if (strlen($bytes) < $count) { + if (strlen((string) $bytes) < $count) { // Initialize on the first call. The contents of $_SERVER includes a mix of // user-specific and system information that varies a little with each page. if (!isset($random_state)) { diff --git a/includes/common.inc b/includes/common.inc index 904a77ad0e3..9659ff23c2f 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -1104,6 +1104,14 @@ function drupal_http_request($url, array $options = array()) { // Redirect to the new location. $options['max_redirects']--; + // Check if we need to remove any potentially sensitive headers before + // following the redirect. + // @see https://www.rfc-editor.org/rfc/rfc9110.html#name-redirection-3xx + if (_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location)) { + unset($options['headers']['Cookie']); + unset($options['headers']['Authorization']); + } + // We need to unset the 'Host' header // as we are redirecting to a new location. unset($options['headers']['Host']); @@ -1122,6 +1130,36 @@ function drupal_http_request($url, array $options = array()) { return $result; } +/** + * Determine whether to strip sensitive headers from a request when redirected. + * + * @param string $url + * The url from the original outbound http request. + * + * @param string $location + * The location to which the request has been redirected. + * + * @return boolean + * Whether sensitive headers should be stripped from the request before + * following the redirect. + */ +function _drupal_should_strip_sensitive_headers_on_http_redirect($url, $location) { + $url_parsed = parse_url($url); + $location_parsed = parse_url($location); + if (!isset($location_parsed['host'])) { + return FALSE; + } + $strip_on_host_change = variable_get('drupal_http_request_strip_sensitive_headers_on_host_change', TRUE); + $strip_on_https_downgrade = variable_get('drupal_http_request_strip_sensitive_headers_on_https_downgrade', TRUE); + if ($strip_on_host_change && strcasecmp($url_parsed['host'], $location_parsed['host']) !== 0) { + return TRUE; + } + if ($strip_on_https_downgrade && $url_parsed['scheme'] !== $location_parsed['scheme'] && 'https' !== $location_parsed['scheme']) { + return TRUE; + } + return FALSE; +} + /** * Splits an HTTP response status line into components. * @@ -2570,6 +2608,7 @@ function l($text, $path, array $options = array()) { $use_theme = FALSE; } } + $path = drupal_strip_dangerous_protocols((string) $path); if ($use_theme) { return theme('link', array('text' => $text, 'path' => $path, 'options' => $options)); } @@ -6105,7 +6144,7 @@ function drupal_render_page($page) { */ function drupal_render(&$elements) { // Early-return nothing if user does not have access. - if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) { + if (empty($elements) || !is_array($elements) || (isset($elements['#access']) && !$elements['#access'])) { return ''; } diff --git a/includes/database/prefetch.inc b/includes/database/prefetch.inc index 1211e4d4573..2a0bd55ce1e 100644 --- a/includes/database/prefetch.inc +++ b/includes/database/prefetch.inc @@ -286,7 +286,7 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface case PDO::FETCH_OBJ: return (object) $this->currentRow; case PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE: - $class_name = array_unshift($this->currentRow); + $class_name = array_shift($this->currentRow); // Deliberate no break. case PDO::FETCH_CLASS: if (!isset($class_name)) { diff --git a/includes/database/query.inc b/includes/database/query.inc index 94b7a8fc933..7346fbc0bd3 100644 --- a/includes/database/query.inc +++ b/includes/database/query.inc @@ -1905,7 +1905,7 @@ class DatabaseCondition implements QueryConditionInterface, Countable { public function __toString() { // If the caller forgot to call compile() first, refuse to run. if ($this->changed) { - return NULL; + return ''; } return $this->stringVersion; } diff --git a/includes/database/select.inc b/includes/database/select.inc index 399ef6e4f92..fa3745609ad 100644 --- a/includes/database/select.inc +++ b/includes/database/select.inc @@ -883,7 +883,7 @@ class SelectQuery extends Query implements SelectQueryInterface { * 'type' => $join_type (one of INNER, LEFT OUTER, RIGHT OUTER), * 'table' => $table, * 'alias' => $alias_of_the_table, - * 'condition' => $condition_clause_on_which_to_join, + * 'condition' => $join_condition (string or Condition object), * 'arguments' => $array_of_arguments_for_placeholders_in_the condition. * 'all_fields' => TRUE to SELECT $alias.*, FALSE or NULL otherwise. * ) @@ -891,6 +891,10 @@ class SelectQuery extends Query implements SelectQueryInterface { * If $table is a string, it is taken as the name of a table. If it is * a SelectQuery object, it is taken as a subquery. * + * If $join_condition is a Condition object, any arguments should be + * incorporated into the object; a separate array of arguments does not need + * to be provided. + * * @var array */ protected $tables = array(); @@ -1028,6 +1032,10 @@ class SelectQuery extends Query implements SelectQueryInterface { if ($table['table'] instanceof SelectQueryInterface) { $args += $table['table']->arguments(); } + // If the join condition is an object, grab its arguments recursively. + if (!empty($table['condition']) && $table['condition'] instanceof QueryConditionInterface) { + $args += $table['condition']->arguments(); + } } foreach ($this->expressions as $expression) { @@ -1079,6 +1087,10 @@ class SelectQuery extends Query implements SelectQueryInterface { if ($table['table'] instanceof SelectQueryInterface) { $table['table']->compile($connection, $queryPlaceholder); } + // Make sure join conditions are also compiled. + if (!empty($table['condition']) && $table['condition'] instanceof QueryConditionInterface) { + $table['condition']->compile($connection, $queryPlaceholder); + } } // If there are any dependent queries to UNION, compile it recursively. @@ -1099,6 +1111,11 @@ class SelectQuery extends Query implements SelectQueryInterface { return FALSE; } } + if (!empty($table['condition']) && $table['condition'] instanceof QueryConditionInterface) { + if (!$table['condition']->compiled()) { + return FALSE; + } + } } foreach ($this->union as $union) { @@ -1568,7 +1585,7 @@ class SelectQuery extends Query implements SelectQueryInterface { $query .= $table_string . ' ' . $this->connection->escapeAlias($table['alias']); if (!empty($table['condition'])) { - $query .= ' ON ' . $table['condition']; + $query .= ' ON ' . (string) $table['condition']; } } @@ -1589,6 +1606,14 @@ class SelectQuery extends Query implements SelectQueryInterface { $query .= "\nHAVING " . $this->having; } + // UNION is a little odd, as the select queries to combine are passed into + // this query, but syntactically they all end up on the same level. + if ($this->union) { + foreach ($this->union as $union) { + $query .= ' ' . $union['type'] . ' ' . (string) $union['query']; + } + } + // ORDER BY if ($this->order) { $query .= "\nORDER BY "; @@ -1608,14 +1633,6 @@ class SelectQuery extends Query implements SelectQueryInterface { $query .= "\nLIMIT " . (int) $this->range['length'] . " OFFSET " . (int) $this->range['start']; } - // UNION is a little odd, as the select queries to combine are passed into - // this query, but syntactically they all end up on the same level. - if ($this->union) { - foreach ($this->union as $union) { - $query .= ' ' . $union['type'] . ' ' . (string) $union['query']; - } - } - if ($this->forUpdate) { $query .= ' FOR UPDATE'; } @@ -1624,6 +1641,8 @@ class SelectQuery extends Query implements SelectQueryInterface { } public function __clone() { + parent::__clone(); + // On cloning, also clone the dependent objects. However, we do not // want to clone the database connection object as that would duplicate the // connection itself. @@ -1633,6 +1652,11 @@ class SelectQuery extends Query implements SelectQueryInterface { foreach ($this->union as $key => $aggregate) { $this->union[$key]['query'] = clone($aggregate['query']); } + foreach ($this->tables as $alias => $table) { + if ($table['table'] instanceof SelectQueryInterface) { + $this->tables[$alias]['table'] = clone $table['table']; + } + } } } diff --git a/includes/form.inc b/includes/form.inc index 6ada36e458c..fe52b0afaac 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -4327,10 +4327,14 @@ function theme_form_required_marker($variables) { * required. That is especially important for screenreader users to know * which field is required. * + * To associate the label with a different field, set the #label_for property + * to the ID of the desired field. + * * @param $variables * An associative array containing: * - element: An associative array containing the properties of the element. - * Properties used: #required, #title, #id, #value, #description. + * Properties used: #required, #title, #id, #value, #description, + * #label_for. * * @ingroup themeable */ @@ -4359,7 +4363,14 @@ function theme_form_element_label($variables) { $attributes['class'] = 'element-invisible'; } - if (!empty($element['#id'])) { + // Use the element's ID as the default value of the "for" attribute (to + // associate the label with this form element), but allow this to be + // overridden in order to associate the label with a different form element + // instead. + if (!empty($element['#label_for'])) { + $attributes['for'] = $element['#label_for']; + } + elseif (!empty($element['#id'])) { $attributes['for'] = $element['#id']; } diff --git a/includes/locale.inc b/includes/locale.inc index b0287faedd1..48dbdce9887 100644 --- a/includes/locale.inc +++ b/includes/locale.inc @@ -615,7 +615,7 @@ function locale_add_language($langcode, $name = NULL, $native = NULL, $direction 'direction' => $direction, 'domain' => $domain, 'prefix' => $prefix, - 'enabled' => $enabled, + 'enabled' => $enabled ? 1 : 0, )) ->execute(); diff --git a/includes/module.inc b/includes/module.inc index 4c2b3fbeeba..ab368b4b670 100644 --- a/includes/module.inc +++ b/includes/module.inc @@ -142,9 +142,10 @@ function system_list($type) { foreach ($bootstrap_list as $module) { drupal_get_filename('module', $module->name, $module->filename); } - // We only return the module names here since module_list() doesn't need - // the filename itself. - $lists['bootstrap'] = array_keys($bootstrap_list); + // Only return module names here since module_list() doesn't need the + // filename itself. Don't use drupal_map_assoc() as that requires common.inc. + $list = array_keys($bootstrap_list); + $lists['bootstrap'] = (!empty($list) ? array_combine($list, $list) : array()); } // Otherwise build the list for enabled modules and themes. elseif (!isset($lists['module_enabled'])) { diff --git a/includes/pager.inc b/includes/pager.inc index 316e17d5ad8..48daa15e739 100644 --- a/includes/pager.inc +++ b/includes/pager.inc @@ -164,6 +164,21 @@ class PagerDefault extends SelectQueryExtender { } return $this; } + + /** + * Gets the element ID for this pager query. + * + * The element is used to differentiate different pager queries on the same + * page so that they may be operated independently. + * + * @return + * Element ID that is used to differentiate between different pager + * queries. + */ + public function getElement() { + $this->ensureElement(); + return $this->element; + } } /** diff --git a/includes/path.inc b/includes/path.inc index 28d1146a5e2..5ed4a4ce0fc 100644 --- a/includes/path.inc +++ b/includes/path.inc @@ -417,6 +417,8 @@ function path_load($conditions) { } return $select ->fields('url_alias') + ->orderBy('pid', 'DESC') + ->range(0, 1) ->execute() ->fetchAssoc(); } diff --git a/includes/stream_wrappers.inc b/includes/stream_wrappers.inc index 0465b859e9a..e0565772943 100644 --- a/includes/stream_wrappers.inc +++ b/includes/stream_wrappers.inc @@ -405,6 +405,12 @@ abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface public function stream_open($uri, $mode, $options, &$opened_path) { $this->uri = $uri; $path = $this->getLocalPath(); + if ($path === FALSE) { + if ($options & STREAM_REPORT_ERRORS) { + trigger_error('stream_open() filename cannot be empty', E_USER_WARNING); + } + return FALSE; + } $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode); if ((bool) $this->handle && $options & STREAM_USE_PATH) { diff --git a/misc/machine-name.js b/misc/machine-name.js index 4678e0b534d..45c146b4f4f 100644 --- a/misc/machine-name.js +++ b/misc/machine-name.js @@ -27,7 +27,7 @@ Drupal.behaviors.machineName = { attach: function (context, settings) { var self = this; $.each(settings.machineName, function (source_id, options) { - var $source = $(source_id, context).addClass('machine-name-source'); + var $source = $(source_id, context).addClass('machine-name-source').once('machine-name'); var $target = $(options.target, context).addClass('machine-name-target'); var $suffix = $(options.suffix, context); var $wrapper = $target.closest('.form-item'); diff --git a/modules/field/modules/list/list.module b/modules/field/modules/list/list.module index 976f13c8b2d..47544be6819 100644 --- a/modules/field/modules/list/list.module +++ b/modules/field/modules/list/list.module @@ -388,7 +388,8 @@ function _list_values_in_use($field, $values) { * - 'list_illegal_value': The value is not part of the list of allowed values. */ function list_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { - $allowed_values = list_allowed_values($field, $instance, $entity_type, $entity); + // Flatten the array before validating to account for optgroups. + $allowed_values = options_array_flatten(list_allowed_values($field, $instance, $entity_type, $entity)); foreach ($items as $delta => $item) { if (!empty($item['value'])) { if (!empty($allowed_values) && !isset($allowed_values[$item['value']])) { diff --git a/modules/field/modules/options/options.test b/modules/field/modules/options/options.test index 321c2a4b5d1..67110960ff6 100644 --- a/modules/field/modules/options/options.test +++ b/modules/field/modules/options/options.test @@ -311,6 +311,11 @@ class OptionsWidgetsTestCase extends FieldTestCase { $edit = array("card_1[$langcode]" => '_none'); $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); $this->assertFieldValues($entity_init, 'card_1', $langcode, array()); + + // Submit form: select the option from optgroup. + $edit = array("card_1[$langcode]" => 2); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertFieldValues($entity_init, 'card_1', $langcode, array(2)); } /** diff --git a/modules/file/file.field.inc b/modules/file/file.field.inc index 0ce6c172916..706e01ceed3 100644 --- a/modules/file/file.field.inc +++ b/modules/file/file.field.inc @@ -184,7 +184,7 @@ function file_field_load($entity_type, $entities, $field, $instances, $langcode, foreach ($items[$id] as $delta => $item) { // If the file does not exist, mark the entire item as empty. if (empty($item['fid']) || !isset($files[$item['fid']])) { - $items[$id][$delta] = NULL; + unset($items[$id][$delta]); } else { $items[$id][$delta] = array_merge((array) $files[$item['fid']], $item); diff --git a/modules/file/file.module b/modules/file/file.module index 1f1d594475a..4db60669e5f 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -246,7 +246,15 @@ function file_ajax_upload() { // Invalid request. drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error'); $commands = array(); - $commands[] = ajax_command_replace(NULL, theme('status_messages')); + $commands[] = ajax_command_prepend(NULL, theme('status_messages')); + + // Unset the problematic file from the input so that user can submit the + // form without reloading the page. + // @see https://www.drupal.org/project/drupal/issues/2749245 + $field_name = (string) reset($form_parents); + $wrapper_id = drupal_html_id('edit-' . $field_name); + $commands[] = ajax_command_invoke('#' . $wrapper_id . ' .form-type-managed-file input[type="file"]', 'val', array('')); + return array('#type' => 'ajax', '#commands' => $commands); } @@ -256,7 +264,7 @@ function file_ajax_upload() { // Invalid form_build_id. drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error'); $commands = array(); - $commands[] = ajax_command_replace(NULL, theme('status_messages')); + $commands[] = ajax_command_prepend(NULL, theme('status_messages')); return array('#type' => 'ajax', '#commands' => $commands); } @@ -363,10 +371,6 @@ function file_file_delete($file) { * support for a default value. */ function file_managed_file_process($element, &$form_state, $form) { - // Append the '-upload' to the #id so the field label's 'for' attribute - // corresponds with the file element. - $original_id = $element['#id']; - $element['#id'] .= '-upload'; $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0; // Set some default element properties. @@ -376,7 +380,7 @@ function file_managed_file_process($element, &$form_state, $form) { $ajax_settings = array( 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $original_id . '-ajax-wrapper', + 'wrapper' => $element['#id'] . '-ajax-wrapper', 'effect' => 'fade', 'progress' => array( 'type' => $element['#progress_indicator'], @@ -450,13 +454,26 @@ function file_managed_file_process($element, &$form_state, $form) { $element['upload'] = array( '#name' => 'files[' . implode('_', $element['#parents']) . ']', '#type' => 'file', + // This #title will not actually be used as the upload field's HTML label, + // since the theme function for upload fields never passes the element + // through theme('form_element'). Instead the parent element's #title is + // used as the label (see below). That is usually a more meaningful label + // anyway. '#title' => t('Choose a file'), '#title_display' => 'invisible', + // Set the ID manually so the desired field label can be associated with it + // below. Use the same method for setting the ID that the form API + // autogenerator does. + '#id' => drupal_html_id('edit-' . implode('-', array_merge($element['#parents'], array('upload')))), '#size' => $element['#size'], '#theme_wrappers' => array(), '#weight' => -10, ); + // Indicate that $element['#title'] should be used as the HTML label for the + // file upload field. + $element['#label_for'] = $element['upload']['#id']; + if ($fid && $element['#file']) { $element['filename'] = array( '#type' => 'markup', @@ -482,13 +499,13 @@ function file_managed_file_process($element, &$form_state, $form) { $element['upload']['#attached']['js'] = array( array( 'type' => 'setting', - 'data' => array('file' => array('elements' => array('#' . $element['#id'] => $extension_list))) + 'data' => array('file' => array('elements' => array('#' . $element['upload']['#id'] => $extension_list))) ) ); } // Prefix and suffix used for Ajax replacement. - $element['#prefix'] = '
'; + $element['#prefix'] = '
'; $element['#suffix'] = '
'; return $element; diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test index cd2271fbba7..782d3a2fce8 100644 --- a/modules/file/tests/file.test +++ b/modules/file/tests/file.test @@ -381,6 +381,20 @@ class FileManagedFileElementTestCase extends FileFieldTestCase { ); } + public function setUp() { + parent::setUp(); + + // Disable the displaying of errors, so that the AJAX responses are not + // contaminated with error messages about exceeding the maximum POST size. + $this->originalDisplayErrorsValue = ini_set('display_errors', '0'); + } + + public function tearDown() { + ini_set('display_errors', $this->originalDisplayErrorsValue); + + parent::tearDown(); + } + /** * Tests the managed_file element type. */ @@ -389,6 +403,9 @@ class FileManagedFileElementTestCase extends FileFieldTestCase { $this->drupalGet('file/test'); $this->assertFieldByXpath('//input[@name="files[nested_file]" and @size="13"]', NULL, 'The custom #size attribute is passed to the child upload element.'); + // Check that the file fields don't contain duplicate HTML IDs. + $this->assertNoDuplicateIds('There are no duplicate IDs'); + // Perform the tests with all permutations of $form['#tree'] and // $element['#extended']. foreach (array(0, 1) as $tree) { @@ -470,6 +487,43 @@ class FileManagedFileElementTestCase extends FileFieldTestCase { } } } + + /** + * Tests uploading a file that exceeds the maximum file size. + */ + function testManagedFileExceedMaximumFileSize() { + $path = 'file/test/0/0'; + $this->drupalGet($path); + + // Create a test file that exceeds the maximum POST size with 1 kilobyte. + $post_max_size = $this->_postMaxSizeToInteger(ini_get('post_max_size')); + $filename = 'text-exceeded'; + simpletest_generate_file($filename, ceil(($post_max_size + 1024) / 1024), 1024, 'text'); + $uri = 'public://' . $filename . '.txt'; + $input_base_name = 'file'; + $edit = array('files[' . $input_base_name . ']' => drupal_realpath($uri)); + $this->drupalPostAJAX(NULL, $edit, $input_base_name . '_upload_button'); + $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After uploading a file that exceeds the maximum file size, the "Upload" button is displayed.'); + $this->drupalPost($path, array(), t('Save')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submitted without a file.'); + } + + /** + * Converts php.ini post_max_size value to integer. + * + * @param $string + * The value from php.ini. + * + * @return int + * Converted value. + */ + protected function _postMaxSizeToInteger($string) { + sscanf($string, '%u%c', $number, $suffix); + if (isset($suffix)) { + $number = $number * pow(1024, strpos(' KMG', strtoupper($suffix))); + } + return $number; + } } /** diff --git a/modules/image/image.test b/modules/image/image.test index 87f4168ae4d..f569bec0286 100644 --- a/modules/image/image.test +++ b/modules/image/image.test @@ -1191,6 +1191,36 @@ class ImageFieldDisplayTestCase extends ImageFieldTestCase { $this->drupalGet('node/' . $node->nid); $this->assertRaw($default_output, 'Default private image displayed when no user supplied image is present.'); } + + /** + * Tests the display of image field with the missing FID. + */ + function testMissingImageFieldDisplay() { + $field_name = strtolower($this->randomName()); + $type_name = 'article'; + $field_settings = array( + 'display_field' => '1', + 'display_default' => '1', + ); + $instance_settings = array( + 'description_field' => '1', + ); + $widget_settings = array(); + $this->createImageField($field_name, $type_name, $field_settings, $instance_settings, $widget_settings); + $images = $this->drupalGetTestFiles('image'); + + // Create a new node with the uploaded file. + $nid = $this->uploadNodeImage($images[1], $field_name, 'article'); + // Delete uploaded file from file_managed table. + $max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField(); + $uploaded_file = file_load($max_fid_after); + file_delete($uploaded_file, TRUE); + // Clear field cache. + field_cache_clear(); + // Check the node detail if the file is loaded. + $this->drupalGet('node/' . $nid); + $this->assertResponse(200); + } } /** diff --git a/modules/locale/locale.module b/modules/locale/locale.module index 93a4657f0c9..0d7e080d57f 100644 --- a/modules/locale/locale.module +++ b/modules/locale/locale.module @@ -555,6 +555,8 @@ function locale_language_types_info() { 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_INTERFACE), ), LANGUAGE_TYPE_URL => array( + 'name' => t('URL'), + 'description' => t('Order of language detection methods for URLs. The detected language will be used as the default when generating URLs for internal links on the site.'), 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK), ), ); diff --git a/modules/locale/locale.test b/modules/locale/locale.test index 4f6fd6c3038..1709996d804 100644 --- a/modules/locale/locale.test +++ b/modules/locale/locale.test @@ -1664,6 +1664,14 @@ class LocaleLanguageSwitchingFunctionalTest extends DrupalWebTestCase { $this->assertIdentical($links, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language list item is marked as active on the language switcher block.'); $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language anchor is marked as active on the language switcher block.'); } + + /** + * Tests that languages can be added as disabled. + */ + function testNewDisabledLanguage() { + // Add new language which will be disabled. + locale_add_language('de', 'German', 'Deutsch', LANGUAGE_LTR, '', '', FALSE, FALSE); + } } /** diff --git a/modules/menu/menu.admin.inc b/modules/menu/menu.admin.inc index 4d0a792dda0..4be6be84ed6 100644 --- a/modules/menu/menu.admin.inc +++ b/modules/menu/menu.admin.inc @@ -481,6 +481,7 @@ function menu_edit_menu($form, &$form_state, $type, $menu = array()) { '#default_value' => $menu['menu_name'], '#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI, '#description' => t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'), + '#field_prefix' => empty($menu['old_name']) ? 'menu-' : '', '#machine_name' => array( 'exists' => 'menu_edit_menu_name_exists', 'source' => array('title'), diff --git a/modules/node/node.test b/modules/node/node.test index 3ff600f386e..96c927b28fe 100644 --- a/modules/node/node.test +++ b/modules/node/node.test @@ -2554,6 +2554,26 @@ class NodeTokenReplaceTestCase extends DrupalWebTestCase { $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced.', array('%token' => $input))); } + // Test if the node without nid gets correct tokens (e.g. unsaved node). + $new_node_without_nid = clone $node; + unset($new_node_without_nid->nid); + + // Update tokens values which should be empty + $tests['[node:nid]'] = ''; + $tests['[node:url]'] = ''; + $tests['[node:edit-url]'] = ''; + + // Generate and test sanitized tokens. + foreach ($tests as $input => $expected) { + $output = token_replace($input, array('node' => $new_node_without_nid), array('language' => $language)); + $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced.', array('%token' => $input))); + } + + // Revert tokens values + $tests['[node:nid]'] = $node->nid; + $tests['[node:url]'] = url('node/' . $node->nid, $url_options); + $tests['[node:edit-url]'] = url('node/' . $node->nid . '/edit', $url_options); + // Generate and test unsanitized tokens. $tests['[node:title]'] = $node->title; $tests['[node:body]'] = $node->body[$langcode][0]['value']; diff --git a/modules/node/node.tokens.inc b/modules/node/node.tokens.inc index e63c751d6cc..63b3273359f 100644 --- a/modules/node/node.tokens.inc +++ b/modules/node/node.tokens.inc @@ -109,7 +109,7 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr switch ($name) { // Simple key values on the node. case 'nid': - $replacements[$original] = $node->nid; + $replacements[$original] = isset($node->nid) ? $node->nid : ''; break; case 'vid': @@ -168,11 +168,11 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr break; case 'url': - $replacements[$original] = url('node/' . $node->nid, $url_options); + $replacements[$original] = isset($node->nid) ? url('node/' . $node->nid, $url_options) : ''; break; case 'edit-url': - $replacements[$original] = url('node/' . $node->nid . '/edit', $url_options); + $replacements[$original] = isset($node->nid) ? url('node/' . $node->nid . '/edit', $url_options) : ''; break; // Default values for the chained tokens handled below. diff --git a/modules/simpletest/simpletest.install b/modules/simpletest/simpletest.install index 6c6f5694ded..d92f70d17fa 100644 --- a/modules/simpletest/simpletest.install +++ b/modules/simpletest/simpletest.install @@ -20,7 +20,6 @@ function simpletest_requirements($phase) { $has_curl = function_exists('curl_init'); $has_hash = function_exists('hash_hmac'); $has_domdocument = method_exists('DOMDocument', 'loadHTML'); - $open_basedir = ini_get('open_basedir'); $requirements['curl'] = array( 'title' => $t('cURL'), @@ -48,18 +47,6 @@ function simpletest_requirements($phase) { $requirements['php_domdocument']['description'] = $t('The testing framework requires the DOMDocument class to be available. Check the configure command at the PHP info page.', array('@link-phpinfo' => url('admin/reports/status/php'))); } - // SimpleTest currently needs 2 cURL options which are incompatible with - // having PHP's open_basedir restriction set. - // See http://drupal.org/node/674304. - $requirements['php_open_basedir'] = array( - 'title' => $t('PHP open_basedir restriction'), - 'value' => $open_basedir ? $t('Enabled') : $t('Disabled'), - ); - if ($open_basedir) { - $requirements['php_open_basedir']['severity'] = REQUIREMENT_ERROR; - $requirements['php_open_basedir']['description'] = $t('The testing framework requires the PHP open_basedir restriction to be disabled. Check your webserver configuration or contact your web host.', array('@open_basedir-url' => 'http://php.net/manual/en/ini.core.php#ini.open-basedir')); - } - // Check the current memory limit. If it is set too low, SimpleTest will fail // to load all tests and throw a fatal error. $memory_limit = ini_get('memory_limit'); diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index d7298c367f0..fde70b13859 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -91,6 +91,12 @@ class CommonURLUnitTest extends DrupalWebTestCase { $link = l($text, $path); $sanitized_path = check_url(url($path)); $this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered', array('@path' => $path))); + + // Verify that a dangerous protocol is sanitized. + $text = $this->randomName(); + $path = "javascript:alert('XSS')"; + $link = l($text, $path, array('external' => TRUE)); + $this->assertTrue(strpos($link, 'javascript:') === FALSE, 'Dangerous protocol javascript: was sanitized.'); } /* @@ -1191,6 +1197,10 @@ class DrupalHTTPRequestTestCase extends DrupalWebTestCase { $redirect_301 = drupal_http_request(url('system-test/redirect/301', array('absolute' => TRUE)), array('max_redirects' => 0)); $this->assertFalse(isset($redirect_301->redirect_code), 'drupal_http_request does not follow 301 redirect if max_redirects = 0.'); + $redirect_invalid = drupal_http_request(url('system-test/redirect-protocol-relative', array('absolute' => TRUE)), array('max_redirects' => 1)); + $this->assertEqual($redirect_invalid->code, -1002, format_string('301 redirect to protocol-relative URL returned with error code !error.', array('!error' => $redirect_invalid->error))); + $this->assertEqual($redirect_invalid->error, 'missing schema', format_string('301 redirect to protocol-relative URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); + $redirect_invalid = drupal_http_request(url('system-test/redirect-noscheme', array('absolute' => TRUE)), array('max_redirects' => 1)); $this->assertEqual($redirect_invalid->code, -1002, format_string('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error))); $this->assertEqual($redirect_invalid->error, 'missing schema', format_string('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); @@ -1240,6 +1250,44 @@ class DrupalHTTPRequestTestCase extends DrupalWebTestCase { } } +/** + * Unit tests for processing of http redirects. + */ +class DrupalHTTPRedirectTest extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Drupal HTTP redirect processing', + 'description' => 'Perform unit tests on processing of http redirects.', + 'group' => 'System', + ); + } + + public function testHttpsDowngrade() { + $url = 'https://example.com/foo'; + $location = 'http://example.com/bar'; + $this->assertTrue(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are stripped on HTTPS downgrade.'); + } + + public function testNoHttpsDowngrade() { + $url = 'https://example.com/foo'; + $location = 'https://example.com/bar'; + $this->assertFalse(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are not stripped without HTTPS downgrade.'); + } + + public function testHostChange() { + $url = 'https://example.com/foo'; + $location = 'https://www.example.com/bar'; + $this->assertTrue(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are stripped on change of host.'); + } + + public function testNoHostChange() { + $url = 'http://example.com/foo'; + $location = 'http://example.com/bar'; + $this->assertFalse(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are not stripped without change of host.'); + } +} + /** * Tests parsing of the HTTP response status line. */ @@ -1989,6 +2037,11 @@ class DrupalRenderTestCase extends DrupalWebTestCase { 'value' => '', 'expected' => '', ), + array( + 'name' => 'not a render array', + 'value' => 'this is not an array', + 'expected' => '', + ), array( 'name' => 'no access', 'value' => array( diff --git a/modules/simpletest/tests/database_test.install b/modules/simpletest/tests/database_test.install index 6d9ba833453..ad19430d8bb 100644 --- a/modules/simpletest/tests/database_test.install +++ b/modules/simpletest/tests/database_test.install @@ -54,6 +54,44 @@ function database_test_schema() { ), ); + $schema['test_classtype'] = array( + 'description' => 'A duplicate version of the test table, used for fetch_style PDO::FETCH_CLASSTYPE tests.', + 'fields' => array( + 'classname' => array( + 'description' => "A custom class name", + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + 'description' => "A person's name", + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'age' => array( + 'description' => "The person's age", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'job' => array( + 'description' => "The person's job", + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'primary key' => array('job'), + 'indexes' => array( + 'ages' => array('age'), + ), + ); + // This is an alternate version of the same table that is structured the same // but has a non-serial Primary Key. $schema['test_people'] = array( diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index efa6c8860f3..310178e1bbd 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -22,6 +22,7 @@ class DatabaseTestCase extends DrupalWebTestCase { parent::setUp('database_test'); $schema['test'] = drupal_get_schema('test'); + $schema['test_classtype'] = drupal_get_schema('test_classtype'); $schema['test_people'] = drupal_get_schema('test_people'); $schema['test_people_copy'] = drupal_get_schema('test_people_copy'); $schema['test_one_blob'] = drupal_get_schema('test_one_blob'); @@ -118,6 +119,15 @@ class DatabaseTestCase extends DrupalWebTestCase { )) ->execute(); + db_insert('test_classtype') + ->fields(array( + 'classname' => 'FakeRecord', + 'name' => 'Kay', + 'age' => 26, + 'job' => 'Web Developer', + )) + ->execute(); + db_insert('test_people') ->fields(array( 'name' => 'Meredith', @@ -315,6 +325,10 @@ class DatabaseSelectCloneTest extends DatabaseTestCase { $query->condition('id', $subquery, 'IN'); $clone = clone $query; + + // Cloned query should have a different unique identifier. + $this->assertNotEqual($query->uniqueIdentifier(), $clone->uniqueIdentifier()); + // Cloned query should not be altered by the following modification // happening on original query. $subquery->condition('age', 25, '>'); @@ -326,6 +340,33 @@ class DatabaseSelectCloneTest extends DatabaseTestCase { $this->assertEqual(3, $clone_result, 'The cloned query returns the expected number of rows'); $this->assertEqual(2, $query_result, 'The query returns the expected number of rows'); } + + /** + * Tests that nested SELECT queries are cloned properly. + */ + public function testNestedQueryCloning() { + $sub_query = db_select('test', 't'); + $sub_query->addField('t', 'id', 'id'); + $sub_query->condition('age', 28, '<'); + + $query = db_select($sub_query, 't'); + + $clone = clone $query; + + // Cloned query should have a different unique identifier. + $this->assertNotEqual($query->uniqueIdentifier(), $clone->uniqueIdentifier()); + + // Cloned query should not be altered by the following modification + // happening on original query. + $sub_query->condition('age', 25, '>'); + + $clone_result = $clone->countQuery()->execute()->fetchField(); + $query_result = $query->countQuery()->execute()->fetchField(); + + // Make sure the cloned query has not been modified. + $this->assertEqual(3, $clone_result, 'The cloned query returns the expected number of rows'); + $this->assertEqual(2, $query_result, 'The query returns the expected number of rows'); + } } /** @@ -407,6 +448,27 @@ class DatabaseFetchTestCase extends DatabaseTestCase { $this->assertIdentical(count($records), 1, 'There is only one record.'); } + + /** + * Confirms that we can fetch a record into a new instance of a custom class. + * The name of the class is determined from a value of the first column. + * + * @see FakeRecord + */ + function testQueryFetchClasstype() { + $records = array(); + $result = db_query('SELECT classname, name, job FROM {test_classtype} WHERE age = :age', array(':age' => 26), array('fetch' => PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE)); + foreach ($result as $record) { + $records[] = $record; + if ($this->assertTrue($record instanceof FakeRecord, 'Record is an object of class FakeRecord.')) { + $this->assertIdentical($record->name, 'Kay', 'Kay is found.'); + $this->assertIdentical($record->job, 'Web Developer', 'A 26 year old Web Developer.'); + } + $this->assertFalse(isset($record->classname), 'Classname field not found, as intended.'); + } + + $this->assertIdentical(count($records), 1, 'There is only one record.'); + } } /** @@ -1655,6 +1717,64 @@ class DatabaseSelectTestCase extends DatabaseTestCase { $this->assertEqual($names[2], 'Ringo', 'Third query returned correct name.'); } + /** + * Tests that we can UNION multiple Select queries together and set the ORDER. + */ + function testUnionOrder() { + // This gives George and Ringo. + $query_1 = db_select('test', 't') + ->fields('t', array('name')) + ->condition('age', array(27, 28), 'IN'); + + // This gives Paul. + $query_2 = db_select('test', 't') + ->fields('t', array('name')) + ->condition('age', 26); + + $query_1->union($query_2); + $query_1->orderBy('name', 'DESC'); + + $names = $query_1->execute()->fetchCol(); + + // Ensure we get all 3 records. + $this->assertEqual(count($names), 3, 'UNION returned rows from both queries.'); + + // Ensure that the names are in the correct reverse alphabetical order, + // regardless of which query they came from. + $this->assertEqual($names[0], 'Ringo', 'First query returned correct name.'); + $this->assertEqual($names[1], 'Paul', 'Second query returned correct name.'); + $this->assertEqual($names[2], 'George', 'Third query returned correct name.'); + } + + /** + * Tests that we can UNION multiple Select queries together with a LIMIT. + */ + function testUnionOrderLimit() { + // This gives George and Ringo. + $query_1 = db_select('test', 't') + ->fields('t', array('name')) + ->condition('age', array(27, 28), 'IN'); + + // This gives Paul. + $query_2 = db_select('test', 't') + ->fields('t', array('name')) + ->condition('age', 26); + + $query_1->union($query_2); + $query_1->orderBy('name', 'DESC'); + $query_1->range(0, 2); + + $names = $query_1->execute()->fetchCol(); + + // Ensure we get only 2 of the 3 records. + $this->assertEqual(count($names), 2, 'UNION with a limit returned rows from both queries.'); + + // Ensure that the names are in the correct reverse alphabetical order, + // regardless of which query they came from. + $this->assertEqual($names[0], 'Ringo', 'First query returned correct name.'); + $this->assertEqual($names[1], 'Paul', 'Second query returned correct name.'); + } + /** * Test that random ordering of queries works. * @@ -2320,6 +2440,51 @@ class DatabaseSelectComplexTestCase extends DatabaseTestCase { $this->assertNotEqual($crowded_job->name, $crowded_job->othername, 'Correctly joined same table twice.'); } + /** + * Test that join conditions can use Condition objects. + */ + function testJoinConditionObject() { + // Same test as testDefaultJoin, but with a Condition object. + $query = db_select('test_task', 't'); + $join_cond = db_and()->where('t.pid = p.id'); + $people_alias = $query->join('test', 'p', $join_cond); + $name_field = $query->addField($people_alias, 'name', 'name'); + $query->addField('t', 'task', 'task'); + $priority_field = $query->addField('t', 'priority', 'priority'); + + $query->orderBy($priority_field); + $result = $query->execute(); + + $num_records = 0; + $last_priority = 0; + foreach ($result as $record) { + $num_records++; + $this->assertTrue($record->$priority_field >= $last_priority, 'Results returned in correct order.'); + $this->assertNotEqual($record->$name_field, 'Ringo', 'Taskless person not selected.'); + $last_priority = $record->$priority_field; + } + + $this->assertEqual($num_records, 7, 'Returned the correct number of rows.'); + + // Test a condition object that creates placeholders. + $t1_name = 'John'; + $t2_name = 'George'; + $join_cond = db_and() + ->condition('t1.name', $t1_name) + ->condition('t2.name', $t2_name); + $query = db_select('test', 't1'); + $query->innerJoin('test', 't2', $join_cond); + $query->addField('t1', 'name', 't1_name'); + $query->addField('t2', 'name', 't2_name'); + + $num_records = $query->countQuery()->execute()->fetchField(); + $this->assertEqual($num_records, 1, 'Query expected to return 1 row. Actual: ' . $num_records); + if ($num_records == 1) { + $record = $query->execute()->fetchObject(); + $this->assertEqual($record->t1_name, $t1_name, 'Query expected to retrieve name ' . $t1_name . ' from table t1. Actual: ' . $record->t1_name); + $this->assertEqual($record->t2_name, $t2_name, 'Query expected to retrieve name ' . $t2_name . ' from table t2. Actual: ' . $record->t2_name); + } + } } /** @@ -2511,32 +2676,32 @@ class DatabaseSelectPagerDefaultTestCase extends DatabaseTestCase { function testElementNumbers() { $_GET['page'] = '3, 2, 1, 0'; - $name = db_select('test', 't')->extend('PagerDefault') - ->element(2) + $query = db_select('test', 't')->extend('PagerDefault'); + $query->element(2) ->fields('t', array('name')) ->orderBy('age') - ->limit(1) - ->execute() - ->fetchField(); + ->limit(1); + $this->assertEqual(2, $query->getElement()); + $name = $query->execute()->fetchField(); $this->assertEqual($name, 'Paul', 'Pager query #1 with a specified element ID returned the correct results.'); // Setting an element smaller than the previous one // should not overwrite the pager $maxElement with a smaller value. - $name = db_select('test', 't')->extend('PagerDefault') - ->element(1) + $query = db_select('test', 't')->extend('PagerDefault'); + $query->element(1) ->fields('t', array('name')) ->orderBy('age') - ->limit(1) - ->execute() - ->fetchField(); + ->limit(1); + $this->assertEqual(1, $query->getElement()); + $name = $query->execute()->fetchField(); $this->assertEqual($name, 'George', 'Pager query #2 with a specified element ID returned the correct results.'); - $name = db_select('test', 't')->extend('PagerDefault') - ->fields('t', array('name')) + $query = db_select('test', 't')->extend('PagerDefault'); + $query->fields('t', array('name')) ->orderBy('age') - ->limit(1) - ->execute() - ->fetchField(); + ->limit(1); + $this->assertEqual(3, $query->getElement()); + $name = $query->execute()->fetchField(); $this->assertEqual($name, 'John', 'Pager query #3 with a generated element ID returned the correct results.'); unset($_GET['page']); diff --git a/modules/simpletest/tests/module.test b/modules/simpletest/tests/module.test index cca971c50b3..617c8f3f387 100644 --- a/modules/simpletest/tests/module.test +++ b/modules/simpletest/tests/module.test @@ -66,6 +66,12 @@ class ModuleUnitTest extends DrupalWebTestCase { // Reset the module list. module_list(TRUE); $this->assertModuleList($module_list, t('After reset')); + + // Verify that the module_list() returns correct bootstrap modules. + $bootstrap_module_list = module_list(TRUE, TRUE); + $expected_bootstrap_modules = db_query("SELECT name, filename FROM {system} WHERE status = 1 AND bootstrap = 1 AND type = 'module' ORDER BY weight ASC, name ASC")->fetchAllAssoc('name'); + $expected_bootstrap_module_list = array_combine(array_keys($expected_bootstrap_modules), array_keys($expected_bootstrap_modules)); + $this->assertIdentical($expected_bootstrap_module_list, $bootstrap_module_list, 'module_list() returns correct bootstrap modules.'); } /** diff --git a/modules/simpletest/tests/path.test b/modules/simpletest/tests/path.test index b8b3c93c819..5473bab7c97 100644 --- a/modules/simpletest/tests/path.test +++ b/modules/simpletest/tests/path.test @@ -331,6 +331,17 @@ class PathLookupTest extends DrupalWebTestCase { ); path_save($path); $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], 'Newer alias record is returned when comparing two LANGUAGE_NONE paths with the same alias.'); + + // Test if the alias returned by drupal_lookup_path() is the same as the + // alias returned by path_load(). + $path = array( + 'source' => 'user/' . $account2->uid, + 'alias' => 'bar2', + ); + path_save($path); + $this->assertEqual(drupal_lookup_path('alias', $path['source']), 'bar2', 'Newer alias record is returned when using drupal_lookup_path() on paths with multiple aliases.'); + $loaded_path = path_load(array('source' => $path['source'])); + $this->assertEqual($loaded_path['alias'], 'bar2', 'Newer alias record is returned when using path_load() on paths with multiple aliases.'); } } diff --git a/modules/simpletest/tests/system_test.module b/modules/simpletest/tests/system_test.module index 34a3b46f943..7844ef0ee9f 100644 --- a/modules/simpletest/tests/system_test.module +++ b/modules/simpletest/tests/system_test.module @@ -45,6 +45,11 @@ function system_test_menu() { 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + $items['system-test/redirect-protocol-relative'] = array( + 'page callback' => 'system_test_redirect_protocol_relative', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); $items['system-test/redirect-noparse'] = array( 'page callback' => 'system_test_redirect_noparse', 'access arguments' => array('access content'), @@ -226,6 +231,11 @@ function system_test_redirect_noscheme() { exit; } +function system_test_redirect_protocol_relative() { + header("Location: //example.com/path", TRUE, 301); + exit; +} + function system_test_redirect_noparse() { header("Location: http:///path", TRUE, 301); exit; diff --git a/modules/simpletest/tests/taxonomy_nodes_test.info b/modules/simpletest/tests/taxonomy_nodes_test.info new file mode 100644 index 00000000000..646d50b82ba --- /dev/null +++ b/modules/simpletest/tests/taxonomy_nodes_test.info @@ -0,0 +1,6 @@ +name = "Taxonomy module node list tests" +description = "Support module for taxonomy node list related testing." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE diff --git a/modules/simpletest/tests/taxonomy_nodes_test.module b/modules/simpletest/tests/taxonomy_nodes_test.module new file mode 100644 index 00000000000..3d5035d271f --- /dev/null +++ b/modules/simpletest/tests/taxonomy_nodes_test.module @@ -0,0 +1,26 @@ +getTables() as $alias => $table) { + if ($table['table'] == 'taxonomy_index') { + $taxonomy_index = TRUE; + } + } + + if ($taxonomy_index) { + // Verify that additional data can be added to the default + // taxonomy_select_nodes() query by altering it. + $query->leftJoin('taxonomy_term_data', 'ttd', 'ttd.tid = t.tid'); + } + } +} diff --git a/modules/simpletest/tests/taxonomy_test.module b/modules/simpletest/tests/taxonomy_test.module index f4144380135..16b571ecdc4 100644 --- a/modules/simpletest/tests/taxonomy_test.module +++ b/modules/simpletest/tests/taxonomy_test.module @@ -139,3 +139,14 @@ function taxonomy_test_query_taxonomy_term_access_alter(QueryAlterableInterface variable_set(__FUNCTION__, ++$value); } } + +/** + * Test controller class for taxonomy terms. + * + * The main purpose is to make cacheGet() method available for testing. + */ +class TestTaxonomyTermController extends TaxonomyTermController { + public function loadFromCache($ids, $conditions = array()) { + return parent::cacheGet($ids, $conditions); + } +} diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc index 84e7fef182b..bf3abb67d6b 100644 --- a/modules/system/system.admin.inc +++ b/modules/system/system.admin.inc @@ -2398,6 +2398,7 @@ function system_batch_page() { if ($output === FALSE) { drupal_access_denied(); + drupal_exit(); } elseif (isset($output)) { // Force a page without blocks or messages to diff --git a/modules/system/system.test b/modules/system/system.test index 919fcf70bcc..0d6a9e76aaa 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -1117,6 +1117,16 @@ class AccessDeniedTestCase extends DrupalWebTestCase { // Check that we're still on the same page. $this->assertText(t('Site information')); + + // Check batch page response. + $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 before accessing the batch page.'); + $this->drupalGet('batch'); + $this->assertResponse(403); + $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 after accessing the batch page.'); } } @@ -3207,3 +3217,97 @@ class SystemArchiverTest extends DrupalWebTestCase $this->assertTrue($caught_exception, $message); } } + +/** + * Tests .htaccess is working correctly. + */ +class HtaccessTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => '.htaccess tests', + 'description' => 'Tests .htaccess is working correctly.', + 'group' => 'System', + ); + } + + /** + * Get an array of file paths for access testing. + */ + protected function getProtectedFiles() { + $path = drupal_get_path('module', 'system') . '/tests/fixtures/HtaccessTest'; + + // Tests the FilesMatch directive which denies access to certain file + // extensions. + $file_exts_to_deny = array( + 'engine', + 'inc', + 'info', + 'install', + 'make', + 'module', + 'module~', + 'module.bak', + 'module.orig', + 'module.save', + 'module.swo', + 'module.swp', + 'php~', + 'php.bak', + 'php.orig', + 'php.save', + 'php.swo', + 'php.swp', + 'profile', + 'po', + 'sh', + 'sql', + 'test', + 'theme', + 'tpl.php', + 'xtmpl', + ); + + foreach ($file_exts_to_deny as $file_ext) { + $file_paths["$path/access_test.$file_ext"] = 403; + } + + // Test extensions that should be permitted. + $file_exts_to_allow = array( + 'php-info.txt', + ); + + foreach ($file_exts_to_allow as $file_ext) { + $file_paths["$path/access_test.$file_ext"] = 200; + } + + // Ensure web server configuration files cannot be accessed. + $file_paths["$path/.htaccess"] = 403; + $file_paths["$path/web.config"] = 403; + + return $file_paths; + } + + /** + * Iterates over protected files and calls assertNoFileAccess(). + */ + function testFileAccess() { + foreach ($this->getProtectedFiles() as $file => $response_code) { + $this->assertFileAccess($file, $response_code); + } + } + + /** + * Asserts that a file exists and requesting it returns a specific response. + * + * @param string $path + * Path to file. Without leading slash. + * @param int $response_code + * The expected response code. For example: 200, 403 or 404. + */ + protected function assertFileAccess($path, $response_code) { + global $base_url; + $this->assertTrue(file_exists(DRUPAL_ROOT . '/' . $path), format_string('@filename exists.', array('@filename' => $path))); + $this->drupalGet($base_url . '/' . $path, array('external' => TRUE)); + $this->assertResponse($response_code, "Response code to $path should be $response_code"); + } +} diff --git a/modules/system/tests/fixtures/HtaccessTest/.htaccess b/modules/system/tests/fixtures/HtaccessTest/.htaccess new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.engine b/modules/system/tests/fixtures/HtaccessTest/access_test.engine new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.inc b/modules/system/tests/fixtures/HtaccessTest/access_test.inc new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.info b/modules/system/tests/fixtures/HtaccessTest/access_test.info new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.install b/modules/system/tests/fixtures/HtaccessTest/access_test.install new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.make b/modules/system/tests/fixtures/HtaccessTest/access_test.make new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.module b/modules/system/tests/fixtures/HtaccessTest/access_test.module new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.module.bak b/modules/system/tests/fixtures/HtaccessTest/access_test.module.bak new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.module.orig b/modules/system/tests/fixtures/HtaccessTest/access_test.module.orig new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.module.save b/modules/system/tests/fixtures/HtaccessTest/access_test.module.save new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.module.swo b/modules/system/tests/fixtures/HtaccessTest/access_test.module.swo new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.module.swp b/modules/system/tests/fixtures/HtaccessTest/access_test.module.swp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.module~ b/modules/system/tests/fixtures/HtaccessTest/access_test.module~ new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.php-info.txt b/modules/system/tests/fixtures/HtaccessTest/access_test.php-info.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.php.bak b/modules/system/tests/fixtures/HtaccessTest/access_test.php.bak new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.php.orig b/modules/system/tests/fixtures/HtaccessTest/access_test.php.orig new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.php.save b/modules/system/tests/fixtures/HtaccessTest/access_test.php.save new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.php.swo b/modules/system/tests/fixtures/HtaccessTest/access_test.php.swo new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.php.swp b/modules/system/tests/fixtures/HtaccessTest/access_test.php.swp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.php~ b/modules/system/tests/fixtures/HtaccessTest/access_test.php~ new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.po b/modules/system/tests/fixtures/HtaccessTest/access_test.po new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.profile b/modules/system/tests/fixtures/HtaccessTest/access_test.profile new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.sh b/modules/system/tests/fixtures/HtaccessTest/access_test.sh new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.test b/modules/system/tests/fixtures/HtaccessTest/access_test.test new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.theme b/modules/system/tests/fixtures/HtaccessTest/access_test.theme new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.tpl.php b/modules/system/tests/fixtures/HtaccessTest/access_test.tpl.php new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/access_test.xtmpl b/modules/system/tests/fixtures/HtaccessTest/access_test.xtmpl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/system/tests/fixtures/HtaccessTest/web.config b/modules/system/tests/fixtures/HtaccessTest/web.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module index e3ee48e0cf2..d94e165ad60 100644 --- a/modules/taxonomy/taxonomy.module +++ b/modules/taxonomy/taxonomy.module @@ -207,7 +207,7 @@ function taxonomy_select_nodes($tid, $pager = TRUE, $limit = FALSE, $order = arr } $query = db_select('taxonomy_index', 't'); $query->addTag('node_access'); - $query->condition('tid', $tid); + $query->condition('t.tid', $tid); if ($pager) { $count_query = clone $query; $count_query->addExpression('COUNT(t.nid)'); @@ -1274,7 +1274,7 @@ class TaxonomyTermController extends DrupalDefaultEntityController { // LOWER() and drupal_strtolower() may return different results. foreach ($terms as $term) { $term_values = (array) $term; - if (isset($conditions['name']) && drupal_strtolower($conditions['name'] != drupal_strtolower($term_values['name']))) { + if (isset($conditions['name']) && drupal_strtolower($conditions['name']) != drupal_strtolower($term_values['name'])) { unset($terms[$term->tid]); } } @@ -1513,7 +1513,7 @@ function taxonomy_field_validate($entity_type, $entity, $field, $instance, $lang // Build an array of existing term IDs so they can be loaded with // taxonomy_term_load_multiple(); foreach ($items as $delta => $item) { - if (!empty($item['tid']) && $item['tid'] != 'autocreate') { + if (!empty($item['tid']) && $item['tid'] !== 'autocreate') { $tids[] = $item['tid']; } } @@ -1524,7 +1524,7 @@ function taxonomy_field_validate($entity_type, $entity, $field, $instance, $lang // allowed values for this field. foreach ($items as $delta => $item) { $validate = TRUE; - if (!empty($item['tid']) && $item['tid'] != 'autocreate') { + if (!empty($item['tid']) && $item['tid'] !== 'autocreate') { $validate = FALSE; foreach ($field['settings']['allowed_values'] as $settings) { // If no parent is specified, check if the term is in the vocabulary. @@ -1600,7 +1600,7 @@ function taxonomy_field_formatter_view($entity_type, $entity, $field, $instance, switch ($display['type']) { case 'taxonomy_term_reference_link': foreach ($items as $delta => $item) { - if ($item['tid'] == 'autocreate') { + if ($item['tid'] === 'autocreate') { $element[$delta] = array( '#markup' => check_plain($item['name']), ); @@ -1620,7 +1620,7 @@ function taxonomy_field_formatter_view($entity_type, $entity, $field, $instance, case 'taxonomy_term_reference_plain': foreach ($items as $delta => $item) { - $name = ($item['tid'] != 'autocreate' ? $item['taxonomy_term']->name : $item['name']); + $name = ($item['tid'] !== 'autocreate' ? $item['taxonomy_term']->name : $item['name']); $element[$delta] = array( '#markup' => check_plain($name), ); @@ -1631,9 +1631,9 @@ function taxonomy_field_formatter_view($entity_type, $entity, $field, $instance, foreach ($items as $delta => $item) { $entity->rss_elements[] = array( 'key' => 'category', - 'value' => $item['tid'] != 'autocreate' ? $item['taxonomy_term']->name : $item['name'], + 'value' => $item['tid'] !== 'autocreate' ? $item['taxonomy_term']->name : $item['name'], 'attributes' => array( - 'domain' => $item['tid'] != 'autocreate' ? url('taxonomy/term/' . $item['tid'], array('absolute' => TRUE)) : '', + 'domain' => $item['tid'] !== 'autocreate' ? url('taxonomy/term/' . $item['tid'], array('absolute' => TRUE)) : '', ), ); } @@ -1678,7 +1678,7 @@ function taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, foreach ($entities as $id => $entity) { foreach ($items[$id] as $delta => $item) { // Force the array key to prevent duplicates. - if ($item['tid'] != 'autocreate') { + if ($item['tid'] !== 'autocreate') { $tids[$item['tid']] = $item['tid']; } } @@ -1697,7 +1697,7 @@ function taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, $items[$id][$delta]['taxonomy_term'] = $terms[$item['tid']]; } // Terms to be created are not in $terms, but are still legitimate. - elseif ($item['tid'] == 'autocreate') { + elseif ($item['tid'] === 'autocreate') { // Leave the item in place. } // Otherwise, unset the instance value, since the term does not exist. @@ -1901,7 +1901,7 @@ function taxonomy_rdf_mapping() { */ function taxonomy_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { foreach ($items as $delta => $item) { - if ($item['tid'] == 'autocreate') { + if ($item['tid'] === 'autocreate') { $term = (object) $item; unset($term->tid); taxonomy_term_save($term); @@ -2061,3 +2061,12 @@ function taxonomy_entity_query_alter($query) { unset($conditions['bundle']); } } + +/** + * Implements hook_file_download_access(). + */ +function taxonomy_file_download_access($field, $entity_type, $entity) { + if ($entity_type == 'taxonomy_term') { + return user_access('access content'); + } +} diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test index a4b7ee833e3..6188d8be5c2 100644 --- a/modules/taxonomy/taxonomy.test +++ b/modules/taxonomy/taxonomy.test @@ -1008,6 +1008,29 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase { $this->assertEqual(count($terms), 0, 'No terms loaded when restricted by a non-existing vocabulary.'); } + /** + * Tests that taxonomy term detail page is working even after the default + * taxonomy_select_nodes() query is altered. + */ + public function testTaxonomySelectNodesAlter() { + // Create a new term. + $term = $this->createTerm($this->vocabulary); + + // Create an article. + $settings = array( + 'type' => 'article', + $this->instance['field_name'] => array(LANGUAGE_NONE => array(array('tid' => $term->tid))), + ); + $this->drupalCreateNode($settings); + + // Check if the taxonomy term detail page is working. + module_enable(array('taxonomy_nodes_test')); + variable_set('taxonomy_nodes_test_query_node_access_alter', TRUE); + $this->drupalGet('taxonomy/term/' . $term->tid); + $this->assertResponse(200, 'The taxonomy term page is working.'); + variable_set('taxonomy_nodes_test_query_node_access_alter', FALSE); + } + } /** @@ -1610,6 +1633,21 @@ class TaxonomyTermFieldTestCase extends TaxonomyWebTestCase { $this->assertEqual($allowed_values[2]['vocabulary'], 'foo', 'Index 2: Machine name was left untouched.'); } + /** + * Test empty taxonomy term reference field. + */ + function testEmptyTaxonomyTermReferenceField() { + // Test if an empty value in the taxonomy reference field would trigger + // autocreate or if the value would be saved correctly. + $langcode = LANGUAGE_NONE; + $entity = field_test_create_stub_entity(NULL, NULL); + $entity->{$this->field_name}[$langcode][0]['tid'] = 0; + field_test_entity_save($entity); + $entity = field_test_entity_test_load($entity->ftid); + field_test_entity_save($entity); + $this->pass('Empty term ID does not trigger autocreate.'); + } + } /** @@ -2093,3 +2131,114 @@ class TaxonomyQueryAlterTestCase extends TaxonomyWebTestCase { } } + +/** + * Tests for taxonomy terms cache usage. + */ +class TaxonomyTermCacheUsageTestCase extends TaxonomyWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Taxonomy term cache usage', + 'description' => 'Tests for taxonomy term cache usage.', + 'group' => 'Taxonomy', + ); + } + + function setUp() { + parent::setUp('taxonomy', 'taxonomy_test'); + } + + /** + * Test taxonomy_get_term_by_name() cache usage. + */ + function testTaxonomyGetTermByNameCacheUsage() { + // Create vocabulary and term. + $new_vocabulary = $this->createVocabulary(); + $new_term = new stdClass(); + $new_term->name = 'MixedCaseTerm'; + $new_term->vid = $new_vocabulary->vid; + taxonomy_term_save($new_term); + + // Try to load term with mixed case letters from the cache. + $taxonomy_controller = new TestTaxonomyTermController('taxonomy_term'); + // First load to warm the cache. + $terms = $taxonomy_controller->load(array(), array('name' => $new_term->name)); + $this->assertTrue(isset($terms[$new_term->tid]), 'Term loaded using exact name and vocabulary machine name.'); + // Second load should load the $new_term from the cache. + $terms = $taxonomy_controller->loadFromCache(array(), array('name' => $new_term->name)); + $this->assertTrue(isset($terms[$new_term->tid]), 'Term loaded using the cache.'); + } + +} + +/** + * Tests appropriate access control to private file fields on a term. + */ +class TaxonomyPrivateFileTestCase extends TaxonomyWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Taxonomy term private file access', + 'description' => 'Verifies private files on terms have appropriate access control.', + 'group' => 'Taxonomy', + ); + } + + public function setUp() { + parent::setUp('taxonomy_test'); + + // Remove access content permission from registered users. + user_role_revoke_permissions(DRUPAL_AUTHENTICATED_RID, array('access content')); + + $this->vocabulary = $this->createVocabulary(); + // Add a field instance to the vocabulary. + $field = array( + 'field_name' => 'field_test', + 'type' => 'image', + 'settings' => array( + 'uri_scheme' => 'private' + ), + ); + field_create_field($field); + $instance = array( + 'field_name' => 'field_test', + 'entity_type' => 'taxonomy_term', + 'label' => 'test', + 'bundle' => $this->vocabulary->machine_name, + 'widget' => array( + 'type' => 'image_image', + 'settings' => array(), + ), + ); + field_create_instance($instance); + } + + /** + * Tests access to a private file on a taxonomy term entity. + */ + public function testTaxonomyImageAccess() { + $user = $this->drupalCreateUser(array('administer site configuration', 'administer taxonomy', 'access user profiles')); + $this->drupalLogin($user); + + // Create a term and upload the image. + $term = $this->createTerm($this->vocabulary); + $files = $this->drupalGetTestFiles('image'); + $image = array_pop($files); + $edit['files[field_test_' . LANGUAGE_NONE . '_0]'] = drupal_realpath($image->uri); + $this->drupalPost('taxonomy/term/' . $term->tid . '/edit', $edit, t('Save')); + $term = taxonomy_term_load($term->tid); + $this->assertText(t('Updated term @name.', array('@name' => $term->name))); + + // Create a user that should have access to the file and one that doesn't. + $access_user = $this->drupalCreateUser(array('access content')); + $no_access_user = $this->drupalCreateUser(); + $image = file_load($term->field_test[LANGUAGE_NONE][0]['fid']); + $image_url = file_create_url($image->uri); + $this->drupalLogin($access_user); + $this->drupalGet($image_url); + $this->assertResponse(200, 'Private image on term is accessible with right permission'); + + $this->drupalLogin($no_access_user); + $this->drupalGet($image_url); + $this->assertResponse(403, 'Private image on term not accessible without right permission'); + } +} diff --git a/modules/user/user.admin.inc b/modules/user/user.admin.inc index 21281b9b867..7a6adf2f2ef 100644 --- a/modules/user/user.admin.inc +++ b/modules/user/user.admin.inc @@ -857,27 +857,26 @@ function theme_user_permission_description($variables) { */ function user_admin_roles($form, $form_state) { $roles = user_roles(); + $role_weights = db_query('SELECT r.rid, r.weight FROM {role} r')->fetchAllKeyed(); $form['roles'] = array( '#tree' => TRUE, ); - $order = 0; foreach ($roles as $rid => $name) { $form['roles'][$rid]['#role'] = (object) array( 'rid' => $rid, 'name' => $name, - 'weight' => $order, + 'weight' => $role_weights[$rid], ); - $form['roles'][$rid]['#weight'] = $order; + $form['roles'][$rid]['#weight'] = $role_weights[$rid]; $form['roles'][$rid]['weight'] = array( '#type' => 'textfield', '#title' => t('Weight for @title', array('@title' => $name)), '#title_display' => 'invisible', '#size' => 4, - '#default_value' => $order, + '#default_value' => $role_weights[$rid], '#attributes' => array('class' => array('role-weight')), ); - $order++; } $form['name'] = array( diff --git a/modules/user/user.module b/modules/user/user.module index 84e7bd6e61a..9f99980c55e 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -322,7 +322,7 @@ class UserController extends DrupalDefaultEntityController { // Add the full file objects for user pictures if enabled. if (!empty($picture_fids) && variable_get('user_pictures', 0)) { - $pictures = file_load_multiple($picture_fids); + $pictures = file_load_multiple(array_filter($picture_fids)); foreach ($queried_users as $account) { if (!empty($account->picture) && isset($pictures[$account->picture])) { $account->picture = $pictures[$account->picture]; diff --git a/modules/user/user.test b/modules/user/user.test index e799183ac4f..4cfb5162e6c 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -2498,6 +2498,10 @@ class UserRoleAdminTestCase extends DrupalWebTestCase { $role = user_role_load($rid); $new_weight = $role->weight; $this->assertTrue(($old_weight + 1) == $new_weight, 'Role weight updated successfully.'); + + // Check if the updated weight is displayed on the roles settings page. + $this->drupalGet('admin/people/permissions/roles'); + $this->assertFieldByXPath("//input[@name='roles[$rid][weight]']", $new_weight, 'The role weight is displayed correctly.'); } } diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index ff0eab5ab3d..bc902339152 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -328,7 +328,11 @@ function simpletest_script_init($server_software) { if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { // Ensure that any and all environment variables are changed to https://. foreach ($_SERVER as $key => $value) { - $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]); + // The first time this script runs $_SERVER['SERVER_SOFTWARE'] will be + // NULL, so avoid errors from str_replace(). + if (!empty($_SERVER[$key])) { + $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]); + } } } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 5dbd63fbbe8..c521bc3b3a1 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -805,3 +805,19 @@ * access to all files within that scheme. */ # $conf['file_additional_public_schemes'] = array('example'); + +/** + * Sensitive request headers in drupal_http_request() when following a redirect. + * + * By default drupal_http_request() will strip sensitive request headers when + * following a redirect if the redirect location has a different http host to + * the original request, or if the scheme downgrades from https to http. + * + * These variables allow opting out of this behaviour. Careful consideration of + * the security implications of opting out is recommended. + * + * @see _drupal_should_strip_sensitive_headers_on_http_redirect() + * @see drupal_http_request() + */ +# $conf['drupal_http_request_strip_sensitive_headers_on_host_change'] = TRUE; +# $conf['drupal_http_request_strip_sensitive_headers_on_https_downgrade'] = TRUE;