diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ed734316..039ab82a 100755 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,158 @@ +Drupal 7.36, 2015-04-01 +----------------------- +- Added a 'file_public_schema' variable which allows modules that define + publicly-accessible streams in hook_stream_wrappers() to bypass file download + access checks when processing managed file upload fields. +- Fixed a bug that caused database query tags not to be added to search-related + database queries under many circumstances, and which prevented the + corresponding hook_query_TAG_alter() implementations from being called. +- Fixed the "for" attribute on managed file upload field labels to improve + accessibility (minor markup change). +- Added a 'javascript_always_use_jquery' variable which can be set to FALSE by + sites that may not need jQuery loaded on all pages, and a 'requires_jquery' + option to drupal_add_js() which modules can set to FALSE when adding + JavaScript files that have no dependency on jQuery (API addition: + https://www.drupal.org/node/2462717). +- Fixed incorrect foreign keys in the User module's role_permission and + users_roles database tables. +- Changed permission descriptions throughout Drupal core to consistently link + to relevant administrative pages, regardless of whether the user viewing the + Permissions page can view the page being linked to (minor UI change). +- Fixed the drupal_add_region_content() function so that it actually adds + content to the page. +- Added an 'image_suppress_itok_output' variable to allow sites already using + the existing 'image_allow_insecure_derivatives' variable to also prevent + security tokens from appearing in image derivative URLs. +- Fixed double-escaping of theme names in the Block module administrative + interface (minor string change). +- Added basic support for Xdebug when running automated tests. +- Fixed a bug which caused previewing a node to remove elements from the node + being edited. With this fix, calling node_preview() will no longer modify the + passed-in node object (minor API change). +- Added a user_has_role() function to check whether a user has a particular + role (API addition: https://www.drupal.org/node/2462411). +- Fixed installation failures when an opcode cache is enabled. +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused private + files to be inaccessible. +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused user + pictures to be lost. +- Fixed missing language code in hook_field_attach_view_alter() when it is + invoked from field_view_field(). +- Stopped sending ETag and Last-Modified headers for uncached page requests, + since they break caching for certain Varnish and Nginx configurations. +- Changed the Simpletest module to allow PSR-4 test classes to be used in + Drupal 7. +- Fixed a fatal error that occurred when using the Comment module's "Unpublish + comment containing keyword(s)" action. +- Changed the "lang" attribute on language links to "xml:lang" so it validates + as XHTML (minor markup change). +- Prevented the form API from allowing arrays to be submitted for various form + elements, such as textfields, textareas, and password fields (API change: + https://www.drupal.org/node/2462723). +- Fixed a bug in the Contact module which caused the global user object to have + the incorrect name and e-mail address during the remainder of the page + request after the contact form is submitted. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.35, 2015-03-18 +---------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-001. + +Drupal 7.34, 2014-11-19 +---------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-006. + +Drupal 7.33, 2014-11-07 +----------------------- +- Began storing the file modification time of each module and theme in the + {system} database table so that contributed modules can use it to identify + recently changed modules and themes (minor data structure change to the + return value of system_get_info() and other related functions). +- Added a "Did you mean?" feature to the run-tests.sh script for running + automated tests from the command line, to help developers who are attempting + to run a particular test class or group. +- Changed the date format used in various HTTP headers output by Drupal core + from RFC 1123 format to RFC 7231 format. +- Added a "block_cache_bypass_node_grants" variable to allow sites which have + node access modules enabled to use the block cache if desired (API addition). +- Made image derivative generation HTTP requests return a 404 error (rather + than a 500 error) when the source image does not exist. +- Fixed a bug which caused user pictures to be removed from the user object + after saving, and resulted in data loss if the user account was subsequently + re-saved. +- Fixed a bug in which field_has_data() did not return TRUE for fields that + only had data in older entity revisions, leading to loss of the field's data + when the field configuration was edited. +- Fixed a bug which caused the Ajax progress throbber to appear misaligned in + many situatons (minor styling change). +- Prevented the Bartik theme from lower-casing the "Permalink" link on + comments, for improved multilingual support (minor UI change). +- Added a "preferred_menu_links" tag to the database query that is used by + menu_link_get_preferred() to find the preferred menu link for a given path, + to make it easier to alter. +- Increased the maximum allowed length of block titles to 255 characters + (database schema change to the {block} table). +- Removed the Field module's field_modules_uninstalled() function, since it did + not do anything when it was invoked. +- Added a "theme_hook_original" variable to templates and theme functions and + an optional sitewide theme debug mode, to provide contextual information in + the page's HTML to theme developers. The theme debug mode is based on the one + used with Twig in Drupal 8 and can be accessed by setting the "theme_debug" + variable to TRUE (API addition). +- Added an entity_view_mode_prepare() API function to allow entity-defining + modules to properly invoke hook_entity_view_mode_alter(), and used it + throughout Drupal core to fix bugs with the invocation of that hook (API + change: https://www.drupal.org/node/2369141). +- Security improvement: Made the database API's orderBy() method sanitize the + sort direction ("ASC" or "DESC") for queries built with db_select(), so that + calling code does not have to. +- Changed the RDF module to consistently output RDF metadata for nodes and + comments near where the node is rendered in the HTML (minor markup and data + structure change). +- Added an HTML class to RDFa metatags throughout Drupal to prevent them from + accidentally affecting the site appearance (minor markup change). +- Fixed a bug in the Unicode requirements check which prevented installing + Drupal on PHP 5.6. +- Fixed a bug which caused drupal_get_bootstrap_phase() to abort the bootstrap + when called early in the page request. +- Renamed the "Search result" view mode to "Search result highlighting input" + to better reflect how it is used (UI change). +- Improved database queries generated by EntityFieldQuery in the case where + delta or language condition groups are used, to reduce the number of INNER + JOINs (this is a minor data structure change affecting code which implements + hook_query_alter() on these queries). +- Removed special-case behavior for file uploads which allowed user #1 to + bypass maximum file size and user quota limits. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.32, 2014-10-15 +---------------------- +- Fixed security issues (SQL injection). See SA-CORE-2014-005. + +Drupal 7.31, 2014-08-06 +---------------------- +- Fixed security issues (denial of service). See SA-CORE-2014-004. + +Drupal 7.30, 2014-07-24 +----------------------- +- Fixed a regression introduced in Drupal 7.29 that caused files or images + attached to taxonomy terms to be deleted when the taxonomy term was edited + and resaved (and other related bugs with contributed and custom modules). +- Added a warning on the permissions page to recommend restricting access to + the "View site reports" permission to trusted administrators. See + DRUPAL-PSA-2014-002. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.29, 2014-07-16 +---------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-003. + Drupal 7.28, 2014-05-08 ----------------------- - Fixed a regression introduced in Drupal 7.27 that caused JavaScript to break diff --git a/MAINTAINERS.txt b/MAINTAINERS.txt index 61715668..f5cf6f89 100755 --- a/MAINTAINERS.txt +++ b/MAINTAINERS.txt @@ -27,7 +27,6 @@ Ajax system - Earl Miles 'merlinofchaos' http://drupal.org/user/26979 Base system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Damien Tournoud 'DamZ' http://drupal.org/user/22211 - Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 @@ -39,7 +38,6 @@ Cache system - Nathaniel Catchpole 'catch' http://drupal.org/user/35733 Cron system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Derek Wright 'dww' http://drupal.org/user/46549 Database system @@ -55,10 +53,8 @@ Database system - Sqlite driver - Damien Tournoud 'DamZ' http://drupal.org/user/22211 - - Károly Négyesi 'chx' http://drupal.org/user/9446 Database update system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Ashok Modi 'BTMash' http://drupal.org/user/60422 Entity system @@ -71,7 +67,6 @@ File system - Aaron Winborn 'aaron' http://drupal.org/user/33420 Form system -- Károly Négyesi 'chx' http://drupal.org/user/9446 - Alex Bronstein 'effulgentsia' http://drupal.org/user/78040 - Wolfgang Ziegler 'fago' http://drupal.org/user/16747 - Daniel F. Kudwien 'sun' http://drupal.org/user/54136 @@ -105,7 +100,6 @@ Markup Menu system - Peter Wolanin 'pwolanin' http://drupal.org/user/49851 -- Károly Négyesi 'chx' http://drupal.org/user/9446 Path system - Dave Reid 'davereid' http://drupal.org/user/53892 @@ -139,9 +133,6 @@ Accessibility Documentation - Jennifer Hodgdon 'jhodgdon' http://drupal.org/user/155601 -Security -- Greg Knaddison 'greggles' http://drupal.org/user/36762 - Translations - Gerhard Killesreiter 'killes' http://drupal.org/user/83 @@ -154,6 +145,20 @@ Node Access - Ken Rickard 'agentrickard' http://drupal.org/user/20975 - Jess Myrbo 'xjm' http://drupal.org/user/65776 + +Security team +----------------- + +To report a security issue, see: https://drupal.org/security-team/report-issue + +The Drupal security team provides Security Advisories for vulnerabilities, +assists developers in resolving security issues, and provides security +documentation. See http://drupal.org/security-team for more information. The +security team lead is: + +- Michael Hess 'mlhess' https://drupal.org/user/102818 + + Module maintainers ------------------ @@ -250,7 +255,6 @@ Shortcut module Simpletest module - Jimmy Berry 'boombatower' http://drupal.org/user/214218 -- Károly Négyesi 'chx' http://drupal.org/user/9446 Statistics module - Tim Millwood 'timmillwood' http://drupal.org/user/227849 diff --git a/includes/ajax.inc b/includes/ajax.inc index 8446bf89..6e8e277b 100755 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -211,7 +211,7 @@ * * When returning an Ajax command array, it is often useful to have * status messages rendered along with other tasks in the command array. - * In that case the the Ajax commands array may be constructed like this: + * In that case the Ajax commands array may be constructed like this: * @code * $commands = array(); * $commands[] = ajax_command_replace(NULL, $output); @@ -276,7 +276,7 @@ function ajax_render($commands = array()) { $extra_commands = array(); if (!empty($styles)) { - $extra_commands[] = ajax_command_prepend('head', $styles); + $extra_commands[] = ajax_command_add_css($styles); } if (!empty($scripts_header)) { $extra_commands[] = ajax_command_prepend('head', $scripts_header); @@ -292,7 +292,7 @@ function ajax_render($commands = array()) { $scripts = drupal_add_js(); if (!empty($scripts['settings'])) { $settings = $scripts['settings']; - array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $settings['data']), TRUE)); + array_unshift($commands, ajax_command_settings(drupal_array_merge_deep_array($settings['data']), TRUE)); } // Allow modules to alter any Ajax response. @@ -1257,3 +1257,26 @@ function ajax_command_update_build_id($form) { 'new' => $form['#build_id'], ); } + +/** + * Creates a Drupal Ajax 'add_css' command. + * + * This method will add css via ajax in a cross-browser compatible way. + * + * This command is implemented by Drupal.ajax.prototype.commands.add_css() + * defined in misc/ajax.js. + * + * @param $styles + * A string that contains the styles to be added. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see misc/ajax.js + */ +function ajax_command_add_css($styles) { + return array( + 'command' => 'add_css', + 'data' => $styles, + ); +} diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 09c2044b..b9d49de9 100755 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.28'); +define('VERSION', '7.36'); /** * Core API compatibility. @@ -248,6 +248,15 @@ define('REGISTRY_WRITE_LOOKUP_CACHE', 2); */ define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'); +/** + * A RFC7231 Compliant date. + * + * http://tools.ietf.org/html/rfc7231#section-7.1.1.1 + * + * Example: Sun, 06 Nov 1994 08:49:37 GMT + */ +define('DATE_RFC7231', 'D, d M Y H:i:s \G\M\T'); + /** * Provides a caching wrapper to be used in place of large array structures. * @@ -520,9 +529,8 @@ function timer_stop($name) { * Returns the appropriate configuration directory. * * Returns the configuration path based on the site's hostname, port, and - * pathname. Uses find_conf_path() to find the current configuration directory. - * See default.settings.php for examples on how the URL is converted to a - * directory. + * pathname. See default.settings.php for examples on how the URL is converted + * to a directory. * * @param bool $require_settings * Only configuration directories with an existing settings.php file @@ -700,7 +708,14 @@ function drupal_environment_initialize() { * TRUE if only containing valid characters, or FALSE otherwise. */ function drupal_valid_http_host($host) { - return preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host); + // Limit the length of the host name to 1000 bytes to prevent DoS attacks with + // long host names. + return strlen($host) <= 1000 + // Limit the number of subdomains and port separators to prevent DoS attacks + // in conf_path(). + && substr_count($host, '.') <= 100 + && substr_count($host, ':') <= 100 + && preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host); } /** @@ -845,7 +860,7 @@ function drupal_get_filename($type, $name, $filename = NULL) { try { if (function_exists('db_query')) { $file = db_query("SELECT filename FROM {system} WHERE name = :name AND type = :type", array(':name' => $name, ':type' => $type))->fetchField(); - if (file_exists(DRUPAL_ROOT . '/' . $file)) { + if ($file !== FALSE && file_exists(DRUPAL_ROOT . '/' . $file)) { $files[$type][$name] = $file; } } @@ -1230,23 +1245,10 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE) * fresh page on every request. This prevents authenticated users from seeing * locally cached pages. * - * Also give each page a unique ETag. This will force clients to include both - * an If-Modified-Since header and an If-None-Match header when doing - * conditional requests for the page (required by RFC 2616, section 13.3.4), - * making the validation more robust. This is a workaround for a bug in Mozilla - * Firefox that is triggered when Drupal's caching is enabled and the user - * accesses Drupal via an HTTP proxy (see - * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated - * user requests a page, and then logs out and requests the same page again, - * Firefox may send a conditional request based on the page that was cached - * locally when the user was logged in. If this page did not have an ETag - * header, the request only contains an If-Modified-Since header. The date will - * be recent, because with authenticated users the Last-Modified header always - * refers to the time of the request. If the user accesses Drupal via a proxy - * server, and the proxy already has a cached copy of the anonymous page with an - * older Last-Modified date, the proxy may respond with 304 Not Modified, making - * the client think that the anonymous and authenticated pageviews are - * identical. + * ETag and Last-Modified headers are not set per default for authenticated + * users so that browsers do not send If-Modified-Since headers from + * authenticated user pages. drupal_serve_page_from_cache() will set appropriate + * ETag and Last-Modified headers for cached pages. * * @see drupal_page_set_cache() */ @@ -1259,9 +1261,7 @@ function drupal_page_header() { $default_headers = array( 'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT', - 'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME), 'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0', - 'ETag' => '"' . REQUEST_TIME . '"', ); drupal_send_headers($default_headers); } @@ -1329,7 +1329,7 @@ function drupal_serve_page_from_cache(stdClass $cache) { drupal_add_http_header($name, $value); } - $default_headers['Last-Modified'] = gmdate(DATE_RFC1123, $cache->created); + $default_headers['Last-Modified'] = gmdate(DATE_RFC7231, $cache->created); // HTTP/1.0 proxies does not support the Vary header, so prevent any caching // by sending an Expires date in the past. HTTP/1.1 clients ignores the @@ -1552,12 +1552,13 @@ function format_string($string, array $args = array()) { * Also validates strings as UTF-8 to prevent cross site scripting attacks on * Internet Explorer 6. * - * @param $text + * @param string $text * The text to be checked or processed. * - * @return - * An HTML safe version of $text, or an empty string if $text is not - * valid UTF-8. + * @return string + * An HTML safe version of $text. If $text is not valid UTF-8, an empty string + * is returned and, on PHP < 5.4, a warning may be issued depending on server + * configuration (see @link https://bugs.php.net/bug.php?id=47494 @endlink). * * @see drupal_validate_utf8() * @ingroup sanitization @@ -1642,14 +1643,14 @@ function request_uri() { * information about the passed-in exception is used. * @param $variables * Array of variables to replace in the message on display. Defaults to the - * return value of drupal_decode_exception(). + * return value of _drupal_decode_exception(). * @param $severity * The severity of the message, as per RFC 3164. * @param $link * A link to associate with the message. * * @see watchdog() - * @see drupal_decode_exception() + * @see _drupal_decode_exception() */ function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) { @@ -2169,7 +2170,7 @@ function drupal_anonymous_user() { * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); * @endcode * - * @param $phase + * @param int $phase * A constant telling which phase to bootstrap to. When you bootstrap to a * particular phase, all earlier phases are run automatically. Possible * values: @@ -2182,11 +2183,11 @@ function drupal_anonymous_user() { * - DRUPAL_BOOTSTRAP_LANGUAGE: Finds out the language of the page. * - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input * data. - * @param $new_phase + * @param boolean $new_phase * A boolean, set to FALSE if calling drupal_bootstrap from inside a * function called from drupal_bootstrap (recursion). * - * @return + * @return int * The most recently completed phase. */ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { @@ -2208,12 +2209,13 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { // bootstrap state. static $stored_phase = -1; - // When not recursing, store the phase name so it's not forgotten while - // recursing. - if ($new_phase) { - $final_phase = $phase; - } if (isset($phase)) { + // When not recursing, store the phase name so it's not forgotten while + // recursing but take care of not going backwards. + if ($new_phase && $phase >= $stored_phase) { + $final_phase = $phase; + } + // Call a phase if it has not been called before and is below the requested // phase. while ($phases && $phase > $stored_phase && $final_phase > $stored_phase) { @@ -2479,6 +2481,26 @@ function _drupal_bootstrap_variables() { // Load bootstrap modules. require_once DRUPAL_ROOT . '/includes/module.inc'; module_load_all(TRUE); + + // Sanitize the destination parameter (which is often used for redirects) to + // prevent open redirect attacks leading to other domains. Sanitize both + // $_GET['destination'] and $_REQUEST['destination'] to protect code that + // relies on either, but do not sanitize $_POST to avoid interfering with + // unrelated form submissions. The sanitization happens here because + // url_is_external() requires the variable system to be available. + if (isset($_GET['destination']) || isset($_REQUEST['destination'])) { + require_once DRUPAL_ROOT . '/includes/common.inc'; + // If the destination is an external URL, remove it. + if (isset($_GET['destination']) && url_is_external($_GET['destination'])) { + unset($_GET['destination']); + unset($_REQUEST['destination']); + } + // If there's still something in $_REQUEST['destination'] that didn't come + // from $_GET, check it too. + if (isset($_REQUEST['destination']) && (!isset($_GET['destination']) || $_REQUEST['destination'] != $_GET['destination']) && url_is_external($_REQUEST['destination'])) { + unset($_REQUEST['destination']); + } + } } /** @@ -2501,7 +2523,7 @@ function _drupal_bootstrap_page_header() { * @see drupal_bootstrap() */ function drupal_get_bootstrap_phase() { - return drupal_bootstrap(); + return drupal_bootstrap(NULL, FALSE); } /** @@ -2615,7 +2637,7 @@ function drupal_installation_attempted() { * * This would include implementations of hook_install(), which could run * during the Drupal installation phase, and might also be run during - * non-installation time, such as while installing the module from the the + * non-installation time, such as while installing the module from the * module administration page. * * Example usage: @@ -3144,10 +3166,13 @@ function _registry_check_code($type, $name = NULL) { // This function may get called when the default database is not active, but // there is no reason we'd ever want to not use the default database for // this query. - $file = Database::getConnection('default', 'default')->query("SELECT filename FROM {registry} WHERE name = :name AND type = :type", array( - ':name' => $name, - ':type' => $type, - )) + $file = Database::getConnection('default', 'default') + ->select('registry', 'r', array('target' => 'default')) + ->fields('r', array('filename')) + // Use LIKE here to make the query case-insensitive. + ->condition('r.name', db_like($name), 'LIKE') + ->condition('r.type', $type) + ->execute() ->fetchField(); // Flag that we've run a lookup query and need to update the cache. @@ -3321,11 +3346,9 @@ function registry_update() { * @param $default_value * Optional default value. * @param $reset - * TRUE to reset a specific named variable, or all variables if $name is NULL. - * Resetting every variable should only be used, for example, for running - * unit tests with a clean environment. Should be used only though via - * function drupal_static_reset() and the return value should not be used in - * this case. + * TRUE to reset one or all variables(s). This parameter is only used + * internally and should not be passed in; use drupal_static_reset() instead. + * (This function's return value should not be used when TRUE is passed in.) * * @return * Returns a variable by reference. @@ -3370,6 +3393,8 @@ function &drupal_static($name, $default_value = NULL, $reset = FALSE) { * * @param $name * Name of the static variable to reset. Omit to reset all variables. + * Resetting all variables should only be used, for example, for running unit + * tests with a clean environment. */ function drupal_static_reset($name = NULL) { drupal_static($name, NULL, TRUE); @@ -3485,3 +3510,34 @@ function drupal_check_memory_limit($required, $memory_limit = NULL) { // - The memory limit is greater than the memory required for the operation. return ((!$memory_limit) || ($memory_limit == -1) || (parse_size($memory_limit) >= parse_size($required))); } + +/** + * Invalidates a PHP file from any active opcode caches. + * + * If the opcode cache does not support the invalidation of individual files, + * the entire cache will be flushed. + * + * @param string $filepath + * The absolute path of the PHP file to invalidate. + */ +function drupal_clear_opcode_cache($filepath) { + if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50300) { + // Below PHP 5.3, clearstatcache does not accept any function parameters. + clearstatcache(); + } + else { + clearstatcache(TRUE, $filepath); + } + + // Zend OPcache. + if (function_exists('opcache_invalidate')) { + opcache_invalidate($filepath, TRUE); + } + // APC. + if (function_exists('apc_delete_file')) { + // apc_delete_file() throws a PHP warning in case the specified file was + // not compiled yet. + // @see http://php.net/apc-delete-file + @apc_delete_file($filepath); + } +} diff --git a/includes/cache.inc b/includes/cache.inc index 09f4d753..207bf661 100755 --- a/includes/cache.inc +++ b/includes/cache.inc @@ -98,9 +98,11 @@ function cache_get_multiple(array &$cids, $bin = 'cache') { * @param $data * The data to store in the cache. Complex data types will be automatically * serialized before insertion. Strings will be stored as plain text and are - * not serialized. + * not serialized. Some storage engines only allow objects up to a maximum of + * 1MB in size to be stored by default. When caching large arrays or similar, + * take care to ensure $data does not exceed this size. * @param $bin - * The cache bin to store the data in. Valid core values are: + * (optional) The cache bin to store the data in. Valid core values are: * - cache: (default) Generic cache storage bin (used for theme registry, * locale date, list of simpletest tests, etc.). * - cache_block: Stores the content of various blocks. @@ -119,7 +121,7 @@ function cache_get_multiple(array &$cids, $bin = 'cache') { * the administrator panel. * - cache_path: Stores the system paths that have an alias. * @param $expire - * One of the following values: + * (optional) One of the following values: * - CACHE_PERMANENT: Indicates that the item should never be removed unless * explicitly told to using cache_clear_all() with a cache ID. * - CACHE_TEMPORARY: Indicates that the item should be removed at the next @@ -254,10 +256,12 @@ interface DrupalCacheInterface { * The cache ID of the data to store. * @param $data * The data to store in the cache. Complex data types will be automatically - * serialized before insertion. - * Strings will be stored as plain text and not serialized. + * serialized before insertion. Strings will be stored as plain text and not + * serialized. Some storage engines only allow objects up to a maximum of + * 1MB in size to be stored by default. When caching large arrays or + * similar, take care to ensure $data does not exceed this size. * @param $expire - * One of the following values: + * (optional) One of the following values: * - CACHE_PERMANENT: Indicates that the item should never be removed unless * explicitly told to using cache_clear_all() with a cache ID. * - CACHE_TEMPORARY: Indicates that the item should be removed at the next diff --git a/includes/common.inc b/includes/common.inc index e1a1673d..0437ec1a 100755 --- a/includes/common.inc +++ b/includes/common.inc @@ -544,37 +544,32 @@ function drupal_get_destination() { } /** - * Parses a system URL string into an associative array suitable for url(). + * Parses a URL string into its path, query, and fragment components. * - * This function should only be used for URLs that have been generated by the - * system, such as via url(). It should not be used for URLs that come from - * external sources, or URLs that link to external resources. + * This function splits both internal paths like @code node?b=c#d @endcode and + * external URLs like @code https://example.com/a?b=c#d @endcode into their + * component parts. See + * @link http://tools.ietf.org/html/rfc3986#section-3 RFC 3986 @endlink for an + * explanation of what the component parts are. * - * The returned array contains a 'path' that may be passed separately to url(). - * For example: - * @code - * $options = drupal_parse_url($_GET['destination']); - * $my_url = url($options['path'], $options); - * $my_link = l('Example link', $options['path'], $options); - * @endcode + * Note that, unlike the RFC, when passed an external URL, this function + * groups the scheme, authority, and path together into the path component. * - * This is required, because url() does not support relative URLs containing a - * query string or fragment in its $path argument. Instead, any query string - * needs to be parsed into an associative query parameter array in - * $options['query'] and the fragment into $options['fragment']. + * @param string $url + * The internal path or external URL string to parse. * - * @param $url - * The URL string to parse, f.e. $_GET['destination']. + * @return array + * An associative array containing: + * - path: The path component of $url. If $url is an external URL, this + * includes the scheme, authority, and path. + * - query: An array of query parameters from $url, if they exist. + * - fragment: The fragment component from $url, if it exists. * - * @return - * An associative array containing the keys: - * - 'path': The path of the URL. If the given $url is external, this includes - * the scheme and host. - * - 'query': An array of query parameters of $url, if existent. - * - 'fragment': The fragment of $url, if existent. - * - * @see url() * @see drupal_goto() + * @see l() + * @see url() + * @see http://tools.ietf.org/html/rfc3986 + * * @ingroup php_wrappers */ function drupal_parse_url($url) { @@ -990,9 +985,10 @@ function drupal_http_request($url, array $options = array()) { $response = preg_split("/\r\n|\n|\r/", $response); // Parse the response status line. - list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3); - $result->protocol = $protocol; - $result->status_message = $status_message; + $response_status_array = _drupal_parse_response_status(trim(array_shift($response))); + $result->protocol = $response_status_array['http_version']; + $result->status_message = $response_status_array['reason_phrase']; + $code = $response_status_array['response_code']; $result->headers = array(); @@ -1083,12 +1079,43 @@ function drupal_http_request($url, array $options = array()) { } break; default: - $result->error = $status_message; + $result->error = $result->status_message; } return $result; } +/** + * Splits an HTTP response status line into components. + * + * See the @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html status line definition @endlink + * in RFC 2616. + * + * @param string $respone + * The response status line, for example 'HTTP/1.1 500 Internal Server Error'. + * + * @return array + * Keyed array containing the component parts. If the response is malformed, + * all possible parts will be extracted. 'reason_phrase' could be empty. + * Possible keys: + * - 'http_version' + * - 'response_code' + * - 'reason_phrase' + */ +function _drupal_parse_response_status($response) { + $response_array = explode(' ', trim($response), 3); + // Set up empty values. + $result = array( + 'reason_phrase' => '', + ); + $result['http_version'] = $response_array[0]; + $result['response_code'] = $response_array[1]; + if (isset($response_array[2])) { + $result['reason_phrase'] = $response_array[2]; + } + return $result; +} + /** * Helper function for determining hosts excluded from needing a proxy. * @@ -2187,14 +2214,20 @@ function url($path = NULL, array $options = array()) { 'prefix' => '' ); + // A duplicate of the code from url_is_external() to avoid needing another + // function call, since performance inside url() is critical. if (!isset($options['external'])) { - // Return an external link if $path contains an allowed absolute URL. Only - // call the slow drupal_strip_dangerous_protocols() if $path contains a ':' - // before any / ? or #. Note: we could use url_is_external($path) here, but - // that would require another function call, and performance inside url() is - // critical. + // Return an external link if $path contains an allowed absolute URL. Avoid + // calling drupal_strip_dangerous_protocols() if there is any slash (/), + // hash (#) or question_mark (?) before the colon (:) occurrence - if any - + // as this would clearly mean it is not a URL. If the path starts with 2 + // slashes then it is always considered an external URL without an explicit + // protocol part. $colonpos = strpos($path, ':'); - $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path); + $options['external'] = (strpos($path, '//') === 0) + || ($colonpos !== FALSE + && !preg_match('![/?#]!', substr($path, 0, $colonpos)) + && drupal_strip_dangerous_protocols($path) == $path); } // Preserve the original path before altering or aliasing. @@ -2232,6 +2265,11 @@ function url($path = NULL, array $options = array()) { return $path . $options['fragment']; } + // Strip leading slashes from internal paths to prevent them becoming external + // URLs without protocol. /example.com should not be turned into + // //example.com. + $path = ltrim($path, '/'); + global $base_url, $base_secure_url, $base_insecure_url; // The base_url might be rewritten from the language rewrite in domain mode. @@ -2309,10 +2347,15 @@ function url($path = NULL, array $options = array()) { */ function url_is_external($path) { $colonpos = strpos($path, ':'); - // Avoid calling drupal_strip_dangerous_protocols() if there is any - // slash (/), hash (#) or question_mark (?) before the colon (:) - // occurrence - if any - as this would clearly mean it is not a URL. - return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path; + // Avoid calling drupal_strip_dangerous_protocols() if there is any slash (/), + // hash (#) or question_mark (?) before the colon (:) occurrence - if any - as + // this would clearly mean it is not a URL. If the path starts with 2 slashes + // then it is always considered an external URL without an explicit protocol + // part. + return (strpos($path, '//') === 0) + || ($colonpos !== FALSE + && !preg_match('![/?#]!', substr($path, 0, $colonpos)) + && drupal_strip_dangerous_protocols($path) == $path); } /** @@ -2609,7 +2652,10 @@ function drupal_deliver_html_page($page_callback_result) { // Keep old path for reference, and to allow forms to redirect to it. if (!isset($_GET['destination'])) { - $_GET['destination'] = $_GET['q']; + // Make sure that the current path is not interpreted as external URL. + if (!url_is_external($_GET['q'])) { + $_GET['destination'] = $_GET['q']; + } } $path = drupal_get_normal_path(variable_get('site_404', '')); @@ -2638,7 +2684,10 @@ function drupal_deliver_html_page($page_callback_result) { // Keep old path for reference, and to allow forms to redirect to it. if (!isset($_GET['destination'])) { - $_GET['destination'] = $_GET['q']; + // Make sure that the current path is not interpreted as external URL. + if (!url_is_external($_GET['q'])) { + $_GET['destination'] = $_GET['q']; + } } $path = drupal_get_normal_path(variable_get('site_403', '')); @@ -3447,7 +3496,11 @@ function drupal_pre_render_styles($elements) { $import_batch = array_slice($import, 0, 31); $import = array_slice($import, 31); $element = $style_element_defaults; - $element['#value'] = implode("\n", $import_batch); + // This simplifies the JavaScript regex, allowing each line + // (separated by \n) to be treated as a completely different string. + // This means that we can use ^ and $ on one line at a time, and not + // worry about style tags since they'll never match the regex. + $element['#value'] = "\n" . implode("\n", $import_batch) . "\n"; $element['#attributes']['media'] = $group['media']; $element['#browsers'] = $group['browsers']; $elements[] = $element; @@ -3773,7 +3826,7 @@ function _drupal_load_stylesheet($matches) { // Alter all internal url() paths. Leave external paths alone. We don't need // to normalize absolute paths here (i.e. remove folder/... segments) because // that will be done later. - return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)/i', 'url(\1'. $directory, $file); + return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)([^\'")]+)([\'"]?)\s*\)/i', 'url(\1' . $directory . '\2\3)', $file); } /** @@ -4109,6 +4162,13 @@ function drupal_region_class($region) { * else being the same, JavaScript added by a call to drupal_add_js() that * happened later in the page request gets added to the page after one for * which drupal_add_js() happened earlier in the page request. + * - requires_jquery: Set this to FALSE if the JavaScript you are adding does + * not have a dependency on jQuery. Defaults to TRUE, except for JavaScript + * settings where it defaults to FALSE. This is used on sites that have the + * 'javascript_always_use_jquery' variable set to FALSE; on those sites, if + * all the JavaScript added to the page by drupal_add_js() does not have a + * dependency on jQuery, then for improved front-end performance Drupal + * will not add jQuery and related libraries and settings to the page. * - defer: If set to TRUE, the defer attribute is set on the '; + filter_format_save($format); + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array(filter_permission_name($format) => 1)); + $this->drupalGet('filter/tips'); + $this->assertNoRaw($format->name, 'Text format name contains no xss.'); } /** diff --git a/modules/forum/forum.info b/modules/forum/forum.info index e28d10e1..b7d518c3 100755 --- a/modules/forum/forum.info +++ b/modules/forum/forum.info @@ -9,8 +9,8 @@ files[] = forum.test configure = admin/structure/forum stylesheets[all][] = forum.css -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/forum/forum.module b/modules/forum/forum.module index 575de36a..1224418a 100755 --- a/modules/forum/forum.module +++ b/modules/forum/forum.module @@ -263,10 +263,10 @@ function _forum_node_check_node_type($node) { * Implements hook_node_view(). */ function forum_node_view($node, $view_mode) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); if (_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); // Breadcrumb navigation $breadcrumb[] = l(t('Home'), NULL); $breadcrumb[] = l($vocabulary->name, 'forum'); diff --git a/modules/help/help.info b/modules/help/help.info index f882a440..8e4b2392 100755 --- a/modules/help/help.info +++ b/modules/help/help.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x files[] = help.test -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/image/image.info b/modules/image/image.info index a1adc322..1e17ac14 100755 --- a/modules/image/image.info +++ b/modules/image/image.info @@ -7,8 +7,8 @@ dependencies[] = file files[] = image.test configure = admin/config/media/image-styles -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/image/image.module b/modules/image/image.module index c6a23f25..fac8de95 100755 --- a/modules/image/image.module +++ b/modules/image/image.module @@ -845,6 +845,12 @@ function image_style_deliver($style, $scheme) { } } + // Confirm that the original source image exists before trying to process it. + if (!is_file($image_uri)) { + watchdog('image', 'Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri)); + return MENU_NOT_FOUND; + } + // Don't start generating the image if the derivative already exists or if // generation is in progress in another thread. $lock_name = 'image_style_deliver:' . $style['name'] . ':' . drupal_hash_base64($image_uri); @@ -854,6 +860,7 @@ function image_style_deliver($style, $scheme) { // Tell client to retry again in 3 seconds. Currently no browsers are known // to support Retry-After. drupal_add_http_header('Status', '503 Service Unavailable'); + drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); drupal_add_http_header('Retry-After', 3); print t('Image generation in progress. Try again shortly.'); drupal_exit(); @@ -875,6 +882,7 @@ function image_style_deliver($style, $scheme) { else { watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri)); drupal_add_http_header('Status', '500 Internal Server Error'); + drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); print t('Error generating image.'); drupal_exit(); } @@ -1019,7 +1027,15 @@ function image_style_url($style_name, $path) { // The token query is added even if the 'image_allow_insecure_derivatives' // variable is TRUE, so that the emitted links remain valid if it is changed // back to the default FALSE. - $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, $original_uri)); + // However, sites which need to prevent the token query from being emitted at + // all can additionally set the 'image_suppress_itok_output' variable to TRUE + // to achieve that (if both are set, the security token will neither be + // emitted in the image derivative URL nor checked for in + // image_style_deliver()). + $token_query = array(); + if (!variable_get('image_suppress_itok_output', FALSE)) { + $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, $original_uri)); + } // If not using clean URLs, the image derivative callback is only available // with the query string. If the file does not exist, use url() to ensure @@ -1031,8 +1047,12 @@ function image_style_url($style_name, $path) { } $file_url = file_create_url($uri); - // Append the query string with the token. - return $file_url . (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); + // Append the query string with the token, if necessary. + if ($token_query) { + $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); + } + + return $file_url; } /** diff --git a/modules/image/image.test b/modules/image/image.test index 4a4aab05..35919794 100755 --- a/modules/image/image.test +++ b/modules/image/image.test @@ -173,6 +173,16 @@ class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase { $this->_testImageStyleUrlAndPath('public', TRUE, TRUE); } + /** + * Test that an invalid source image returns a 404. + */ + function testImageStyleUrlForMissingSourceImage() { + $non_existent_uri = 'public://foo.png'; + $generated_url = image_style_url($this->style_name, $non_existent_uri); + $this->drupalGet($generated_url); + $this->assertResponse(404, 'Accessing an image style URL with a source image that does not exist provides a 404 error response.'); + } + /** * Test image_style_url(). */ @@ -320,6 +330,15 @@ class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase { $this->drupalGet($nested_url); $this->assertResponse(200, 'Image was accessible when a correct token was provided in the URL.'); + // Suppress the security token in the URL, then get the URL of a file. Check + // that the security token is not present in the URL but that the image is + // still accessible. + variable_set('image_suppress_itok_output', TRUE); + $generate_url = image_style_url($this->style_name, $original_uri); + $this->assertIdentical(strpos($generate_url, IMAGE_DERIVATIVE_TOKEN . '='), FALSE, 'The security token does not appear in the image style URL.'); + $this->drupalGet($generate_url); + $this->assertResponse(200, 'Image was accessible at the URL with a missing token.'); + // Check that requesting a nonexistent image does not create any new // directories in the file system. $directory = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/' . $this->randomName(); diff --git a/modules/image/tests/image_module_test.info b/modules/image/tests/image_module_test.info index 21dfb92c..d382a738 100755 --- a/modules/image/tests/image_module_test.info +++ b/modules/image/tests/image_module_test.info @@ -6,8 +6,8 @@ core = 7.x files[] = image_module_test.module hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/locale/locale.admin.inc b/modules/locale/locale.admin.inc index b736f79b..e813962d 100755 --- a/modules/locale/locale.admin.inc +++ b/modules/locale/locale.admin.inc @@ -1139,11 +1139,11 @@ function locale_translate_edit_form($form, &$form_state, $lid) { '#value' => $source->location ); - // Include default form controls with empty values for all languages. - // This ensures that the languages are always in the same order in forms. + // Include both translated and not yet translated target languages in the + // list. The source language is English for built-in strings and the default + // language for other strings. $languages = language_list(); $default = language_default(); - // We don't need the default language value, that value is in $source. $omit = $source->textgroup == 'default' ? 'en' : $default->language; unset($languages[($omit)]); $form['translations'] = array('#tree' => TRUE); diff --git a/modules/locale/locale.info b/modules/locale/locale.info index b9868f3d..7431c7b3 100755 --- a/modules/locale/locale.info +++ b/modules/locale/locale.info @@ -6,8 +6,8 @@ core = 7.x files[] = locale.test configure = admin/config/regional/language -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/locale/locale.test b/modules/locale/locale.test index 9ffec9f1..90865872 100755 --- a/modules/locale/locale.test +++ b/modules/locale/locale.test @@ -1202,7 +1202,7 @@ EOF; * Helper function that returns a .po file with context. */ function getPoFileWithContext() { - // Croatian (code hr) is one the the languages that have a different + // Croatian (code hr) is one of the languages that have a different // form for the full name and the abbreviated name for the month May. return <<< EOF msgid "" diff --git a/modules/locale/tests/locale_test.info b/modules/locale/tests/locale_test.info index 868b6831..5391d65f 100755 --- a/modules/locale/tests/locale_test.info +++ b/modules/locale/tests/locale_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/menu/menu.info b/modules/menu/menu.info index f941c232..4c8f89fc 100755 --- a/modules/menu/menu.info +++ b/modules/menu/menu.info @@ -6,8 +6,8 @@ core = 7.x files[] = menu.test configure = admin/structure/menu -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/menu/menu.module b/modules/menu/menu.module index 64447912..dc8f015d 100755 --- a/modules/menu/menu.module +++ b/modules/menu/menu.module @@ -69,7 +69,7 @@ function menu_menu() { 'title' => 'Parent menu items', 'page callback' => 'menu_parent_options_js', 'type' => MENU_CALLBACK, - 'access arguments' => array(TRUE), + 'access arguments' => array('administer menu'), ); $items['admin/structure/menu/list'] = array( 'title' => 'List menus', diff --git a/modules/menu/menu.test b/modules/menu/menu.test index 95e0ee9e..a9bdb5f2 100755 --- a/modules/menu/menu.test +++ b/modules/menu/menu.test @@ -513,6 +513,23 @@ class MenuTestCase extends DrupalWebTestCase { } } + /** + * Test administrative users other than user 1 can access the menu parents AJAX callback. + */ + public function testMenuParentsJsAccess() { + $admin = $this->drupalCreateUser(array('administer menu')); + $this->drupalLogin($admin); + // Just check access to the callback overall, the POST data is irrelevant. + $this->drupalGetAJAX('admin/structure/menu/parents'); + $this->assertResponse(200); + + // Do standard user tests. + // Login the user. + $this->drupalLogin($this->std_user); + $this->drupalGetAJAX('admin/structure/menu/parents'); + $this->assertResponse(403); + } + /** * Get standard menu link. */ diff --git a/modules/node/node.api.php b/modules/node/node.api.php index 95026761..9a4d0959 100755 --- a/modules/node/node.api.php +++ b/modules/node/node.api.php @@ -17,11 +17,14 @@ * During node operations (create, update, view, delete, etc.), there are * several sets of hooks that get invoked to allow modules to modify the base * node operation: - * - Node-type-specific hooks: These hooks are only invoked on the primary - * module, using the "base" return component of hook_node_info() as the - * function prefix. For example, poll.module defines the base for the Poll - * content type as "poll", so during creation of a poll node, hook_insert() is - * only invoked by calling poll_insert(). + * - Node-type-specific hooks: When defining a node type, hook_node_info() + * returns a 'base' component. Node-type-specific hooks are named + * base_hookname() instead of mymodule_hookname() (in a module called + * 'mymodule' for example). Only the node type's corresponding implementation + * is invoked. For example, poll_node_info() in poll.module defines the base + * for the 'poll' node type as 'poll'. So when a poll node is created, + * hook_insert() is invoked on poll_insert() only. + * Hooks that are node-type-specific are noted below. * - All-module hooks: This set of hooks is invoked on all implementing modules, * to allow other modules to modify what the primary node module is doing. For * example, hook_node_insert() is invoked on all modules when creating a poll @@ -195,7 +198,7 @@ function hook_node_grants($account, $op) { if (user_access('access private content', $account)) { $grants['example'] = array(1); } - $grants['example_owner'] = array($account->uid); + $grants['example_author'] = array($account->uid); return $grants; } @@ -885,11 +888,10 @@ function hook_node_view_alter(&$build) { * name as the key. Each sub-array has up to 10 attributes. Possible * attributes: * - name: (required) The human-readable name of the node type. - * - base: (required) The base string used to construct callbacks - * corresponding to this node type (for example, if base is defined as - * example_foo, then example_foo_insert will be called when inserting a node - * of that type). This string is usually the name of the module, but not - * always. + * - base: (required) The base name for implementations of node-type-specific + * hooks that respond to this node type. Base is usually the name of the + * module or 'node_content', but not always. See + * @link node_api_hooks Node API hooks @endlink for more information. * - description: (required) A brief description of the node type. * - help: (optional) Help information shown to the user when creating a node * of this type. @@ -1030,8 +1032,11 @@ function hook_node_type_delete($info) { /** * Respond to node deletion. * - * This hook is invoked only on the module that defines the node's content type - * (use hook_node_delete() to respond to all node deletions). + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_node_delete() to respond to node deletion of all node types. * * This hook is invoked from node_delete_multiple() before hook_node_delete() * is invoked and before field_attach_delete() is called. @@ -1059,8 +1064,11 @@ function hook_delete($node) { /** * Act on a node object about to be shown on the add/edit form. * - * This hook is invoked only on the module that defines the node's content type - * (use hook_node_prepare() to act on all node preparations). + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_node_prepare() to respond to node preparation of all node types. * * This hook is invoked from node_object_prepare() before the general * hook_node_prepare() is invoked. @@ -1089,6 +1097,13 @@ function hook_prepare($node) { /** * Display a node editing form. * + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_form_BASE_FORM_ID_alter(), with base form ID 'node_form', to alter + * node forms for all node types. + * * This hook, implemented by node modules, is called to retrieve the form * that is displayed to create or edit a node. This form is displayed at path * node/add/[node type] or node/[node ID]/edit. @@ -1144,8 +1159,11 @@ function hook_form($node, &$form_state) { /** * Respond to creation of a new node. * - * This hook is invoked only on the module that defines the node's content type - * (use hook_node_insert() to act on all node insertions). + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_node_insert() to respond to node insertion of all node types. * * This hook is invoked from node_save() after the node is inserted into the * node table in the database, before field_attach_insert() is called, and @@ -1168,8 +1186,11 @@ function hook_insert($node) { /** * Act on nodes being loaded from the database. * - * This hook is invoked only on the module that defines the node's content type - * (use hook_node_load() to respond to all node loads). + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_node_load() to respond to node load of all node types. * * This hook is invoked during node loading, which is handled by entity_load(), * via classes NodeController and DrupalDefaultEntityController. After the node @@ -1202,8 +1223,11 @@ function hook_load($nodes) { /** * Respond to updates to a node. * - * This hook is invoked only on the module that defines the node's content type - * (use hook_node_update() to act on all node updates). + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_node_update() to respond to node update of all node types. * * This hook is invoked from node_save() after the node is updated in the * node table in the database, before field_attach_update() is called, and @@ -1224,8 +1248,11 @@ function hook_update($node) { /** * Perform node validation before a node is created or updated. * - * This hook is invoked only on the module that defines the node's content type - * (use hook_node_validate() to act on all node validations). + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_node_validate() to respond to node validation of all node types. * * This hook is invoked from node_validate(), after a user has finished * editing the node and is previewing or submitting it. It is invoked at the end @@ -1258,8 +1285,11 @@ function hook_validate($node, $form, &$form_state) { /** * Display a node. * - * This hook is invoked only on the module that defines the node's content type - * (use hook_node_view() to act on all node views). + * This is a node-type-specific hook, which is invoked only for the node type + * being affected. See + * @link node_api_hooks Node API hooks @endlink for more information. + * + * Use hook_node_view() to respond to node view of all node types. * * This hook is invoked during node viewing after the node is fully loaded, so * that the node type module can define a custom method for display, or add to @@ -1269,6 +1299,10 @@ function hook_validate($node, $form, &$form_state) { * The node to be displayed, as returned by node_load(). * @param $view_mode * View mode, e.g. 'full', 'teaser', ... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * * @return * The passed $node parameter should be modified as necessary and returned so * it can be properly presented. Nodes are prepared for display by assembling @@ -1282,7 +1316,7 @@ function hook_validate($node, $form, &$form_state) { * * @ingroup node_api_hooks */ -function hook_view($node, $view_mode) { +function hook_view($node, $view_mode, $langcode = NULL) { if ($view_mode == 'full' && node_is_page($node)) { $breadcrumb = array(); $breadcrumb[] = l(t('Home'), NULL); diff --git a/modules/node/node.info b/modules/node/node.info index f0acbc5f..c91b28ac 100755 --- a/modules/node/node.info +++ b/modules/node/node.info @@ -9,8 +9,8 @@ required = TRUE configure = admin/structure/types stylesheets[all][] = node.css -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/node/node.module b/modules/node/node.module index 5a4e0194..fd848e24 100755 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -210,7 +210,7 @@ function node_entity_info() { 'custom settings' => FALSE, ), 'search_result' => array( - 'label' => t('Search result'), + 'label' => t('Search result highlighting input'), 'custom settings' => FALSE, ), ); @@ -506,7 +506,8 @@ function node_type_load($name) { * - custom: TRUE or FALSE indicating whether this type is defined by a module * (FALSE) or by a user (TRUE) via Add Content Type. * - modified: TRUE or FALSE indicating whether this type has been modified by - * an administrator. Currently not used in any way. + * an administrator. When modifying an existing node type, set to TRUE, or + * the change will be ignored on node_types_rebuild(). * - locked: TRUE or FALSE indicating whether the administrator can change the * machine name of this type. * - disabled: TRUE or FALSE indicating whether this type has been disabled. @@ -739,11 +740,9 @@ function _node_types_build($rebuild = FALSE) { $type_db = $type_object->type; // Original disabled value. $disabled = $type_object->disabled; - // Check for node types from disabled modules and mark their types for removal. - // Types defined by the node module in the database (rather than by a separate - // module using hook_node_info) have a base value of 'node_content'. The isset() - // check prevents errors on old (pre-Drupal 7) databases. - if (isset($type_object->base) && $type_object->base != 'node_content' && empty($_node_types->types[$type_db])) { + // Check for node types either from disabled modules or otherwise not defined + // and mark as disabled. + if (empty($type_object->custom) && empty($_node_types->types[$type_db])) { $type_object->disabled = TRUE; } if (isset($_node_types->types[$type_db])) { @@ -1397,12 +1396,7 @@ function node_build_content($node, $view_mode = 'full', $langcode = NULL) { $node->content = array(); // Allow modules to change the view mode. - $context = array( - 'entity_type' => 'node', - 'entity' => $node, - 'langcode' => $langcode, - ); - drupal_alter('entity_view_mode', $view_mode, $context); + $view_mode = key(entity_view_mode_prepare('node', array($node->nid => $node), $view_mode, $langcode)); // The 'view' hook can be implemented to overwrite the default function // to display nodes. @@ -1585,9 +1579,7 @@ function node_permission() { ), 'access content overview' => array( 'title' => t('Access the content overview page'), - 'description' => user_access('access content overview') - ? t('Get an overview of all content.', array('@url' => url('admin/content'))) - : t('Get an overview of all content.'), + 'description' => t('Get an overview of all content.', array('@url' => url('admin/content'))), ), 'access content' => array( 'title' => t('View published content'), @@ -1615,7 +1607,7 @@ function node_permission() { } /** - * Gathers the rankings from the the hook_ranking() implementations. + * Gathers the rankings from the hook_ranking() implementations. * * @param $query * A query object that has been extended with the Search DB Extender. @@ -2604,9 +2596,10 @@ function node_feed($nids = FALSE, $channel = array()) { $node->link = url("node/$node->nid", array('absolute' => TRUE)); $node->rss_namespaces = array(); + $account = user_load($node->uid); $node->rss_elements = array( array('key' => 'pubDate', 'value' => gmdate('r', $node->created)), - array('key' => 'dc:creator', 'value' => $node->name), + array('key' => 'dc:creator', 'value' => format_username($account)), array('key' => 'guid', 'value' => $node->nid . ' at ' . $base_url, 'attributes' => array('isPermaLink' => 'false')) ); @@ -2664,15 +2657,26 @@ function node_feed($nids = FALSE, $channel = array()) { * An array in the format expected by drupal_render(). */ function node_view_multiple($nodes, $view_mode = 'teaser', $weight = 0, $langcode = NULL) { - field_attach_prepare_view('node', $nodes, $view_mode, $langcode); - entity_prepare_view('node', $nodes, $langcode); $build = array(); + $entities_by_view_mode = entity_view_mode_prepare('node', $nodes, $view_mode, $langcode); + foreach ($entities_by_view_mode as $entity_view_mode => $entities) { + field_attach_prepare_view('node', $entities, $entity_view_mode, $langcode); + entity_prepare_view('node', $entities, $langcode); + + foreach ($entities as $entity) { + $build['nodes'][$entity->nid] = node_view($entity, $entity_view_mode, $langcode); + } + } + foreach ($nodes as $node) { - $build['nodes'][$node->nid] = node_view($node, $view_mode, $langcode); $build['nodes'][$node->nid]['#weight'] = $weight; $weight++; } + // Sort here, to preserve the input order of the entities that were passed to + // this function. + uasort($build['nodes'], 'element_sort'); $build['nodes']['#sorted'] = TRUE; + return $build; } @@ -3629,7 +3633,8 @@ function node_access_rebuild($batch_mode = FALSE) { // Try to allocate enough time to rebuild node grants drupal_set_time_limit(240); - $nids = db_query("SELECT nid FROM {node}")->fetchCol(); + // Rebuild newest nodes first so that recent content becomes available quickly. + $nids = db_query("SELECT nid FROM {node} ORDER BY nid DESC")->fetchCol(); foreach ($nids as $nid) { $node = node_load($nid, NULL, TRUE); // To preserve database integrity, only acquire grants if the node diff --git a/modules/node/node.pages.inc b/modules/node/node.pages.inc index 62674636..cc3908e3 100755 --- a/modules/node/node.pages.inc +++ b/modules/node/node.pages.inc @@ -371,35 +371,38 @@ function node_form_build_preview($form, &$form_state) { * @see node_form_build_preview() */ function node_preview($node) { - if (node_access('create', $node) || node_access('update', $node)) { - _field_invoke_multiple('load', 'node', array($node->nid => $node)); + // Clone the node before previewing it to prevent the node itself from being + // modified. + $cloned_node = clone $node; + if (node_access('create', $cloned_node) || node_access('update', $cloned_node)) { + _field_invoke_multiple('load', 'node', array($cloned_node->nid => $cloned_node)); // Load the user's name when needed. - if (isset($node->name)) { + if (isset($cloned_node->name)) { // The use of isset() is mandatory in the context of user IDs, because // user ID 0 denotes the anonymous user. - if ($user = user_load_by_name($node->name)) { - $node->uid = $user->uid; - $node->picture = $user->picture; + if ($user = user_load_by_name($cloned_node->name)) { + $cloned_node->uid = $user->uid; + $cloned_node->picture = $user->picture; } else { - $node->uid = 0; // anonymous user + $cloned_node->uid = 0; // anonymous user } } - elseif ($node->uid) { - $user = user_load($node->uid); - $node->name = $user->name; - $node->picture = $user->picture; + elseif ($cloned_node->uid) { + $user = user_load($cloned_node->uid); + $cloned_node->name = $user->name; + $cloned_node->picture = $user->picture; } - $node->changed = REQUEST_TIME; - $nodes = array($node->nid => $node); + $cloned_node->changed = REQUEST_TIME; + $nodes = array($cloned_node->nid => $cloned_node); field_attach_prepare_view('node', $nodes, 'full'); // Display a preview of the node. if (!form_get_errors()) { - $node->in_preview = TRUE; - $output = theme('node_preview', array('node' => $node)); - unset($node->in_preview); + $cloned_node->in_preview = TRUE; + $output = theme('node_preview', array('node' => $cloned_node)); + unset($cloned_node->in_preview); } drupal_set_title(t('Preview'), PASS_THROUGH); diff --git a/modules/node/node.test b/modules/node/node.test index 0777e113..5c9118eb 100755 --- a/modules/node/node.test +++ b/modules/node/node.test @@ -2782,8 +2782,8 @@ class NodeEntityViewModeAlterTest extends NodeWebTestCase { $edit = array(); $langcode = LANGUAGE_NONE; $edit["title"] = $this->randomName(8); - $edit["body[$langcode][0][value]"] = t('Data that should appear only in the body for the node.'); - $edit["body[$langcode][0][summary]"] = t('Extra data that should appear only in the teaser for the node.'); + $edit["body[$langcode][0][value]"] = 'Data that should appear only in the body for the node.'; + $edit["body[$langcode][0][summary]"] = 'Extra data that should appear only in the teaser for the node.'; $this->drupalPost('node/add/page', $edit, t('Save')); $node = $this->drupalGetNodeByTitle($edit["title"]); @@ -2801,6 +2801,45 @@ class NodeEntityViewModeAlterTest extends NodeWebTestCase { $build = node_view($node); $this->assertEqual($build['#view_mode'], 'teaser', 'The view mode has correctly been set to teaser.'); } + + /** + * Tests fields that were previously hidden when the view mode is changed. + */ + function testNodeViewModeChangeHiddenField() { + // Hide the tags field on the default display + $instance = field_info_instance('node', 'field_tags', 'article'); + $instance['display']['default']['type'] = 'hidden'; + field_update_instance($instance); + + $web_user = $this->drupalCreateUser(array('create article content', 'edit own article content')); + $this->drupalLogin($web_user); + + // Create a node. + $edit = array(); + $langcode = LANGUAGE_NONE; + $edit["title"] = $this->randomName(8); + $edit["body[$langcode][0][value]"] = 'Data that should appear only in the body for the node.'; + $edit["body[$langcode][0][summary]"] = 'Extra data that should appear only in the teaser for the node.'; + $edit["field_tags[$langcode]"] = 'Extra tag'; + $this->drupalPost('node/add/article', $edit, t('Save')); + + $node = $this->drupalGetNodeByTitle($edit["title"]); + + // Set the flag to alter the view mode and view the node. + variable_set('node_test_change_view_mode', 'teaser'); + $this->drupalGet('node/' . $node->nid); + + // Check that teaser mode is viewed. + $this->assertText('Extra data that should appear only in the teaser for the node.', 'Teaser text present'); + // Make sure body text is not present. + $this->assertNoText('Data that should appear only in the body for the node.', 'Body text not present'); + // Make sure tags are present. + $this->assertText('Extra tag', 'Taxonomy term present'); + + // Test that the correct build mode has been set. + $build = node_view($node); + $this->assertEqual($build['#view_mode'], 'teaser', 'The view mode has correctly been set to teaser.'); + } } /** diff --git a/modules/node/tests/node_access_test.info b/modules/node/tests/node_access_test.info index a2216141..69362c94 100755 --- a/modules/node/tests/node_access_test.info +++ b/modules/node/tests/node_access_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/node/tests/node_access_test.module b/modules/node/tests/node_access_test.module index ec35c41f..7932c552 100755 --- a/modules/node/tests/node_access_test.module +++ b/modules/node/tests/node_access_test.module @@ -211,7 +211,7 @@ function node_access_test_node_insert($node) { } /** - * Implements hook_nodeapi_update(). + * Implements hook_node_update(). */ function node_access_test_node_update($node) { _node_access_test_node_write($node); diff --git a/modules/node/tests/node_test.info b/modules/node/tests/node_test.info index 5b28222d..aa658eaa 100755 --- a/modules/node/tests/node_test.info +++ b/modules/node/tests/node_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/node/tests/node_test_exception.info b/modules/node/tests/node_test_exception.info index 92eba8fc..42894814 100755 --- a/modules/node/tests/node_test_exception.info +++ b/modules/node/tests/node_test_exception.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc index d7ef663b..a1da1d0b 100755 --- a/modules/openid/openid.inc +++ b/modules/openid/openid.inc @@ -158,6 +158,11 @@ function _openid_xrds_parse($raw_xml) { return array(); } + // Also stop parsing if there is an unreasonably large number of tags. + if ($dom->getElementsByTagName('*')->length > variable_get('openid_xrds_maximum_tag_count', 30000)) { + return array(); + } + // Parse the DOM document for the information we need. if ($xml = simplexml_import_dom($dom)) { foreach ($xml->children(OPENID_NS_XRD)->XRD as $xrd) { diff --git a/modules/openid/openid.info b/modules/openid/openid.info index b92aacd1..3acd5dc1 100755 --- a/modules/openid/openid.info +++ b/modules/openid/openid.info @@ -5,8 +5,8 @@ package = Core core = 7.x files[] = openid.test -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/openid/tests/openid_test.info b/modules/openid/tests/openid_test.info index b17175e3..c8282754 100755 --- a/modules/openid/tests/openid_test.info +++ b/modules/openid/tests/openid_test.info @@ -6,8 +6,8 @@ core = 7.x dependencies[] = openid hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/overlay/overlay.info b/modules/overlay/overlay.info index e559572a..f1130643 100755 --- a/modules/overlay/overlay.info +++ b/modules/overlay/overlay.info @@ -4,8 +4,8 @@ package = Core version = VERSION core = 7.x -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/path/path.info b/modules/path/path.info index d4f5b55f..8e2b63b9 100755 --- a/modules/path/path.info +++ b/modules/path/path.info @@ -6,8 +6,8 @@ core = 7.x files[] = path.test configure = admin/config/search/path -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/php/php.info b/modules/php/php.info index aa5f7633..aea13e3b 100755 --- a/modules/php/php.info +++ b/modules/php/php.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x files[] = php.test -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/poll/poll.info b/modules/poll/poll.info index fe99797f..bd0cd896 100755 --- a/modules/poll/poll.info +++ b/modules/poll/poll.info @@ -6,8 +6,8 @@ core = 7.x files[] = poll.test stylesheets[all][] = poll.css -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/poll/poll.module b/modules/poll/poll.module index 70eb65dc..d3d64b17 100755 --- a/modules/poll/poll.module +++ b/modules/poll/poll.module @@ -249,6 +249,7 @@ function poll_form($node, &$form_state) { '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => $node->title, + '#maxlength' => 255, '#weight' => -5, ); @@ -720,7 +721,6 @@ function poll_view_voting($form, &$form_state, $node, $block = FALSE) { '#type' => 'radios', '#title' => t('Choices'), '#title_display' => 'invisible', - '#default_value' => -1, '#options' => $list, ); } @@ -748,7 +748,7 @@ function poll_view_voting($form, &$form_state, $node, $block = FALSE) { * Validation function for processing votes */ function poll_view_voting_validate($form, &$form_state) { - if ($form_state['values']['choice'] == -1) { + if (empty($form_state['values']['choice'])) { form_set_error( 'choice', t('Your vote could not be recorded because you did not select any of the choices.')); } } @@ -925,7 +925,6 @@ function template_preprocess_poll_results(&$variables) { * * @see poll-bar.tpl.php * @see poll-bar--block.tpl.php - * @see theme_poll_bar() */ function template_preprocess_poll_bar(&$variables) { if ($variables['block']) { diff --git a/modules/poll/poll.test b/modules/poll/poll.test index 35eea223..e24032d5 100755 --- a/modules/poll/poll.test +++ b/modules/poll/poll.test @@ -315,6 +315,11 @@ class PollVoteTestCase extends PollTestCase { $this->drupalLogin($vote_user); + // Record a vote without selecting any choice. + $edit = array(); + $this->drupalPost('node/' . $poll_nid, $edit, t('Vote')); + $this->assertText(t('Your vote could not be recorded because you did not select any of the choices.'), 'Found the empty poll submission error message.'); + // Record a vote for the first choice. $edit = array( 'choice' => '1', diff --git a/modules/profile/profile.info b/modules/profile/profile.info index 68a5f07f..174bbcdb 100755 --- a/modules/profile/profile.info +++ b/modules/profile/profile.info @@ -11,8 +11,8 @@ configure = admin/config/people/profile ; See user_system_info_alter(). hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/rdf/rdf.info b/modules/rdf/rdf.info index dba2b8e3..8626a8e5 100755 --- a/modules/rdf/rdf.info +++ b/modules/rdf/rdf.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x files[] = rdf.test -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/rdf/rdf.module b/modules/rdf/rdf.module index 877b598c..90a73826 100755 --- a/modules/rdf/rdf.module +++ b/modules/rdf/rdf.module @@ -190,17 +190,33 @@ function _rdf_get_default_mapping($type) { * An RDF mapping structure or an empty array if no record was found. */ function _rdf_mapping_load($type, $bundle) { - $mapping = db_select('rdf_mapping') - ->fields(NULL, array('mapping')) - ->condition('type', $type) - ->condition('bundle', $bundle) - ->execute() - ->fetchField(); + $mappings = _rdf_mapping_load_multiple($type, array($bundle)); + return $mappings ? reset($mappings) : array(); +} - if (!$mapping) { - return array(); +/** + * Helper function to retrieve a set of RDF mappings from the database. + * + * @param $type + * The entity type of the mappings. + * @param $bundles + * The bundles the mappings refer to. + * + * @return + * An array of RDF mapping structures, or an empty array. + */ +function _rdf_mapping_load_multiple($type, array $bundles) { + $mappings = db_select('rdf_mapping') + ->fields(NULL, array('bundle', 'mapping')) + ->condition('type', $type) + ->condition('bundle', $bundles) + ->execute() + ->fetchAllKeyed(); + + foreach ($mappings as $bundle => $mapping) { + $mappings[$bundle] = unserialize($mapping); } - return unserialize($mapping); + return $mappings; } /** @@ -368,10 +384,13 @@ function rdf_modules_uninstalled($modules) { function rdf_entity_info_alter(&$entity_info) { // Loop through each entity type and its bundles. foreach ($entity_info as $entity_type => $entity_type_info) { - if (isset($entity_type_info['bundles'])) { - foreach ($entity_type_info['bundles'] as $bundle => $bundle_info) { - if ($mapping = _rdf_mapping_load($entity_type, $bundle)) { - $entity_info[$entity_type]['bundles'][$bundle]['rdf_mapping'] = $mapping; + if (!empty($entity_type_info['bundles'])) { + $bundles = array_keys($entity_type_info['bundles']); + $mappings = _rdf_mapping_load_multiple($entity_type, $bundles); + + foreach ($bundles as $bundle) { + if (isset($mappings[$bundle])) { + $entity_info[$entity_type]['bundles'][$bundle]['rdf_mapping'] = $mappings[$bundle]; } else { // If no mapping was found in the database, assign the default RDF @@ -471,27 +490,17 @@ function rdf_preprocess_node(&$variables) { $variables['attributes_array']['about'] = empty($variables['node_url']) ? NULL: $variables['node_url']; $variables['attributes_array']['typeof'] = empty($variables['node']->rdf_mapping['rdftype']) ? NULL : $variables['node']->rdf_mapping['rdftype']; - // Adds RDFa markup to the title of the node. Because the RDFa markup is - // added to the
' . t('Blocks are boxes of content rendered into an area, or region, of a web page. The default theme Bartik, for example, implements the regions "Sidebar first", "Sidebar second", "Featured", "Content", "Header", "Footer", etc., and a block may appear in any one of these areas. The blocks administration page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions.', array('@blocks' => url('admin/structure/block'))) . '
'; + + // Help for another path in the block module + case 'admin/structure/block': + return '' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the Save blocks button at the bottom of the page.') . '
'; + } +} + /** * Register a module (or theme's) theme implementations. * @@ -3371,24 +3447,31 @@ function hook_install() { * hooks. See @link update_api Update versions of API functions @endlink for * details. * - * If your update task is potentially time-consuming, you'll need to implement a - * multipass update to avoid PHP timeouts. Multipass updates use the $sandbox - * parameter provided by the batch API (normally, $context['sandbox']) to store - * information between successive calls, and the $sandbox['#finished'] value - * to provide feedback regarding completion level. + * The $sandbox parameter should be used when a multipass update is needed, in + * circumstances where running the whole update at once could cause PHP to + * timeout. Each pass is run in a way that avoids PHP timeouts, provided each + * pass remains under the timeout limit. To signify that an update requires + * at least one more pass, set $sandbox['#finished'] to a number less than 1 + * (you need to do this each pass). The value of $sandbox['#finished'] will be + * unset between passes but all other data in $sandbox will be preserved. The + * system will stop iterating this update when $sandbox['#finished'] is left + * unset or set to a number higher than 1. It is recommended that + * $sandbox['#finished'] is initially set to 0, and then updated each pass to a + * number between 0 and 1 that represents the overall % completed for this + * update, finishing with 1. * - * See the batch operations page for more information on how to use the - * @link http://drupal.org/node/180528 Batch API. @endlink + * See the @link batch Batch operations topic @endlink for more information on + * how to use the Batch API. * - * @param $sandbox + * @param array $sandbox * Stores information for multipass updates. See above for more information. * - * @throws DrupalUpdateException, PDOException + * @throws DrupalUpdateException|PDOException * In case of error, update hooks should throw an instance of DrupalUpdateException * with a meaningful message for the user. If a database query fails for whatever * reason, it will throw a PDOException. * - * @return + * @return string|null * Optionally, update hooks may return a translated string that will be * displayed to the user after the update has completed. If no message is * returned, no message will be presented to the user. diff --git a/modules/system/system.base-rtl.css b/modules/system/system.base-rtl.css index 075cafec..79d6302d 100755 --- a/modules/system/system.base-rtl.css +++ b/modules/system/system.base-rtl.css @@ -9,10 +9,10 @@ */ /* Animated throbber */ html.js input.form-autocomplete { - background-position: 0% 2px; + background-position: 0% center; } html.js input.throbbing { - background-position: 0% -18px; + background-position: 0% center; } /** diff --git a/modules/system/system.base.css b/modules/system/system.base.css index 412b18a8..7e9220da 100755 --- a/modules/system/system.base.css +++ b/modules/system/system.base.css @@ -31,12 +31,13 @@ } /* Animated throbber */ html.js input.form-autocomplete { - background-image: url(../../misc/throbber.gif); - background-position: 100% 2px; /* LTR */ + background-image: url(../../misc/throbber-inactive.png); + background-position: 100% center; /* LTR */ background-repeat: no-repeat; } html.js input.throbbing { - background-position: 100% -18px; /* LTR */ + background-image: url(../../misc/throbber-active.gif); + background-position: 100% center; /* LTR */ } /** @@ -164,7 +165,7 @@ table.sticky-header { display: inline-block; } .ajax-progress .throbber { - background: transparent url(../../misc/throbber.gif) no-repeat 0px -18px; + background: transparent url(../../misc/throbber-active.gif) no-repeat 0px center; float: left; /* LTR */ height: 15px; margin: 2px; diff --git a/modules/system/system.info b/modules/system/system.info index 79b57d4b..fc3f30c1 100755 --- a/modules/system/system.info +++ b/modules/system/system.info @@ -12,8 +12,8 @@ files[] = system.test required = TRUE configure = admin/config/system -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/system/system.install b/modules/system/system.install index 43c7383a..64c989a6 100755 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -2854,7 +2854,14 @@ function system_update_7061(&$sandbox) { // We will convert filepaths to URI using the default scheme // and stripping off the existing file directory path. $file['uri'] = $scheme . preg_replace('!^' . preg_quote($basename) . '!', '', $file['filepath']); - $file['uri'] = file_stream_wrapper_uri_normalize($file['uri']); + // Normalize the URI but don't call file_stream_wrapper_uri_normalize() + // directly, since that is a higher-level API function which invokes + // hooks while validating the scheme, and those will not work during + // the upgrade. Instead, use a simpler version that just assumes the + // scheme from above is already valid. + if (($file_uri_scheme = file_uri_scheme($file['uri'])) && ($file_uri_target = file_uri_target($file['uri']))) { + $file['uri'] = $file_uri_scheme . '://' . $file_uri_target; + } unset($file['filepath']); // Insert into the file_managed table. // Each fid should only be stored once in file_managed. diff --git a/modules/system/system.module b/modules/system/system.module index d4f3bc47..6a6200ea 100755 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -242,6 +242,7 @@ function system_permission() { ), 'access site reports' => array( 'title' => t('View site reports'), + 'restrict access' => TRUE, ), 'block IP addresses' => array( 'title' => t('Block IP addresses'), @@ -373,6 +374,9 @@ function system_element_info() { '#element_validate' => array('form_validate_machine_name'), '#theme' => 'textfield', '#theme_wrappers' => array('form_element'), + // Use the same value callback as for textfields; this ensures that we only + // get string values. + '#value_callback' => 'form_type_textfield_value', ); $types['password'] = array( '#input' => TRUE, @@ -381,6 +385,9 @@ function system_element_info() { '#process' => array('ajax_process_form'), '#theme' => 'password', '#theme_wrappers' => array('form_element'), + // Use the same value callback as for textfields; this ensures that we only + // get string values. + '#value_callback' => 'form_type_textfield_value', ); $types['password_confirm'] = array( '#input' => TRUE, @@ -2398,6 +2405,10 @@ function _system_rebuild_module_data() { continue; } + // Add the info file modification time, so it becomes available for + // contributed modules to use for ordering module lists. + $module->info['mtime'] = filemtime(dirname($module->uri) . '/' . $module->name . '.info'); + // Merge in defaults and save. $modules[$key]->info = $module->info + $defaults; @@ -2536,6 +2547,10 @@ function _system_rebuild_theme_data() { $themes[$key]->filename = $theme->uri; $themes[$key]->info = drupal_parse_info_file($theme->uri) + $defaults; + // Add the info file modification time, so it becomes available for + // contributed modules to use for ordering theme lists. + $themes[$key]->info['mtime'] = filemtime($theme->uri); + // Invoke hook_system_info_alter() to give installed modules a chance to // modify the data in the .info files if necessary. $type = 'theme'; @@ -3385,7 +3400,7 @@ function system_timezone($abbreviation = '', $offset = -1, $is_daylight_saving_t * @ingroup themeable */ function theme_system_powered_by() { - return '' . t('Powered by Drupal', array('@poweredby' => 'http://drupal.org')) . ''; + return '' . t('Powered by Drupal', array('@poweredby' => 'https://www.drupal.org')) . ''; } /** diff --git a/modules/system/system.test b/modules/system/system.test index cae5cc78..3e26bae8 100755 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -556,6 +556,34 @@ class ModuleDependencyTestCase extends ModuleTestCase { $this->drupalPost(NULL, NULL, t('Uninstall')); $this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.'); } + + /** + * Tests whether the correct module metadata is returned. + */ + function testModuleMetaData() { + // Generate the list of available modules. + $modules = system_rebuild_module_data(); + // Check that the mtime field exists for the system module. + $this->assertTrue(!empty($modules['system']->info['mtime']), 'The system.info file modification time field is present.'); + // Use 0 if mtime isn't present, to avoid an array index notice. + $test_mtime = !empty($modules['system']->info['mtime']) ? $modules['system']->info['mtime'] : 0; + // Ensure the mtime field contains a number that is greater than zero. + $this->assertTrue(is_numeric($test_mtime) && ($test_mtime > 0), 'The system.info file modification time field contains a timestamp.'); + } + + /** + * Tests whether the correct theme metadata is returned. + */ + function testThemeMetaData() { + // Generate the list of available themes. + $themes = system_rebuild_theme_data(); + // Check that the mtime field exists for the bartik theme. + $this->assertTrue(!empty($themes['bartik']->info['mtime']), 'The bartik.info file modification time field is present.'); + // Use 0 if mtime isn't present, to avoid an array index notice. + $test_mtime = !empty($themes['bartik']->info['mtime']) ? $themes['bartik']->info['mtime'] : 0; + // Ensure the mtime field contains a number that is greater than zero. + $this->assertTrue(is_numeric($test_mtime) && ($test_mtime > 0), 'The bartik.info file modification time field contains a timestamp.'); + } } /** @@ -2797,3 +2825,75 @@ class SystemValidTokenTest extends DrupalUnitTestCase { return TRUE; } } + +/** + * Tests drupal_set_message() and related functions. + */ +class DrupalSetMessageTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Messages', + 'description' => 'Tests that messages can be displayed using drupal_set_message().', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * Tests setting messages and removing one before it is displayed. + */ + function testSetRemoveMessages() { + // The page at system-test/drupal-set-message sets two messages and then + // removes the first before it is displayed. + $this->drupalGet('system-test/drupal-set-message'); + $this->assertNoText('First message (removed).'); + $this->assertText('Second message (not removed).'); + } +} + +/** + * Tests confirm form destinations. + */ +class ConfirmFormTest extends DrupalWebTestCase { + protected $admin_user; + + public static function getInfo() { + return array( + 'name' => 'Confirm form', + 'description' => 'Tests that the confirm form does not use external destinations.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp(); + + $this->admin_user = $this->drupalCreateUser(array('administer users')); + $this->drupalLogin($this->admin_user); + } + + /** + * Tests that the confirm form does not use external destinations. + */ + function testConfirmForm() { + $this->drupalGet('user/1/cancel'); + $this->assertCancelLinkUrl(url('user/1')); + $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'node'))); + $this->assertCancelLinkUrl(url('node')); + $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'http://example.com'))); + $this->assertCancelLinkUrl(url('user/1')); + } + + /** + * Asserts that a cancel link is present pointing to the provided URL. + */ + function assertCancelLinkUrl($url, $message = '', $group = 'Other') { + $links = $this->xpath('//a[normalize-space(text())=:label and @href=:url]', array(':label' => t('Cancel'), ':url' => $url)); + $message = ($message ? $message : format_string('Cancel link with url %url found.', array('%url' => $url))); + return $this->assertTrue(isset($links[0]), $message, $group); + } +} diff --git a/modules/system/tests/cron_queue_test.info b/modules/system/tests/cron_queue_test.info index da0984a1..75eb4aa2 100644 --- a/modules/system/tests/cron_queue_test.info +++ b/modules/system/tests/cron_queue_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/taxonomy/taxonomy.api.php b/modules/taxonomy/taxonomy.api.php index b9c23dbf..f3c5022b 100755 --- a/modules/taxonomy/taxonomy.api.php +++ b/modules/taxonomy/taxonomy.api.php @@ -212,7 +212,7 @@ function hook_taxonomy_term_view($term, $view_mode, $langcode) { * documentation respectively for details. * * @param $build - * A renderable array representing the node content. + * A renderable array representing the term. * * @see hook_entity_view_alter() */ diff --git a/modules/taxonomy/taxonomy.info b/modules/taxonomy/taxonomy.info index d8dcd982..0e0f111b 100755 --- a/modules/taxonomy/taxonomy.info +++ b/modules/taxonomy/taxonomy.info @@ -8,8 +8,8 @@ files[] = taxonomy.module files[] = taxonomy.test configure = admin/structure/taxonomy -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module index 4191146a..e147c1ca 100755 --- a/modules/taxonomy/taxonomy.module +++ b/modules/taxonomy/taxonomy.module @@ -776,15 +776,26 @@ function taxonomy_term_show($term) { * An array in the format expected by drupal_render(). */ function taxonomy_term_view_multiple($terms, $view_mode = 'teaser', $weight = 0, $langcode = NULL) { - field_attach_prepare_view('taxonomy_term', $terms, $view_mode, $langcode); - entity_prepare_view('taxonomy_term', $terms, $langcode); $build = array(); + $entities_by_view_mode = entity_view_mode_prepare('taxonomy_term', $terms, $view_mode, $langcode); + foreach ($entities_by_view_mode as $entity_view_mode => $entities) { + field_attach_prepare_view('taxonomy_term', $entities, $entity_view_mode, $langcode); + entity_prepare_view('taxonomy_term', $entities, $langcode); + + foreach ($entities as $entity) { + $build['taxonomy_terms'][$entity->tid] = taxonomy_term_view($entity, $entity_view_mode, $langcode); + } + } + foreach ($terms as $term) { - $build['taxonomy_terms'][$term->tid] = taxonomy_term_view($term, $view_mode, $langcode); $build['taxonomy_terms'][$term->tid]['#weight'] = $weight; $weight++; } + // Sort here, to preserve the input order of the entities that were passed to + // this function. + uasort($build['taxonomy_terms'], 'element_sort'); $build['taxonomy_terms']['#sorted'] = TRUE; + return $build; } @@ -817,12 +828,7 @@ function taxonomy_term_build_content($term, $view_mode = 'full', $langcode = NUL $term->content = array(); // Allow modules to change the view mode. - $context = array( - 'entity_type' => 'taxonomy_term', - 'entity' => $term, - 'langcode' => $langcode, - ); - drupal_alter('entity_view_mode', $view_mode, $context); + $view_mode = key(entity_view_mode_prepare('taxonomy_term', array($term->tid => $term), $view_mode, $langcode)); // Add the term description if the term has one and it is visible. $type = 'taxonomy_term'; diff --git a/modules/toolbar/toolbar.info b/modules/toolbar/toolbar.info index 7cf9fd02..7aac4b74 100755 --- a/modules/toolbar/toolbar.info +++ b/modules/toolbar/toolbar.info @@ -4,8 +4,8 @@ core = 7.x package = Core version = VERSION -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/tracker/tracker.info b/modules/tracker/tracker.info index 85826146..a5cbcc08 100755 --- a/modules/tracker/tracker.info +++ b/modules/tracker/tracker.info @@ -6,8 +6,8 @@ version = VERSION core = 7.x files[] = tracker.test -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/tracker/tracker.module b/modules/tracker/tracker.module index 8694222d..ab3e7485 100755 --- a/modules/tracker/tracker.module +++ b/modules/tracker/tracker.module @@ -263,7 +263,7 @@ function _tracker_add($nid, $uid, $changed) { )) ->execute(); - // Create or update the user-level data. + // Create or update the user-level data, first for the user posting. db_merge('tracker_user') ->key(array( 'nid' => $nid, @@ -274,6 +274,14 @@ function _tracker_add($nid, $uid, $changed) { 'published' => $node->status, )) ->execute(); + // Update the times for all the other users tracking the post. + db_update('tracker_user') + ->condition('nid', $nid) + ->fields(array( + 'changed' => $changed, + 'published' => $node->status, + )) + ->execute(); } /** diff --git a/modules/tracker/tracker.test b/modules/tracker/tracker.test index 66ebb848..8a48ea81 100755 --- a/modules/tracker/tracker.test +++ b/modules/tracker/tracker.test @@ -182,6 +182,72 @@ class TrackerTest extends DrupalWebTestCase { $this->assertText('1 new', 'New comments are counted on the tracker listing pages.'); } + /** + * Tests for ordering on a users tracker listing when comments are posted. + */ + function testTrackerOrderingNewComments() { + $this->drupalLogin($this->user); + + $node_one = $this->drupalCreateNode(array( + 'title' => $this->randomName(8), + )); + + $node_two = $this->drupalCreateNode(array( + 'title' => $this->randomName(8), + )); + + // Now get other_user to track these pieces of content. + $this->drupalLogin($this->other_user); + + // Add a comment to the first page. + $comment = array( + 'subject' => $this->randomName(), + 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), + ); + $this->drupalPost('comment/reply/' . $node_one->nid, $comment, t('Save')); + + // If the comment is posted in the same second as the last one then Drupal + // can't tell the difference, so we wait one second here. + sleep(1); + + // Add a comment to the second page. + $comment = array( + 'subject' => $this->randomName(), + 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), + ); + $this->drupalPost('comment/reply/' . $node_two->nid, $comment, t('Save')); + + // We should at this point have in our tracker for other_user: + // 1. node_two + // 2. node_one + // Because that's the reverse order of the posted comments. + + // Now we're going to post a comment to node_one which should jump it to the + // top of the list. + + $this->drupalLogin($this->user); + // If the comment is posted in the same second as the last one then Drupal + // can't tell the difference, so we wait one second here. + sleep(1); + + // Add a comment to the second page. + $comment = array( + 'subject' => $this->randomName(), + 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), + ); + $this->drupalPost('comment/reply/' . $node_one->nid, $comment, t('Save')); + + // Switch back to the other_user and assert that the order has swapped. + $this->drupalLogin($this->other_user); + $this->drupalGet('user/' . $this->other_user->uid . '/track'); + // This is a cheeky way of asserting that the nodes are in the right order + // on the tracker page. + // It's almost certainly too brittle. + $pattern = '/' . preg_quote($node_one->title) . '.+' . preg_quote($node_two->title) . '/s'; + $this->verbose($pattern); + $this->assertPattern($pattern, 'Most recently commented on node appears at the top of tracker'); + } + /** * Tests that existing nodes are indexed by cron. */ diff --git a/modules/translation/tests/translation_test.info b/modules/translation/tests/translation_test.info index c959a4cd..b12ce1f2 100755 --- a/modules/translation/tests/translation_test.info +++ b/modules/translation/tests/translation_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/translation/translation.info b/modules/translation/translation.info index fa53de36..5815c938 100755 --- a/modules/translation/translation.info +++ b/modules/translation/translation.info @@ -6,8 +6,8 @@ version = VERSION core = 7.x files[] = translation.test -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/translation/translation.module b/modules/translation/translation.module index 33123570..53c4641e 100755 --- a/modules/translation/translation.module +++ b/modules/translation/translation.module @@ -336,9 +336,13 @@ function translation_node_insert($node) { 'tnid' => $tnid, 'translate' => 0, )) - ->condition('nid', $node->translation_source->nid) + ->condition('nid', $tnid) ->execute(); + + // Flush the (untranslated) source node from the load cache. + entity_get_controller('node')->resetCache(array($tnid)); } + db_update('node') ->fields(array( 'tnid' => $tnid, @@ -368,13 +372,23 @@ function translation_node_update($node) { )) ->condition('nid', $node->nid) ->execute(); + if (!empty($node->translation['retranslate'])) { // This is the source node, asking to mark all translations outdated. - db_update('node') - ->fields(array('translate' => 1)) + $translations = db_select('node', 'n') + ->fields('n', array('nid')) ->condition('nid', $node->nid, '<>') ->condition('tnid', $node->tnid) + ->execute() + ->fetchCol(); + + db_update('node') + ->fields(array('translate' => 1)) + ->condition('nid', $translations, 'IN') ->execute(); + + // Flush the modified translation nodes from the load cache. + entity_get_controller('node')->resetCache($translations); } } } @@ -420,11 +434,16 @@ function translation_remove_from_set($node) { 'tnid' => 0, 'translate' => 0, )); - if (db_query('SELECT COUNT(*) FROM {node} WHERE tnid = :tnid', array(':tnid' => $node->tnid))->fetchField() == 1) { + + // Determine which nodes to apply the update to. + $set_nids = db_query('SELECT nid FROM {node} WHERE tnid = :tnid', array(':tnid' => $node->tnid))->fetchCol(); + if (count($set_nids) == 1) { // There is only one node left in the set: remove the set altogether. $query ->condition('tnid', $node->tnid) ->execute(); + + $flush_set = TRUE; } else { $query @@ -439,8 +458,14 @@ function translation_remove_from_set($node) { ->fields(array('tnid' => $new_tnid)) ->condition('tnid', $node->tnid) ->execute(); + + $flush_set = TRUE; } } + + // Flush the modified nodes from the load cache. + $nids = !empty($flush_set) ? $set_nids : array($node->nid); + entity_get_controller('node')->resetCache($nids); } } diff --git a/modules/trigger/tests/trigger_test.info b/modules/trigger/tests/trigger_test.info index 8a0a3458..f0b254b0 100755 --- a/modules/trigger/tests/trigger_test.info +++ b/modules/trigger/tests/trigger_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/trigger/trigger.info b/modules/trigger/trigger.info index 3ffb5f58..18e1e196 100755 --- a/modules/trigger/trigger.info +++ b/modules/trigger/trigger.info @@ -6,8 +6,8 @@ core = 7.x files[] = trigger.test configure = admin/structure/trigger -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/update/tests/aaa_update_test.info b/modules/update/tests/aaa_update_test.info index f5c804f6..716ecf91 100755 --- a/modules/update/tests/aaa_update_test.info +++ b/modules/update/tests/aaa_update_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/update/tests/bbb_update_test.info b/modules/update/tests/bbb_update_test.info index 9d1a30cc..3b38ddd5 100755 --- a/modules/update/tests/bbb_update_test.info +++ b/modules/update/tests/bbb_update_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/update/tests/ccc_update_test.info b/modules/update/tests/ccc_update_test.info index dbf63f6d..f1ce3c27 100755 --- a/modules/update/tests/ccc_update_test.info +++ b/modules/update/tests/ccc_update_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info b/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info index faa6cdff..17ce0add 100755 --- a/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info +++ b/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info @@ -3,8 +3,8 @@ description = Test theme which acts as a base theme for other test subthemes. core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info b/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info index dced67c6..87de3a78 100755 --- a/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info +++ b/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info @@ -4,8 +4,8 @@ core = 7.x base theme = update_test_basetheme hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/update/tests/update_test.info b/modules/update/tests/update_test.info index af28f502..ae3f6c2f 100755 --- a/modules/update/tests/update_test.info +++ b/modules/update/tests/update_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/update/update.compare.inc b/modules/update/update.compare.inc index 6e0c5fee..072a0daa 100755 --- a/modules/update/update.compare.inc +++ b/modules/update/update.compare.inc @@ -417,14 +417,15 @@ function update_calculate_project_data($available) { * version (e.g., 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development * snapshots for a given major version are always listed last. * - * @param $project - * An array containing information about a specific project. + * @param $unused + * Input is not being used, but remains in function for API compatibility + * reasons. * @param $project_data * An array containing information about a specific project. * @param $available * Data about available project releases of a specific project. */ -function update_calculate_project_update_status($project, &$project_data, $available) { +function update_calculate_project_update_status($unused, &$project_data, $available) { foreach (array('title', 'link') as $attribute) { if (!isset($project_data[$attribute]) && isset($available[$attribute])) { $project_data[$attribute] = $available[$attribute]; diff --git a/modules/update/update.info b/modules/update/update.info index f416d5de..230a4147 100755 --- a/modules/update/update.info +++ b/modules/update/update.info @@ -6,8 +6,8 @@ core = 7.x files[] = update.test configure = admin/reports/updates/settings -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/user/tests/user_form_test.info b/modules/user/tests/user_form_test.info index 2639cff1..e0588111 100755 --- a/modules/user/tests/user_form_test.info +++ b/modules/user/tests/user_form_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/user/user.api.php b/modules/user/user.api.php index 3afc88ab..edc61bd3 100755 --- a/modules/user/user.api.php +++ b/modules/user/user.api.php @@ -327,14 +327,6 @@ function hook_user_logout($account) { * The module should format its custom additions for display and add them to the * $account->content array. * - * Note that when this hook is invoked, the changes have not yet been written to - * the database, because a database transaction is still in progress. The - * transaction is not finalized until the save operation is entirely completed - * and user_save() goes out of scope. You should not rely on data in the - * database at this time as it is not updated yet. You should also note that any - * write/update database queries executed from this hook are also not committed - * immediately. Check user_save() and db_transaction() for more info. - * * @param $account * The user object on which the operation is being performed. * @param $view_mode @@ -386,7 +378,7 @@ function hook_user_view_alter(&$build) { } /** - * Inform other modules that a user role is about to be saved. + * Act on a user role being inserted or updated. * * Modules implementing this hook can act on the user role object before * it has been saved to the database. @@ -405,7 +397,7 @@ function hook_user_role_presave($role) { } /** - * Inform other modules that a user role has been added. + * Respond to creation of a new user role. * * Modules implementing this hook can act on the user role object when saved to * the database. It's recommended that you implement this hook if your module @@ -426,7 +418,7 @@ function hook_user_role_insert($role) { } /** - * Inform other modules that a user role has been updated. + * Respond to updates to a user role. * * Modules implementing this hook can act on the user role object when updated. * It's recommended that you implement this hook if your module adds additional @@ -447,7 +439,7 @@ function hook_user_role_update($role) { } /** - * Inform other modules that a user role has been deleted. + * Respond to user role deletion. * * This hook allows you act when a user role has been deleted. * If your module stores references to roles, it's recommended that you diff --git a/modules/user/user.info b/modules/user/user.info index c51b75b9..d2de841a 100755 --- a/modules/user/user.info +++ b/modules/user/user.info @@ -9,8 +9,8 @@ required = TRUE configure = admin/config/people stylesheets[all][] = user.css -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/modules/user/user.install b/modules/user/user.install index 4e1a3c22..b573e72d 100755 --- a/modules/user/user.install +++ b/modules/user/user.install @@ -81,7 +81,7 @@ function user_schema() { ), 'foreign keys' => array( 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -278,7 +278,7 @@ function user_schema() { 'columns' => array('uid' => 'uid'), ), 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -356,11 +356,13 @@ function user_update_dependencies() { 'filter' => 7000, ); - // user_update_7012() uses the file API, which relies on the {file_managed} - // table, so it must run after system_update_7034(), which creates that - // table. + // user_update_7012() uses the file API and inserts records into the + // {file_managed} table, so it therefore must run after system_update_7061(), + // which inserts files with specific IDs into the table and therefore relies + // on the table being empty (otherwise it would accidentally overwrite + // existing records). $dependencies['user'][7012] = array( - 'system' => 7034, + 'system' => 7061, ); // user_update_7013() uses the file usage API, which relies on the diff --git a/modules/user/user.module b/modules/user/user.module index 01ae17f4..9637a716 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -32,7 +32,7 @@ define('USER_REGISTER_VISITORS', 1); define('USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL', 2); /** - * Implement hook_help(). + * Implements hook_help(). */ function user_help($path, $arg) { global $user; @@ -501,12 +501,17 @@ function user_save($account, $edit = array(), $category = 'account') { file_usage_delete($account->original->picture, 'user', 'user', $account->uid); file_delete($account->original->picture); } + // Save the picture object, if it is set. drupal_write_record() expects + // $account->picture to be a FID. + $picture = empty($account->picture) ? NULL : $account->picture; $account->picture = empty($account->picture->fid) ? 0 : $account->picture->fid; // Do not allow 'uid' to be changed. $account->uid = $account->original->uid; // Save changes to the user table. $success = drupal_write_record('users', $account, 'uid'); + // Restore the picture object. + $account->picture = $picture; if ($success === FALSE) { // The query failed - better to abort the save than risk further // data loss. @@ -589,16 +594,16 @@ function user_save($account, $edit = array(), $category = 'account') { user_module_invoke('insert', $edit, $account, $category); module_invoke_all('entity_insert', $account, 'user'); - // Save user roles. - if (count($account->roles) > 1) { + // Save user roles. Skip built-in roles, and ones that were already saved + // to the database during hook calls. + $rids_to_skip = array_merge(array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID), db_query('SELECT rid FROM {users_roles} WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol()); + if ($rids_to_save = array_diff(array_keys($account->roles), $rids_to_skip)) { $query = db_insert('users_roles')->fields(array('uid', 'rid')); - foreach (array_keys($account->roles) as $rid) { - if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) { - $query->values(array( - 'uid' => $account->uid, - 'rid' => $rid, - )); - } + foreach ($rids_to_save as $rid) { + $query->values(array( + 'uid' => $account->uid, + 'rid' => $rid, + )); } $query->execute(); } @@ -843,6 +848,26 @@ function user_is_blocked($name) { ->execute()->fetchObject(); } +/** + * Checks if a user has a role. + * + * @param int $rid + * A role ID. + * + * @param object|null $account + * (optional) A user account. Defaults to the current user. + * + * @return bool + * TRUE if the user has the role, or FALSE if not. + */ +function user_has_role($rid, $account = NULL) { + if (!$account) { + $account = $GLOBALS['user']; + } + + return isset($account->roles[$rid]); +} + /** * Implements hook_permission(). */ @@ -2323,27 +2348,14 @@ function user_external_login_register($name, $module) { * following properties: * - uid: The user ID number. * - login: The UNIX timestamp of the user's last login. - * @param array $options - * (optional) A keyed array of settings. Supported options are: - * - langcode: A language code to be used when generating locale-sensitive - * urls. If langcode is NULL the users preferred language is used. - * * * @return * A unique URL that provides a one-time log in for the user, from which * they can change their password. */ -function user_pass_reset_url($account, $options = array()) { +function user_pass_reset_url($account) { $timestamp = REQUEST_TIME; - $url_options = array('absolute' => TRUE); - if (isset($options['langcode'])) { - $languages = language_list(); - $url_options['language'] = $languages[$options['langcode']]; - } - else { - $url_options['language'] = user_preferred_language($account); - } - return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), $url_options); + return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2355,11 +2367,6 @@ function user_pass_reset_url($account, $options = array()) { * - uid: The user ID number. * - pass: The hashed user password string. * - login: The UNIX timestamp of the user's last login. - * @param array $options - * (optional) A keyed array of settings. Supported options are: - * - langcode: A language code to be used when generating locale-sensitive - * urls. If langcode is NULL the users preferred language is used. - * * * @return * A unique URL that may be used to confirm the cancellation of the user @@ -2368,17 +2375,9 @@ function user_pass_reset_url($account, $options = array()) { * @see user_mail_tokens() * @see user_cancel_confirm() */ -function user_cancel_url($account, $options = array()) { +function user_cancel_url($account) { $timestamp = REQUEST_TIME; - $url_options = array('absolute' => TRUE); - if (isset($options['langcode'])) { - $languages = language_list(); - $url_options['language'] = $languages[$options['langcode']]; - } - else { - $url_options['language'] = user_preferred_language($account); - } - return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), $url_options); + return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2398,12 +2397,33 @@ function user_cancel_url($account, $options = array()) { * A UNIX timestamp, typically REQUEST_TIME. * @param int $login * The UNIX timestamp of the user's last login. + * @param int $uid + * The user ID of the user account. * * @return * A string that is safe for use in URLs and SQL statements. */ -function user_pass_rehash($password, $timestamp, $login) { - return drupal_hmac_base64($timestamp . $login, drupal_get_hash_salt() . $password); +function user_pass_rehash($password, $timestamp, $login, $uid) { + // Backwards compatibility: Try to determine a $uid if one was not passed. + // (Since $uid is a required parameter to this function, a PHP warning will + // be generated if it's not provided, which is an indication that the calling + // code should be updated. But the code below will try to generate a correct + // hash in the meantime.) + if (!isset($uid)) { + $uids = db_query_range('SELECT uid FROM {users} WHERE pass = :password AND login = :login AND uid > 0', 0, 2, array(':password' => $password, ':login' => $login))->fetchCol(); + // If exactly one user account matches the provided password and login + // timestamp, proceed with that $uid. + if (count($uids) == 1) { + $uid = reset($uids); + } + // Otherwise there is no safe hash to return, so return a random string + // that will never be treated as a valid token. + else { + return drupal_random_key(); + } + } + + return drupal_hmac_base64($timestamp . $login . $uid, drupal_get_hash_salt() . $password); } /** @@ -2659,12 +2679,7 @@ function user_build_content($account, $view_mode = 'full', $langcode = NULL) { $account->content = array(); // Allow modules to change the view mode. - $context = array( - 'entity_type' => 'user', - 'entity' => $account, - 'langcode' => $langcode, - ); - drupal_alter('entity_view_mode', $view_mode, $context); + $view_mode = key(entity_view_mode_prepare('user', array($account->uid => $account), $view_mode, $langcode)); // Build fields content. field_attach_prepare_view('user', array($account->uid => $account), $view_mode, $langcode); @@ -2848,7 +2863,7 @@ Your account on [site:name] has been canceled. if ($replace) { // We do not sanitize the token replacement, since the output of this // replacement is intended for an e-mail message, not a web browser. - return token_replace($text, $variables, array('langcode' => $langcode, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE)); + return token_replace($text, $variables, array('language' => $language, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE)); } return $text; @@ -2875,8 +2890,8 @@ Your account on [site:name] has been canceled. */ function user_mail_tokens(&$replacements, $data, $options) { if (isset($data['user'])) { - $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options); - $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options); + $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user']); + $replacements['[user:cancel-url]'] = user_cancel_url($data['user']); } } @@ -3799,8 +3814,8 @@ function user_register_form($form, &$form_state) { // inside the submit function interferes with form processing and breaks // hook_form_alter(). $form['administer_users'] = array( - '#type' => 'value', - '#value' => $admin, + '#type' => 'value', + '#value' => $admin, ); // If we aren't admin but already logged on, go to the user page instead. diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc index 7d40663e..f21bd134 100755 --- a/modules/user/user.pages.inc +++ b/modules/user/user.pages.inc @@ -126,7 +126,7 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.')); drupal_goto('user/password'); } - elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) { + elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { // First stage is a confirmation form, then login if ($action == 'login') { // Set the new user. @@ -183,8 +183,11 @@ function user_logout() { /** * Process variables for user-profile.tpl.php. * - * The $variables array contains the following arguments: - * - $account + * @param array $variables + * An associative array containing: + * - elements: An associative array containing the user information and any + * fields attached to the user. Properties used: + * - #account: The user account of the profile being viewed. * * @see user-profile.tpl.php */ @@ -356,7 +359,6 @@ function user_cancel_confirm_form($form, &$form_state, $account) { $form['_account'] = array('#type' => 'value', '#value' => $account); // Display account cancellation method selection, if allowed. - $default_method = variable_get('user_cancel_method', 'user_cancel_block'); $admin_access = user_access('administer users'); $can_select_method = $admin_access || user_access('select account cancellation method'); $form['user_cancel_method'] = array( @@ -520,7 +522,7 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') { // Basic validation of arguments. if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) { // Validate expiration and hashed password/login. - if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) { + if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { $edit = array( 'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE), ); diff --git a/modules/user/user.test b/modules/user/user.test index 0fa0345b..07be4c2c 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -498,7 +498,7 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { // To attempt an expired password reset, create a password reset link as if // its request time was 60 seconds older than the allowed limit of timeout. $bogus_timestamp = REQUEST_TIME - variable_get('user_password_reset_timeout', 86400) - 60; - $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); } @@ -519,6 +519,74 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertFieldByName('name', $edit['name'], 'User name found.'); } + /** + * Make sure that users cannot forge password reset URLs of other users. + */ + function testResetImpersonation() { + // Make sure user 1 has a valid password, so it does not interfere with the + // test user accounts that are created below. + $account = user_load(1); + user_save($account, array('pass' => user_password())); + + // Create two identical user accounts except for the user name. They must + // have the same empty password, so we can't use $this->drupalCreateUser(). + $edit = array(); + $edit['name'] = $this->randomName(); + $edit['mail'] = $edit['name'] . '@example.com'; + $edit['status'] = 1; + + $user1 = user_save(drupal_anonymous_user(), $edit); + + $edit['name'] = $this->randomName(); + $user2 = user_save(drupal_anonymous_user(), $edit); + + // The password reset URL must not be valid for the second user when only + // the user ID is changed in the URL. + $reset_url = user_pass_reset_url($user1); + $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url); + $this->drupalGet($attack_reset_url); + $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + + // When legacy code calls user_pass_rehash() without providing the $uid + // parameter, neither password reset URL should be valid since it is + // impossible for the system to determine which user account the token was + // intended for. + $timestamp = REQUEST_TIME; + // Pass an explicit NULL for the $uid parameter of user_pass_rehash() + // rather than not passing it at all, to avoid triggering PHP warnings in + // the test. + $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE)); + $this->drupalGet($reset_url); + $this->assertNoText($user1->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url); + $this->drupalGet($attack_reset_url); + $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + + // To verify that user_pass_rehash() never returns a valid result in the + // above situation (even if legacy code also called it to attempt to + // validate the token, rather than just to generate the URL), check that a + // second call with the same parameters produces a different result. + $new_reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $this->assertNotEqual($reset_url_token, $new_reset_url_token); + + // However, when the duplicate account is removed, the password reset URL + // should be valid. + user_delete($user2->uid); + $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE)); + $this->drupalGet($reset_url); + $this->assertText($user1->name, 'The valid password reset page shows the user name.'); + $this->assertUrl($reset_url, array(), 'The user remains on the password reset login page.'); + $this->assertNoText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + } + } /** @@ -558,7 +626,7 @@ class UserCancelTestCase extends DrupalWebTestCase { // Attempt bogus account cancellation request confirmation. $timestamp = $account->login; - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertResponse(403, 'Bogus cancelling request rejected.'); $account = user_load($account->uid); $this->assertTrue($account->status == 1, 'User account was not canceled.'); @@ -631,14 +699,14 @@ class UserCancelTestCase extends DrupalWebTestCase { // Attempt bogus account cancellation request confirmation. $bogus_timestamp = $timestamp + 60; - $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Bogus cancelling request rejected.'); $account = user_load($account->uid); $this->assertTrue($account->status == 1, 'User account was not canceled.'); // Attempt expired account cancellation request confirmation. $bogus_timestamp = $timestamp - 86400 - 60; - $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Expired cancel account request rejected.'); $accounts = user_load_multiple(array($account->uid), array('status' => 1)); $this->assertTrue(reset($accounts), 'User account was not canceled.'); @@ -675,7 +743,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $account = user_load($account->uid, TRUE); $this->assertTrue($account->status == 0, 'User has been blocked.'); @@ -713,7 +781,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $account = user_load($account->uid, TRUE); $this->assertTrue($account->status == 0, 'User has been blocked.'); @@ -763,7 +831,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); // Confirm that user's content has been attributed to anonymous user. @@ -827,7 +895,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); // Confirm that user's content has been deleted. @@ -1127,6 +1195,17 @@ class UserPictureTestCase extends DrupalWebTestCase { $pic_path2 = $this->saveUserPicture($image); $this->assertNotEqual($pic_path, $pic_path2, 'Filename of second picture is different.'); + + // Check if user picture has a valid file ID after saving the user. + $account = user_load($this->user->uid, TRUE); + $this->assertTrue(is_object($account->picture), 'User picture object is valid after user load.'); + $this->assertNotNull($account->picture->fid, 'User picture object has a FID after user load.'); + $this->assertTrue(is_file($account->picture->uri), 'File is located in proper directory after user load.'); + user_save($account); + // Verify that the user save does not destroy the user picture object. + $this->assertTrue(is_object($account->picture), 'User picture object is valid after user save.'); + $this->assertNotNull($account->picture->fid, 'User picture object has a FID after user save.'); + $this->assertTrue(is_file($account->picture->uri), 'File is located in proper directory after user save.'); } } @@ -2066,26 +2145,6 @@ class UserTokenReplaceTestCase extends DrupalWebTestCase { ); } - public function setUp() { - parent::setUp('locale'); - - $account = $this->drupalCreateUser(array('access administration pages', 'administer languages')); - $this->drupalLogin($account); - - // Add language. - $edit = array('langcode' => 'de'); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - - // Enable URL language detection and selection. - $edit = array('language[enabled][locale-url]' => 1); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Reset static caching. - drupal_static_reset('language_list'); - drupal_static_reset('locale_url_outbound_alter'); - drupal_static_reset('locale_language_url_rewrite_url'); - } - /** * Creates a user, then tests the tokens generated from it. */ @@ -2136,39 +2195,6 @@ class UserTokenReplaceTestCase extends DrupalWebTestCase { $output = token_replace($input, array('user' => $account), array('language' => $language, 'sanitize' => FALSE)); $this->assertEqual($output, $expected, format_string('Unsanitized user token %token replaced.', array('%token' => $input))); } - - $languages = language_list(); - - // Generate login and cancel link. - $tests = array(); - $tests['[user:one-time-login-url]'] = user_pass_reset_url($account); - $tests['[user:cancel-url]'] = user_cancel_url($account); - - // Generate tokens with interface language. - $link = url('user', array('absolute' => TRUE)); - foreach ($tests as $input => $expected) { - $output = token_replace($input, array('user' => $account), array('langcode' => $language->language, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE)); - $this->assertTrue(strpos($output, $link) === 0, 'Generated URL is in interface language.'); - } - - // Generate tokens with the user's preferred language. - $edit['language'] = 'de'; - $account = user_save($account, $edit); - $link = url('user', array('language' => $languages[$account->language], 'absolute' => TRUE)); - foreach ($tests as $input => $expected) { - $output = token_replace($input, array('user' => $account), array('callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE)); - $this->assertTrue(strpos($output, $link) === 0, "Generated URL is in the user's preferred language."); - } - - // Generate tokens with one specific language. - $link = url('user', array('language' => $languages['de'], 'absolute' => TRUE)); - foreach ($tests as $input => $expected) { - foreach (array($user1, $user2) as $account) { - $output = token_replace($input, array('user' => $account), array('langcode' => 'de', 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE)); - $this->assertTrue(strpos($output, $link) === 0, "Generated URL in in the requested language."); - } - } - } } diff --git a/profiles/minimal/minimal.info b/profiles/minimal/minimal.info index 26b86610..1b9d8fa7 100755 --- a/profiles/minimal/minimal.info +++ b/profiles/minimal/minimal.info @@ -5,8 +5,8 @@ core = 7.x dependencies[] = block dependencies[] = dblog -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/profiles/standard/standard.info b/profiles/standard/standard.info index 54b5b917..a48955f6 100755 --- a/profiles/standard/standard.info +++ b/profiles/standard/standard.info @@ -24,8 +24,8 @@ dependencies[] = field_ui dependencies[] = file dependencies[] = rdf -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info b/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info index 4503565c..cce38725 100755 --- a/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info +++ b/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info @@ -6,8 +6,8 @@ core = 7.x hidden = TRUE files[] = drupal_system_listing_compatible_test.test -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info index 6d2954cb..25c98dee 100755 --- a/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info +++ b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info @@ -8,8 +8,8 @@ version = VERSION core = 6.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/profiles/testing/testing.info b/profiles/testing/testing.info index f5c4d2ae..d0e7674a 100755 --- a/profiles/testing/testing.info +++ b/profiles/testing/testing.info @@ -4,8 +4,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/robots.txt b/robots.txt index 7de84356..ff9e2868 100755 --- a/robots.txt +++ b/robots.txt @@ -11,10 +11,7 @@ # Ignored: http://example.com/site/robots.txt # # For more information about the robots.txt standard, see: -# http://www.robotstxt.org/wc/robots.html -# -# For syntax checking, see: -# http://www.sxw.org.uk/computing/robots/check.html +# http://www.robotstxt.org/robotstxt.html User-agent: * Crawl-delay: 10 diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 189d7f2e..9078168a 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -419,9 +419,20 @@ function simpletest_script_get_test_list() { else { if ($args['class']) { // Check for valid class names. - foreach ($args['test_names'] as $class_name) { - if (in_array($class_name, $all_tests)) { - $test_list[] = $class_name; + $test_list = array(); + foreach ($args['test_names'] as $test_class) { + if (class_exists($test_class)) { + $test_list[] = $test_class; + } + else { + $groups = simpletest_test_get_all(); + $all_classes = array(); + foreach ($groups as $group) { + $all_classes = array_merge($all_classes, array_keys($group)); + } + simpletest_script_print_error('Test class not found: ' . $test_class); + simpletest_script_print_alternatives($test_class, $all_classes, 6); + exit(1); } } } @@ -444,9 +455,12 @@ function simpletest_script_get_test_list() { // Check for valid group names and get all valid classes in group. foreach ($args['test_names'] as $group_name) { if (isset($groups[$group_name])) { - foreach ($groups[$group_name] as $class_name => $info) { - $test_list[] = $class_name; - } + $test_list = array_merge($test_list, array_keys($groups[$group_name])); + } + else { + simpletest_script_print_error('Test group not found: ' . $group_name); + simpletest_script_print_alternatives($group_name, array_keys($groups)); + exit(1); } } } @@ -674,3 +688,37 @@ function simpletest_script_color_code($status) { } return 0; // Default formatting. } + +/** + * Prints alternative test names. + * + * Searches the provided array of string values for close matches based on the + * Levenshtein algorithm. + * + * @see http://php.net/manual/en/function.levenshtein.php + * + * @param string $string + * A string to test. + * @param array $array + * A list of strings to search. + * @param int $degree + * The matching strictness. Higher values return fewer matches. A value of + * 4 means that the function will return strings from $array if the candidate + * string in $array would be identical to $string by changing 1/4 or fewer of + * its characters. + */ +function simpletest_script_print_alternatives($string, $array, $degree = 4) { + $alternatives = array(); + foreach ($array as $item) { + $lev = levenshtein($string, $item); + if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) { + $alternatives[] = $item; + } + } + if (!empty($alternatives)) { + simpletest_script_print(" Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL); + foreach ($alternatives as $alternative) { + simpletest_script_print(" - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL); + } + } +} diff --git a/themes/bartik/bartik.info b/themes/bartik/bartik.info index 343664c8..72ca64a6 100755 --- a/themes/bartik/bartik.info +++ b/themes/bartik/bartik.info @@ -34,8 +34,8 @@ regions[footer] = Footer settings[shortcut_module_link] = 0 -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/themes/bartik/css/style-rtl.css b/themes/bartik/css/style-rtl.css index 3bb02ca2..991edfc4 100755 --- a/themes/bartik/css/style-rtl.css +++ b/themes/bartik/css/style-rtl.css @@ -225,10 +225,10 @@ ul.action-links li a { /* Animated throbber */ html.js input.form-autocomplete { - background-position: 1% 4px; + background-position: 1% center; } html.js input.throbbing { - background-position: 1% -16px; + background-position: 1% center; } /* Comment form */ diff --git a/themes/bartik/css/style.css b/themes/bartik/css/style.css index c40755b1..8426e56c 100755 --- a/themes/bartik/css/style.css +++ b/themes/bartik/css/style.css @@ -704,7 +704,6 @@ ul.links { } .comment .submitted .comment-permalink { font-size: 0.786em; - text-transform: lowercase; } .comment .content { font-size: 0.929em; @@ -1326,14 +1325,6 @@ input.form-button-disabled:active, color: #717171; } -/* Animated throbber */ -html.js input.form-autocomplete { - background-position: 100% 4px; /* LTR */ -} -html.js input.throbbing { - background-position: 100% -16px; /* LTR */ -} - /* Comment form */ .comment-form label { float: left; /* LTR */ diff --git a/themes/garland/garland.info b/themes/garland/garland.info index ee3b8202..b46364aa 100755 --- a/themes/garland/garland.info +++ b/themes/garland/garland.info @@ -7,8 +7,8 @@ stylesheets[all][] = style.css stylesheets[print][] = print.css settings[garland_width] = fluid -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/themes/seven/seven.info b/themes/seven/seven.info index 3cb21af1..1eff1ffe 100755 --- a/themes/seven/seven.info +++ b/themes/seven/seven.info @@ -13,8 +13,8 @@ regions[page_bottom] = Page bottom regions[sidebar_first] = First sidebar regions_hidden[] = sidebar_first -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826" diff --git a/themes/seven/style.css b/themes/seven/style.css index 56a6094f..c52d661d 100755 --- a/themes/seven/style.css +++ b/themes/seven/style.css @@ -709,12 +709,7 @@ select.form-select:focus { color: #000; border-color: #ace; } -html.js input.form-autocomplete { - background-position: 100% 4px; -} -html.js input.throbbing { - background-position: 100% -16px; -} + ul.action-links { margin: 1em 0; padding: 0 20px 0 20px; /* LTR */ diff --git a/themes/stark/stark.info b/themes/stark/stark.info index d8bdd339..ac551a07 100755 --- a/themes/stark/stark.info +++ b/themes/stark/stark.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x stylesheets[all][] = layout.css -; Information added by Drupal.org packaging script on 2014-05-08 -version = "7.28" +; Information added by Drupal.org packaging script on 2015-04-02 +version = "7.36" project = "drupal" -datestamp = "1399522731" +datestamp = "1427943826"