diff -Naur drupal-7.23/.editorconfig drupal-7.66/.editorconfig --- drupal-7.23/.editorconfig 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/.editorconfig 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,14 @@ +# Drupal editor configuration normalization +# @see http://editorconfig.org/ + +# This is the top-most .editorconfig file; do not search in parent directories. +root = true + +# All files. +[*] +end_of_line = LF +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff -Naur drupal-7.23/.htaccess drupal-7.66/.htaccess --- drupal-7.23/.htaccess 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/.htaccess 2019-04-17 22:20:46.000000000 +0200 @@ -3,8 +3,13 @@ # # Protect files and directories from prying eyes. - - Order allow,deny + + + Require all denied + + + Order allow,deny + # Don't show directory listings for URLs which map to a directory. @@ -80,7 +85,7 @@ # If you do not have mod_rewrite installed, you should remove these # directories from your webroot or otherwise protect them from being # downloaded. - RewriteRule "(^|/)\." - [F] + RewriteRule "/\.|^\.(?!well-known/)" - [F] # If your site can be accessed both with and without the 'www.' prefix, you # can use one of the following settings to redirect users to your preferred @@ -141,3 +146,9 @@ + +# Add headers to all responses. + + # Disable content sniffing, since it's an attack vector. + Header always set X-Content-Type-Options nosniff + diff -Naur drupal-7.23/CHANGELOG.txt drupal-7.66/CHANGELOG.txt --- drupal-7.23/CHANGELOG.txt 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/CHANGELOG.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,3 +1,607 @@ +Drupal 7.xx, xxxx-xx-xx (development version) +----------------------- + +Drupal 7.66, 2019-04-17 +----------------------- +- Fixed security issues: + - SA-CORE-2019-006 + +Drupal 7.65, 2019-03-20 +----------------------- +- Fixed security issues: + - SA-CORE-2019-004 + +Drupal 7.64, 2019-02-06 +----------------------- +- [regression] Unset the 'host' header in drupal_http_request() during redirect +- Fixed: 7.x does not have Phar protection and Phar tests are failing on Drupal 7 +- Fixed: Notice: Undefined index: display_field in file_field_widget_value() (line 582 of /module/file/file.field.inc) +- Performance improvement: Registry rebuild should not parse the same file twice in the same request +- Fixed _registry_update() to clear caches after transaction is committed + +Drupal 7.63, 2019-01-16 +----------------------- +- Fixed a fatal error for some Drush users introduced by SA-CORE-2019-002. + +Drupal 7.62, 2019-01-15 +----------------------- +- Fixed security issues: + - SA-CORE-2019-001 + - SA-CORE-2019-002 + +Drupal 7.61, 2018-11-07 +----------------------- +- File upload validation functions and hook_file_validate() implementations are + now always passed the correct file URI. +- The default form cache expiration of 6 hours is now configurable (API + addition: https://www.drupal.org/node/2857751). +- Allowed callers of drupal_http_request() to optionally specify an explicit + Host header. +- Allowed the + character to appear in usernames. +- PHP 7.2: Fixed Archive_Tar incompatibility. +- PHP 7.2: Removed deprecated function each(). +- PHP 7.2: Avoid count() calls on uncountable variables. +- PHP 7.2: Removed deprecated create_function() call. +- PHP 7.2: Make sure variables are arrays in theme_links(). +- Fixed theme-settings.php not being loaded on cached forms +- Fixed problem with IE11 & Chrome(PointerEvents enabled) & some Firefox scroll to the top of the page after dragging the bottom item with jquery 1.5 <-> 1.11 + +Drupal 7.60, 2018-10-18 +------------------------ +- Fixed security issues. See SA-CORE-2018-006. + +Drupal 7.59, 2018-04-25 +----------------------- +- Fixed security issues (remote code execution). See SA-CORE-2018-004. + +Drupal 7.58, 2018-03-28 +----------------------- +- Fixed security issues (remote code execution). See SA-CORE-2018-002. + +Drupal 7.57, 2018-02-21 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-001. + +Drupal 7.56, 2017-06-21 +----------------------- +- Fixed security issues (access bypass). See SA-CORE-2017-003. + +Drupal 7.55, 2017-06-07 +----------------------- +- Fixed incompatibility with PHP versions 7.0.19 and 7.1.5 due to duplicate + DATE_RFC7231 definition. +- Made Drupal core pass all automated tests on PHP 7.1. +- Allowed services such as Let's Encrypt to work with Drupal on Apache, by + making Drupal's .htaccess file allow access to the .well-known directory + defined by RFC 5785. +- Made new Drupal sites work correctly on Apache 2.4 when the mod_access_compat + Apache module is disabled. +- Fixed Drupal's URL-generating functions to always encode '[' and ']' so that + the URLs will pass HTML5 validation. +- Various additional bug fixes. +- Various API documentation improvements. +- Additional automated test coverage. + +Drupal 7.54, 2017-02-01 +----------------------- +- Modules are now able to define theme engines (API addition: + https://www.drupal.org/node/2826480). +- Logging of searches can now be disabled (new option in the administrative + interface). +- Added menu tree render structure to (pre-)process hooks for theme_menu_tree() + (API addition: https://www.drupal.org/node/2827134). +- Added new function for determining whether an HTTPS request is being served + (API addition: https://www.drupal.org/node/2824590). +- Fixed incorrect default value for short and medium date formats on the date + type configuration page. +- File validation error message is now removed after subsequent upload of valid + file. +- Numerous bug fixes. +- Numerous API documentation improvements. +- Additional performance improvements. +- Additional automated test coverage. + +Drupal 7.53, 2016-12-07 +----------------------- +- Fixed drag and drop support on newer Chrome/IE 11+ versions after 7.51 update + when jQuery is updated to 1.7-1.11.0. + +Drupal 7.52, 2016-11-16 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-005. + +Drupal 7.51, 2016-10-05 +----------------------- +- The Update module now also checks for updates to a disabled theme that is + used as an admin theme. +- Exceptions thrown in dblog_watchdog() are now caught and ignored. +- Clarified the warning that appears when modules are missing or have moved. +- Log messages are now XSS filtered on display. +- Draggable tables now work on touch screen devices. +- Added a setting for allowing double underscores in CSS identifiers + (https://www.drupal.org/node/2810369). +- If a user navigates away from a page while an Ajax request is running they + will no longer get an error message saying "An Ajax HTTP request terminated + abnormally". +- The system_region_list() API function now takes an optional third parameter + which allows region name translations to be skipped when they are not needed + (API addition: https://www.drupal.org/node/2810365). +- Numerous performance improvements. +- Numerous bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.50, 2016-07-07 +----------------------- +- Added a new "administer fields" permission for trusted users, which is + required in addition to other permissions to use the field UI + (https://www.drupal.org/node/2483307). +- Added clickjacking protection to Drupal core by setting the X-Frame-Options + header to SAMEORIGIN by default (https://www.drupal.org/node/2735873). +- Added support for full UTF-8 (emojis, Asian symbols, mathematical symbols) on + MySQL and other database drivers when the site and database are configured to + allow it (https://www.drupal.org/node/2761183). +- Improved performance by avoiding a re-scan of directories when a file is + missing; instead, trigger a PHP warning (minor API change: + https://www.drupal.org/node/2581445). +- Made it possible to use any PHP callable in Ajax form callbacks, form API + form-building functions, and form API wrapper callbacks (API addition: + https://www.drupal.org/node/2761169). +- Fixed that following a password reset link while logged in leaves users unable + to change their password (minor user interface change: + https://www.drupal.org/node/2759023). +- Implemented various fixes for automated test failures on PHP 5.4+ and PHP 7. + Drupal core automated tests now pass in these environments. +- Improved support for PHP 7 by fixing various problems. +- Fixed various bugs with PHP 5.5+ imagerotate(), including when incorrect + color indices are passed in. +- Fixed a regression introduced in Drupal 7.43 that allowed files uploaded by + anonymous users to be lost after form validation errors, and that also caused + regressions with certain contributed modules. +- Fixed a regression introduced in Drupal 7.36 which caused the default value + of hidden textarea fields to be ignored. +- Fixed robots.txt to allow search engines to access CSS, JavaScript and image + files. +- Changed wording on the Update Manager settings page to clarify that the + option to check for disabled module updates also applies to uninstalled + modules (administrative-facing translatable string change). +- Changed the help text when editing menu links and configuring URL redirect + actions so that it does not reference "Drupal" or the drupal.org website + (administrative-facing translatable string change). +- Fixed the locale safety check that is used to ensure that translations are + safe to allow for tokens in the href/src attributes of translated strings. +- Fixed that URL generation only works on port 80 when using domain based + language negotation. +- Made method="get" forms work inside the administrative overlay. The fix adds + a new hidden field to these forms when they appear inside the overlay (minor + data structure change). +- Increased maxlength of menu link title input fields in the node form and + menu link form from 128 to 255 characters. +- Removed meaningless post-check=0 and pre-check=0 cache control headers from + Drupal HTTP responses. +- Added a .editorconfig file to auto-configure editors that support it. +- Added --directory option to run-tests.sh for easier test discovery of all + tests within a project. +- Made run-tests.sh exit with a failure code when there are test fails or + problems running the script. +- Fixed that cookies from previous tests are still present when a new test + starts in DrupalWebTestCase. +- Improved performance of queries on the {authmap} database table. +- Fixed handling of missing files and functions inside the registry. +- Fixed Ajax handling for tableselect form elements that use checkboxes. +- Fixed a bug which caused ip_address() to return nothing when the client IP + address and proxy IP address are the same. +- Added a new option to format_xml_elements() to allow for already encoded + values. +- Changed the {history} table's node ID field to be an unsigned integer, to + match the same field in the {node} table and to prevent errors with very + large node IDs. +- Added an explicit page callback to the "admin/people/create" menu item in the + User module (minor data structure change). Previously this automatically + inherited the page callback from the parent "admin/people" menu item, which + broke contributed modules that override the "admin/people" page. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.44, 2016-06-15 +----------------------- +- Fixed security issues (privilege escalation). See SA-CORE-2016-002. + +Drupal 7.43, 2016-02-24 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-001. + +Drupal 7.42, 2016-02-03 +----------------------- +- Stopped invoking hook_flush_caches() on every cron run, since some modules + use that hook for expensive operations that are only needed on cache clears. +- Changed the default .htaccess and web.config to block Composer-related files. +- Added static caching to module_load_include() to improve performance. +- Fixed double-encoding bugs in select field widgets provided by the Options + module. The fix deprecates the 'strip_tags' property on option widgets and + replaces it with a new 'strip_tags_and_unescape' property (minor data + structure change). +- Improved MySQL 5.7 support by changing the MySQL database driver to stop + using the ANSI SQL mode alias, which has different meanings for different + MySQL versions. +- Fixed a regression introduced in Drupal 7.39 which prevented autocomplete + functionality from working on servers that are not configured to + automatically recognize index.php. +- Updated the Archive_Tar PEAR package to the latest 1.4.0 release, to fix bugs + with tar file handling on various operating systems. +- Fixed fatal errors on node preview when a field is displayed in the node + teaser but hidden in the full node view. The fix removes a + field_attach_prepare_view() call from the node_preview() function since it is + redundant with one in the node preview theme layer. +- Improved the description of the "Trimmed" format option on text fields + (translatable string change, and minor UI and data structure change). +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.41, 2015-10-21 +----------------------- +- Fixed security issues (open redirect). See SA-CORE-2015-004. + +Drupal 7.40, 2015-10-14 +----------------------- +- Made Drupal's code for parsing .info files run much faster and use much less + memory. +- Prevented drupal_http_request() from returning an error when it receives a + 201 through 206 HTTP status code. +- Added support for autoloading traits via the registry on sites running PHP + 5.4 or higher. +- Allowed the user-picture.tpl.php theme template to have HTML classes besides + the default "user-picture" class printed in it (markup change). +- Fixed the URL text filter to convert e-mail addresses with plus signs into + mailto: links. +- Added alternate text to file icons displayed by the File module, to improve + accessibility (string change, and minor API addition to theme_file_icon()). +- Changed one-time login link failure messages to be displayed as errors or + warnings as appropriate, rather than as regular status messages (minor UI + change and data structure change). +- Changed the default settings.php configuration to exclude private files from + the "404_fast_paths" behavior. +- Changed the page that displays filter tips for a particular text format, for + example filter/tips/full_html, to return "page not found" or "access denied" + if the format does not exist or the user does not have access to it. This + change adds a new menu item to the Filter module's hook_menu() entry (minor + data structure change). +- Added a new hook, hook_block_cid_parts_alter(), to allow modules to alter the + cache keys used for caching a particular block. +- Made drupal_set_message() display and return messages when "0" is passed in + as the message to set. +- Fixed non-functional "Files displayed by default" setting on file fields. +- The "worker callback" provided in hook_cron_queue_info() and the "finished" + callback specified during batch processing can now be any PHP callable + instead of just functions. +- Prevented drupal_set_time_limit() from decreasing the time limit in the case + where the PHP maximum execution time is already unlimited. +- Changed the default thousand marker for numeric fields from a space ("1 000") + to nothing ("1000") (minor UI change: https://www.drupal.org/node/1388376). +- Prevented malformed theme .info files (without a "name" key) from causing + exceptions during menu rebuilds. If an .info file without a "name" key is + found in a module or theme directory, Drupal will now use the module or + theme's machine name as the display name instead. +- Made the format column in the {date_format_locale} database table + case-sensitive, to match the equivalent column in the {date_formats} table. +- Fixed a bug in the Statistics module that caused JavaScript files attached to + a node while it is being viewed to be omitted from the page. +- Added an optional 'project:' prefix that can be added to dependencies in a + module's .info file to indicate which project the dependency resides in (API + addition: https://www.drupal.org/node/2299747). +- Fixed various bugs that occurred after hooks were invoked early in the Drupal + bootstrap and that caused module_implements() and drupal_alter() to cache an + incomplete set of hook implementations for later use. +- Set the X-Content-Type-Options header to "nosniff" when possible, to prevent + certain web browsers from picking an unsafe MIME type. +- Prevented the database API from executing multiple queries at once on MySQL, + if the site's PHP version is new enough to do so. This is a secondary defense + against SQL injection (API change: https://www.drupal.org/node/2463973). +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused the upgrade + to fail when there were multiple file records pointing to the same file. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.39, 2015-08-19 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-003. + +Drupal 7.38, 2015-06-17 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-002. + +Drupal 7.37, 2015-05-07 +----------------------- +- Fixed a regression in Drupal 7.36 which caused certain kinds of content types + to become disabled if they were defined by a no-longer-enabled module. +- Removed a confusing description regarding automatic time zone detection from + the user account form (minor UI and data structure change). +- Allowed custom HTML tags with a dash in the name to pass through filter_xss() + when specified in the list of allowed tags. +- Allowed hook_field_schema() implementations to specify indexes for fields + based on a fixed-length column prefix (rather than the entire column), as was + already allowed in hook_schema() implementations. +- Fixed PDO exceptions on PostgreSQL when accessing invalid entity URLs. +- Added a sites/all/libraries folder to the codebase, with instructions for + using it. +- Added a description to the "Administer text formats and filters" permission + on the Permissions page (string change). +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +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 + on older browsers (such as Internet Explorer 8 and earlier) when Ajax was + used. +- Increased the timeout used by the Update Manager module when it fetches data + from drupal.org (from 5 seconds to 30 seconds), to work around a problem + which causes incomplete information about security updates to be presented to + site administrators. This fix may lead to a performance slowdown on the + Update Manager administration pages, when installing Drupal distributions, + and (for sites that use the automated cron feature) on occasional page loads + by site visitors. +- Fixed the behavior of the token system's "[node:summary]" token when the body + field does not have a manual summary. +- Changed the behavior of db_query_temporary() so that it works on SELECT + queries even when they have leading comments/whitespace. A side effect of + this fix is that db_query_temporary() will now fail with an error if it is + ever used on non-SELECT queries. +- Added a "node_admin_filter" tag to the database query used to build the list + of nodes on the content administration page, to make it easier to alter. +- Made the cron queue system log any exceptions that are thrown while an item + in the queue is being processed, rather than stopping the entire PHP request. +- Improved screen reader support by adding an aria-live HTML attribute to file + upload fields when there is an error uploading the file (minor markup + change). +- Made the pager on the Tracker module listing pages show the same number of + items as other pagers throughout Drupal core (minor UI change). +- Fixed a bug which caused caches not to be properly cleared when a file entity + was saved or deleted. +- Added several missing countries to the default list returned by + country_get_list() (string change). +- Replaced the term "weight" with "influence" in the content ranking settings + for search, and added help text for administrators (string change). +- Fixed untranslatable text strings in the administrative interface for the + "Crop" effect provided by the Image module (minor string change). +- Fixed a bug in the Taxonomy module update function introduced in Drupal 7.26 + that caused memory and CPU problems on sites with very large numbers of + unpublished nodes. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.27, 2014-04-16 +----------------------- +- Fixed security issues (information disclosure). See SA-CORE-2014-002. + +Drupal 7.26, 2014-01-15 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-001. + +Drupal 7.25, 2014-01-02 +----------------------- +- Fixed a bug in node_save() which prevented the saved node from being updated + in hook_node_insert() and other similar hooks. +- Added a meta tag to install.php to prevent it from being indexed by search + engines even when Drupal is installed in a subfolder (minor markup change). +- Fixed a bug in the database API that caused frequent deadlock errors when + running merge queries on some servers. +- Performance improvement: Prevented block rehashing from writing blocks to the + database on every cache clear and cron run when the blocks have not changed. + This fix results in an extra 'saved' key which is added and set to TRUE for + each block returned by _block_rehash() that actually is saved to the database + (data structure change). +- Added an optional 'skip on cron' parameter to hook_cron_queue_info() to allow + queues to avoid being automatically processed on cron runs (API addition). +- Fixed a bug which caused hook_block_view_MODULE_DELTA_alter() to never be + invoked if the block delta had a hyphen in it. To implement the hook when the + block delta has a hyphen, modules should now replace hyphens with underscores + when constructing the function name for the hook implementation. +- Fixed a bug which caused cached pages to sometimes be sent to the browser + with incorrect compression. The fix adds a new 'page_compressed' key to the + $cache->data array returned by drupal_page_get_cache() (minor data structure + change). +- Fixed broken tests on PHP 5.5. +- Made the File and Image modules more robust when saving entities that have + deleted files attached. The code in file_field_presave() will now remove the + record of the deleted file from the entity before saving (minor data + structure change). +- Standardized menu callback functions throughout Drupal core to return + MENU_NOT_FOUND and MENU_ACCESS_DENIED rather than printing their own "page + not found" or "access denied" pages (minor API change in the return value of + these functions under some circumstances). +- Fixed a bug in which caches were not properly cleared when a node was deleted + via the administrative interface. +- Changed the Bartik theme to render content contained in
,  and
+  similar tags in a larger font size, so it is easier to read.
+- Fixed a bug in the Search module that caused exceptions to be thrown during
+  searches if the server was not configured to represent decimal points as a
+  period.
+- Fixed a regression in the Image module that made image_style_url() not work
+  when a relative path (rather than a complete file URI) was passed to it.
+- Added an optional feature to the Statistics module to allow node views to be
+  tracked by Ajax requests rather than during the server-side generation of the
+  page. This allows the node counter to work on sites that use external page
+  caches (string change and new administrative option:
+  https://drupal.org/node/2164069).
+- Added a link to the drupal.org documentation page for cron to the Cron
+  settings page (string change).
+- Added a 'drupal_anonymous_user_object' variable to allow the anonymous user
+  object returned by drupal_anonymous_user() to be overridden with a classed
+  object (API addition).
+- Changed the database API to allow inserts based on a SELECT * query to work
+  correctly.
+- Changed the database schema of the {file_managed} table to allow Drupal to
+  manage files larger than 4 GB.
+- Changed the File module's hook_field_load() implementation to prevent file
+  entity properties which have the same name as file or image field properties
+  from overwriting the field properties (minor API change).
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.24, 2013-11-20
+-----------------------
+- Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-003.
 
 Drupal 7.23, 2013-08-07
 -----------------------
@@ -250,8 +854,8 @@
 - Numerous API documentation improvements.
 - Additional automated test coverage.
 
-Drupal 7.14 2012-05-02
-----------------------
+Drupal 7.14, 2012-05-02
+-----------------------
 - Fixed "integrity constraint" fatal errors when rebuilding registry.
 - Fixed custom logo and favicon functionality referencing incorrect paths.
 - Fixed DB Case Sensitivity: Allow BINARY attribute in MySQL.
@@ -299,12 +903,12 @@
   - system_update_7061() converts filepaths too aggressively.
   - Trigger upgrade path: Node triggers removed when upgrading to 7-x from 6.25.
 
-Drupal 7.13 2012-05-02
-----------------------
+Drupal 7.13, 2012-05-02
+-----------------------
 - Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-002.
 
 Drupal 7.12, 2012-02-01
-----------------------
+-----------------------
 - Fixed bug preventing custom menus from receiving an active trail.
 - Fixed hook_field_delete() no longer invoked during field_purge_data().
 - Fixed bug causing entity info cache to not be cleared with the rest of caches.
@@ -338,11 +942,11 @@
   cache.
 
 Drupal 7.11, 2012-02-01
-----------------------
+-----------------------
 - Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-001.
 
 Drupal 7.10, 2011-12-05
-----------------------
+-----------------------
 - Fixed Content-Language HTTP header to not cause issues with Drush 5.x.
 - Reduce memory usage of theme registry (performance).
 - Fixed PECL upload progress bar for FileField
@@ -695,7 +1299,7 @@
       requests.
 
 Drupal 6.23-dev, xxxx-xx-xx (development release)
------------------------
+---------------------------
 
 Drupal 6.22, 2011-05-25
 -----------------------
@@ -705,25 +1309,25 @@
 - Fixed a variety of other bugs.
 
 Drupal 6.21, 2011-05-25
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2011-001.
 
 Drupal 6.20, 2010-12-15
-----------------------
+-----------------------
 - Fixed a variety of small bugs, improved code documentation.
 
 Drupal 6.19, 2010-08-11
-----------------------
+-----------------------
 - Fixed a variety of small bugs, improved code documentation.
 
 Drupal 6.18, 2010-08-11
-----------------------
+-----------------------
 - Fixed security issues (OpenID authentication bypass, File download access
   bypass, Comment unpublishing bypass, Actions cross site scripting),
   see SA-CORE-2010-002.
 
 Drupal 6.17, 2010-06-02
-----------------------
+-----------------------
 - Improved PostgreSQL compatibility
 - Better PHP 5.3 and PHP 4 compatibility
 - Better browser compatibility of CSS and JS aggregation
@@ -732,7 +1336,7 @@
 - Fixed a variety of other bugs.
 
 Drupal 6.16, 2010-03-03
-----------------------
+-----------------------
 - Fixed security issues (Installation cross site scripting, Open redirection,
   Locale module cross site scripting, Blocked user session regeneration),
   see SA-CORE-2010-001.
@@ -744,12 +1348,12 @@
 - Fixed a variety of other bugs.
 
 Drupal 6.15, 2009-12-16
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2009-009.
 - Fixed a variety of other bugs.
 
 Drupal 6.14, 2009-09-16
-----------------------
+-----------------------
 - Fixed security issues (OpenID association cross site request forgeries,
   OpenID impersonation and File upload), see SA-CORE-2009-008.
 - Changed the system modules page to not run all cache rebuilds; use the
@@ -758,18 +1362,18 @@
 - Fixed a variety of small bugs.
 
 Drupal 6.13, 2009-07-01
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting, Input format access bypass and
   Password leakage in URL), see SA-CORE-2009-007.
 - Fixed a variety of small bugs.
 
 Drupal 6.12, 2009-05-13
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
 - Fixed a variety of small bugs.
 
 Drupal 6.11, 2009-04-29
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting and limited information
   disclosure), see SA-CORE-2009-005
 - Fixed performance issues with the menu router cache, the update
@@ -777,7 +1381,7 @@
 - Fixed a variety of small bugs.
 
 Drupal 6.10, 2009-02-25
-----------------------
+-----------------------
 - Fixed a security issue, (Local file inclusion on Windows),
   see SA-CORE-2009-003
 - Fixed node_feed() so custom fields can show up in RSS feeds.
@@ -1173,7 +1777,7 @@
 - fixed a security issue (SQL injection), see SA-2007-031
 
 Drupal 4.7.8, 2007-10-17
-----------------------
+------------------------
 - fixed a security issue (HTTP response splitting), see SA-2007-024
 - fixed a security issue (Cross site scripting via uploads), see SA-2007-026
 - fixed a security issue (API handling of unpublished comment), see SA-2007-030
@@ -1286,7 +1890,7 @@
 - Fixed security issue (DoS), see SA-2007-002
 
 Drupal 4.6.10, 2006-10-18
-------------------------
+-------------------------
 - Fixed security issue (XSS), see SA-2006-024
 - Fixed security issue (CSRF), see SA-2006-025
 - Fixed security issue (Form action attribute injection), see SA-2006-026
diff -Naur drupal-7.23/COPYRIGHT.txt drupal-7.66/COPYRIGHT.txt
--- drupal-7.23/COPYRIGHT.txt	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/COPYRIGHT.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,4 +1,4 @@
-All Drupal code is Copyright 2001 - 2012 by the original authors.
+All Drupal code is Copyright 2001 - 2013 by the original authors.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
diff -Naur drupal-7.23/INSTALL.mysql.txt drupal-7.66/INSTALL.mysql.txt
--- drupal-7.23/INSTALL.mysql.txt	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/INSTALL.mysql.txt	2019-04-17 22:20:46.000000000 +0200
@@ -20,18 +20,21 @@
 Again, you will be asked for the 'username' database password. At the MySQL
 prompt, enter the following command:
 
-  GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER
-  ON databasename.*
+  GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
+  CREATE TEMPORARY TABLES ON databasename.*
   TO 'username'@'localhost' IDENTIFIED BY 'password';
 
-where
+where:
 
  'databasename' is the name of your database
- 'username@localhost' is the username of your MySQL account
+ 'username' is the username of your MySQL account
+ 'localhost' is the web server host where Drupal is installed
  'password' is the password required for that username
 
-Note: Unless your database user has all of the privileges listed above, you will
-not be able to run Drupal.
+Note: Unless the database user/host combination for your Drupal installation
+has all of the privileges listed above (except possibly CREATE TEMPORARY TABLES,
+which is currently only used by Drupal core automated tests and some
+contributed modules), you will not be able to install or run Drupal.
 
 If successful, MySQL will reply with:
 
diff -Naur drupal-7.23/INSTALL.txt drupal-7.66/INSTALL.txt
--- drupal-7.23/INSTALL.txt	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/INSTALL.txt	2019-04-17 22:20:46.000000000 +0200
@@ -20,8 +20,10 @@
   - MySQL 5.0.15 (or greater) (http://www.mysql.com/).
   - MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully
     compatible drop-in replacement for MySQL.
+  - Percona Server 5.1.70 (or greater) (http://www.percona.com/). Percona
+    Server is a backwards-compatible replacement for MySQL.
   - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/).
-  - SQLite 3.4.2 (or greater) (http://www.sqlite.org/).
+  - SQLite 3.3.7 (or greater) (http://www.sqlite.org/).
 
 For more detailed information about Drupal requirements, including a list of
 PHP extensions and configurations that are required, see "System requirements"
diff -Naur drupal-7.23/MAINTAINERS.txt drupal-7.66/MAINTAINERS.txt
--- drupal-7.23/MAINTAINERS.txt	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/MAINTAINERS.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,7 +1,8 @@
 
 Drupal core is built and maintained by the Drupal project community. Everyone is
 encouraged to submit issues and changes (patches) to improve Drupal, and to
-contribute in other ways -- see http://drupal.org/contribute to find out how.
+contribute in other ways -- see https://www.drupal.org/contribute to find out
+how.
 
 Branch maintainers
 ------------------
@@ -9,150 +10,155 @@
 The Drupal Core branch maintainers oversee the development of Drupal as a whole.
 The branch maintainers for Drupal 7 are:
 
-- Dries Buytaert 'dries' http://drupal.org/user/1
-- Angela Byron 'webchick' http://drupal.org/user/24967
-- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+- Dries Buytaert 'dries' https://www.drupal.org/u/dries
+- Angela Byron 'webchick' https://www.drupal.org/u/webchick
+- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
+- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
+- Stefan Ruijsenaars 'stefan.r' https://www.drupal.org/u/stefanr-0
+- (provisional) Pol Dellaiera 'Pol' https://www.drupal.org/u/pol
 
 
 Component maintainers
 ---------------------
 
 The Drupal Core component maintainers oversee the development of Drupal
-subsystems. See http://drupal.org/contribute/core-maintainers for more
+subsystems. See https://www.drupal.org/contribute/core-maintainers for more
 information on their responsibilities, and to find out how to become a component
 maintainer. Current component maintainers for Drupal 7:
 
 Ajax system
-- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
-- Earl Miles 'merlinofchaos' http://drupal.org/user/26979
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
 
 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
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
 
 Batch system
-- Yves Chedemois 'yched' http://drupal.org/user/39567
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
 
 Cache system
-- Damien Tournoud 'DamZ' http://drupal.org/user/22211
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
 
 Cron system
-- Károly Négyesi 'chx' http://drupal.org/user/9446
-- Derek Wright 'dww' http://drupal.org/user/46549
+- Derek Wright 'dww' https://www.drupal.org/u/dww
 
 Database system
-- Larry Garfield 'Crell' http://drupal.org/user/26398
+- ?
 
   - MySQL driver
-    - Larry Garfield 'Crell' http://drupal.org/user/26398
-    - David Strauss 'David Strauss' http://drupal.org/user/93254
+    - David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
   - PostgreSQL driver
-    - Damien Tournoud 'DamZ' http://drupal.org/user/22211
-    - Josh Waihi 'fiasco' http://drupal.org/user/188162
+    - Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
+    - Josh Waihi 'fiasco' https://www.drupal.org/u/josh-waihi
 
   - Sqlite driver
-    - Damien Tournoud 'DamZ' http://drupal.org/user/22211
-    - Károly Négyesi 'chx' http://drupal.org/user/9446
+    - Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 Database update system
-- Károly Négyesi 'chx' http://drupal.org/user/9446
-- Ashok Modi 'BTMash' http://drupal.org/user/60422
+- Ashok Modi 'BTMash' https://www.drupal.org/u/btmash
 
 Entity system
-- Wolfgang Ziegler 'fago' http://drupal.org/user/16747
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
-- Franz Heinzmann 'Frando' http://drupal.org/user/21850
+- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
+- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
 
 File system
-- Andrew Morton 'drewish' http://drupal.org/user/34869
-- Aaron Winborn 'aaron' http://drupal.org/user/33420
+- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
+- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
 
 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
-- Franz Heinzmann 'Frando' http://drupal.org/user/21850
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
+- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
 
 Image system
-- Andrew Morton 'drewish' http://drupal.org/user/34869
-- Nathan Haug 'quicksketch' http://drupal.org/user/35821
+- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
+- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
 
 Install system
-- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
 
 JavaScript
-- Théodore Biadala 'nod_' http://drupal.org/user/598310
-- Steve De Jonghe 'seutje' http://drupal.org/user/264148
-- Jesse Renée Beach 'jessebeach' http://drupal.org/user/748566
+- Théodore Biadala 'nod_' https://www.drupal.org/u/nod_
+- Steve De Jonghe 'seutje' https://www.drupal.org/u/seutje
 
 Language system
-- Francesco Placella 'plach' http://drupal.org/user/183211
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Francesco Placella 'plach' https://www.drupal.org/u/plach
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Lock system
-- Damien Tournoud 'DamZ' http://drupal.org/user/22211
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 Mail system
 - ?
 
 Markup
-- Jacine Luisi 'Jacine' http://drupal.org/user/88931
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Jacine Luisi 'Jacine' https://www.drupal.org/u/jacine
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Menu system
-- Peter Wolanin 'pwolanin' http://drupal.org/user/49851
-- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
 
 Path system
-- Dave Reid 'davereid' http://drupal.org/user/53892
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
 
 Render system
-- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
-- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
-- Franz Heinzmann 'Frando' http://drupal.org/user/21850
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
 
 Theme system
-- Earl Miles 'merlinofchaos' http://drupal.org/user/26979
-- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040
-- Joon Park 'dvessel' http://drupal.org/user/56782
-- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
+- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
+- Joon Park 'dvessel' https://www.drupal.org/u/dvessel
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 Token system
-- Dave Reid 'davereid' http://drupal.org/user/53892
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 XML-RPC system
-- Frederic G. Marand 'fgm' http://drupal.org/user/27985
+- Frederic G. Marand 'fgm' https://www.drupal.org/u/fgm
 
 
 Topic coordinators
 ------------------
 
 Accessibility
-- Everett Zufelt 'Everett Zufelt' http://drupal.org/user/406552
-- Brandon Bowersox-Johnson 'bowersox' http://drupal.org/user/186415
+- Everett Zufelt 'Everett Zufelt' https://www.drupal.org/u/everett-zufelt
+- Brandon Bowersox-Johnson 'bowersox' https://www.drupal.org/u/bowersox
 
 Documentation
-- Jennifer Hodgdon 'jhodgdon' http://drupal.org/user/155601
-
-Security
-- Greg Knaddison 'greggles' http://drupal.org/user/36762
+- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
 
 Translations
-- Gerhard Killesreiter 'killes' http://drupal.org/user/83
+- Gerhard Killesreiter 'killes' https://www.drupal.org/u/gerhard-killesreiter
 
 User experience and usability
-- Roy Scholten 'yoroy' http://drupal.org/user/41502
-- Bojhan Somers 'Bojhan' http://drupal.org/user/87969
+- Roy Scholten 'yoroy' https://www.drupal.org/u/yoroy
+- Bojhan Somers 'Bojhan' https://www.drupal.org/u/bojhan
 
 Node Access
-- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
-- Ken Rickard 'agentrickard' http://drupal.org/user/20975
-- Jess Myrbo 'xjm' http://drupal.org/user/65776
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- Ken Rickard 'agentrickard' https://www.drupal.org/u/agentrickard
+
+
+Security team
+-----------------
+
+To report a security issue, see: https://www.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 https://www.drupal.org/security-team for more information.
+The security team lead is:
+
+- Michael Hess 'mlhess' https://www.drupal.org/u/mlhess
+
 
 Module maintainers
 ------------------
@@ -161,143 +167,141 @@
 - ?
 
 Block module
-- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 Blog module
 - ?
 
 Book module
-- Peter Wolanin 'pwolanin' http://drupal.org/user/49851
+- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
 
 Color module
 - ?
 
 Comment module
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
 
 Contact module
-- Dave Reid 'davereid' http://drupal.org/user/53892
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 Contextual module
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Dashboard module
 - ?
 
 Database logging module
-- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063
+- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
 
 Field module
-- Yves Chedemois 'yched' http://drupal.org/user/39567
-- Barry Jaspan 'bjaspan' http://drupal.org/user/46413
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
+- Barry Jaspan 'bjaspan' https://www.drupal.org/u/bjaspan
 
 Field UI module
-- Yves Chedemois 'yched' http://drupal.org/user/39567
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
 
 File module
-- Aaron Winborn 'aaron' http://drupal.org/user/33420
+- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
 
 Filter module
-- Daniel F. Kudwien 'sun' http://drupal.org/user/54136
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Forum module
-- Lee Rowlands 'larowlan' http://drupal.org/user/395439
+- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
 
 Help module
 - ?
 
 Image module
-- Nathan Haug 'quicksketch' http://drupal.org/user/35821
+- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
 
 Locale module
-- Gábor Hojtsy 'Gábor Hojtsy' http://drupal.org/user/4166
+- Gábor Hojtsy 'Gábor Hojtsy' https://www.drupal.org/u/gábor-hojtsy
 
 Menu module
 - ?
 
 Node module
-- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
-- David Strauss 'David Strauss' http://drupal.org/user/93254
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 OpenID module
-- Vojtech Kusy 'wojtha' http://drupal.org/user/56154
-- Christian Schmidt 'c960657' http://drupal.org/user/216078
-- Damien Tournoud 'DamZ' http://drupal.org/user/22211
+- Vojtech Kusy 'wojtha' https://www.drupal.org/u/wojtha
+- Christian Schmidt 'c960657' https://www.drupal.org/u/c960657
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 Overlay module
-- Katherine Senzee 'ksenzee' http://drupal.org/user/139855
+- Katherine Senzee 'ksenzee' https://www.drupal.org/u/ksenzee
 
 Path module
-- Dave Reid 'davereid' http://drupal.org/user/53892
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 PHP module
 - ?
 
 Poll module
-- Andrei Mateescu 'amateescu' http://drupal.org/user/729614
+- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu
 
 Profile module
 - ?
 
 RDF module
-- Stéphane Corlosquet 'scor' http://drupal.org/user/52142
+- Stéphane Corlosquet 'scor' https://www.drupal.org/u/scor
 
 Search module
-- Doug Green 'douggreen' http://drupal.org/user/29191
+- Doug Green 'douggreen' https://www.drupal.org/u/douggreen
 
 Shortcut module
-- David Rothstein 'David_Rothstein' http://drupal.org/user/124982
+- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
 
 Simpletest module
-- Jimmy Berry 'boombatower' http://drupal.org/user/214218
-- Károly Négyesi 'chx' http://drupal.org/user/9446
+- Jimmy Berry 'boombatower' https://www.drupal.org/u/boombatower
 
 Statistics module
-- Tim Millwood 'timmillwood' http://drupal.org/user/227849
+- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood
 
 Syslog module
-- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063
+- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
 
 System module
 - ?
 
 Taxonomy module
-- Jess Myrbo 'xjm' http://drupal.org/user/65776
-- Nathaniel Catchpole 'catch' http://drupal.org/user/35733
-- Benjamin Doherty 'bangpound' http://drupal.org/user/100456
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
+- Benjamin Doherty 'bangpound' https://www.drupal.org/u/bangpound
 
 Toolbar module
 - ?
 
 Tracker module
-- David Strauss 'David Strauss' http://drupal.org/user/93254
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 Translation module
-- Francesco Placella 'plach' http://drupal.org/user/183211
+- Francesco Placella 'plach' https://www.drupal.org/u/plach
 
 Trigger module
 - ?
 
 Update module
-- Derek Wright 'dww' http://drupal.org/user/46549
+- Derek Wright 'dww' https://www.drupal.org/u/dww
 
 User module
-- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
-- David Strauss 'David Strauss' http://drupal.org/user/93254
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 
 Theme maintainers
 -----------------
 
 Bartik theme
-- Jen Simmons 'jensimmons' http://drupal.org/user/140882
-- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393
+- Jen Simmons 'jensimmons' https://www.drupal.org/u/jensimmons
+- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
 
 Garland theme
-- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 Seven theme
-- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393
+- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
 
 Stark theme
-- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
diff -Naur drupal-7.23/README.txt drupal-7.66/README.txt
--- drupal-7.23/README.txt	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/README.txt	2019-04-17 22:20:46.000000000 +0200
@@ -71,12 +71,12 @@
 sites that were installed with that specific profile.
 
 More about installation profiles and distributions:
-* Read about the difference between installation profiles and distributions:
-  http://drupal.org/node/1089736
-* Download contributed installation profiles and distributions:
-  http://drupal.org/project/distributions
-* Develop your own installation profile or distribution:
-  http://drupal.org/developing/distributions
+ * Read about the difference between installation profiles and distributions:
+   http://drupal.org/node/1089736
+ * Download contributed installation profiles and distributions:
+   http://drupal.org/project/distributions
+ * Develop your own installation profile or distribution:
+   http://drupal.org/developing/distributions
 
 APPEARANCE
 ----------
diff -Naur drupal-7.23/UPGRADE.txt drupal-7.66/UPGRADE.txt
--- drupal-7.23/UPGRADE.txt	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/UPGRADE.txt	2019-04-17 22:20:46.000000000 +0200
@@ -64,6 +64,9 @@
    Sometimes an update includes changes to default.settings.php (this will be
    noted in the release notes). If that's the case, follow these steps:
 
+   - Locate your settings.php file in the /sites/* directory. (Typically
+     sites/default.)
+
    - Make a backup copy of your settings.php file, with a different file name.
 
    - Make a copy of the new default.settings.php file, and name the copy
@@ -74,6 +77,13 @@
      database information, and you will also want to copy in any other
      customizations you have added.
 
+   You can find the release notes for your version at
+   https://www.drupal.org/project/drupal. At bottom of the project page under
+   "Downloads" use the link for your version of Drupal to view the release
+   notes. If your version is not listed, use the 'View all releases' link. From
+   this page you can scroll down or use the filter to find your version and its
+   release notes.
+
 4. Download the latest Drupal 7.x release from http://drupal.org to a
    directory outside of your web root. Extract the archive and copy the files
    into your Drupal directory.
diff -Naur drupal-7.23/includes/ajax.inc drupal-7.66/includes/ajax.inc
--- drupal-7.23/includes/ajax.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/includes/ajax.inc	2019-04-17 22:20:46.000000000 +0200
@@ -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);
@@ -230,6 +230,10 @@
  *   functions.
  */
 function ajax_render($commands = array()) {
+  // Although ajax_deliver() does this, some contributed and custom modules
+  // render Ajax responses without using that delivery callback.
+  ajax_set_verification_header();
+
   // Ajax responses aren't rendered with html.tpl.php, so we have to call
   // drupal_get_css() and drupal_get_js() here, in order to have new files added
   // during this request to be loaded by the page. We only want to send back
@@ -276,7 +280,7 @@
 
   $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 +296,7 @@
   $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.
@@ -308,10 +312,11 @@
  * pulls the form info from $_POST.
  *
  * @return
- *   An array containing the $form and $form_state. Use the list() function
- *   to break these apart:
+ *   An array containing the $form, $form_state, $form_id, $form_build_id and an
+ *   initial list of Ajax $commands. Use the list() function to break these
+ *   apart:
  *   @code
- *     list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
+ *     list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
  *   @endcode
  */
 function ajax_get_form() {
@@ -331,6 +336,17 @@
     drupal_exit();
   }
 
+  // When a page level cache is enabled, the form-build id might have been
+  // replaced from within form_get_cache. If this is the case, it is also
+  // necessary to update it in the browser by issuing an appropriate Ajax
+  // command.
+  $commands = array();
+  if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) {
+    // If the form build ID has changed, issue an Ajax command to update it.
+    $commands[] = ajax_command_update_build_id($form);
+    $form_build_id = $form['#build_id'];
+  }
+
   // Since some of the submit handlers are run, redirects need to be disabled.
   $form_state['no_redirect'] = TRUE;
 
@@ -345,7 +361,7 @@
   $form_state['input'] = $_POST;
   $form_id = $form['#form_id'];
 
-  return array($form, $form_state, $form_id, $form_build_id);
+  return array($form, $form_state, $form_id, $form_build_id, $commands);
 }
 
 /**
@@ -366,7 +382,7 @@
  * @see system_menu()
  */
 function ajax_form_callback() {
-  list($form, $form_state) = ajax_get_form();
+  list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
   drupal_process_form($form['#form_id'], $form, $form_state);
 
   // We need to return the part of the form (or some other content) that needs
@@ -378,8 +394,20 @@
   if (!empty($form_state['triggering_element'])) {
     $callback = $form_state['triggering_element']['#ajax']['callback'];
   }
-  if (!empty($callback) && function_exists($callback)) {
-    return $callback($form, $form_state);
+  if (!empty($callback) && is_callable($callback)) {
+    $result = $callback($form, $form_state);
+
+    if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) {
+      // Turn the response into a #type=ajax array if it isn't one already.
+      $result = array(
+        '#type' => 'ajax',
+        '#commands' => ajax_prepare_response($result),
+      );
+    }
+
+    $result['#commands'] = array_merge($commands, $result['#commands']);
+
+    return $result;
   }
 }
 
@@ -463,6 +491,9 @@
     }
   }
 
+  // Let ajax.js know that this response is safe to process.
+  ajax_set_verification_header();
+
   // Print the response.
   $commands = ajax_prepare_response($page_callback_result);
   $json = ajax_render($commands);
@@ -553,6 +584,29 @@
 }
 
 /**
+ * Sets a response header for ajax.js to trust the response body.
+ *
+ * It is not safe to invoke Ajax commands within user-uploaded files, so this
+ * header protects against those being invoked.
+ *
+ * @see Drupal.ajax.options.success()
+ */
+function ajax_set_verification_header() {
+  $added = &drupal_static(__FUNCTION__);
+
+  // User-uploaded files cannot set any response headers, so a custom header is
+  // used to indicate to ajax.js that this response is safe. Note that most
+  // Ajax requests bound using the Form API will be protected by having the URL
+  // flagged as trusted in Drupal.settings, so this header is used only for
+  // things like custom markup that gets Ajax behaviors attached.
+  if (empty($added)) {
+    drupal_add_http_header('X-Drupal-Ajax-Token', '1');
+    // Avoid sending the header twice.
+    $added = TRUE;
+  }
+}
+
+/**
  * Performs end-of-Ajax-request tasks.
  *
  * This function is the equivalent of drupal_page_footer(), but for Ajax
@@ -740,7 +794,12 @@
 
     $element['#attached']['js'][] = array(
       'type' => 'setting',
-      'data' => array('ajax' => array($element['#id'] => $settings)),
+      'data' => array(
+        'ajax' => array($element['#id'] => $settings),
+        'urlIsAjaxTrusted' => array(
+          $settings['url'] => TRUE,
+        ),
+      ),
     );
 
     // Indicate that Ajax processing was successful.
@@ -1210,3 +1269,49 @@
     'selector' => $selector,
   );
 }
+
+/**
+ * Creates a Drupal Ajax 'update_build_id' command.
+ *
+ * This command updates the value of a hidden form_build_id input element on a
+ * form. It requires the form passed in to have keys for both the old build ID
+ * in #build_id_old and the new build ID in #build_id.
+ *
+ * The primary use case for this Ajax command is to serve a new build ID to a
+ * form served from the cache to an anonymous user, preventing one anonymous
+ * user from accessing the form state of another anonymous users on Ajax enabled
+ * forms.
+ *
+ * @param $form
+ *   The form array representing the form whose build ID should be updated.
+ */
+function ajax_command_update_build_id($form) {
+  return array(
+    'command' => 'updateBuildId',
+    'old' => $form['#build_id_old'],
+    '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 -Naur drupal-7.23/includes/batch.inc drupal-7.66/includes/batch.inc
--- drupal-7.23/includes/batch.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/includes/batch.inc	2019-04-17 22:20:46.000000000 +0200
@@ -460,10 +460,10 @@
       if (isset($batch_set['file']) && is_file($batch_set['file'])) {
         include_once DRUPAL_ROOT . '/' . $batch_set['file'];
       }
-      if (function_exists($batch_set['finished'])) {
+      if (is_callable($batch_set['finished'])) {
         $queue = _batch_queue($batch_set);
         $operations = $queue->getAllItems();
-        $batch_set['finished']($batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
+        call_user_func($batch_set['finished'], $batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
       }
     }
   }
diff -Naur drupal-7.23/includes/bootstrap.inc drupal-7.66/includes/bootstrap.inc
--- drupal-7.23/includes/bootstrap.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/includes/bootstrap.inc	2019-04-17 22:20:46.000000000 +0200
@@ -8,7 +8,7 @@
 /**
  * The current system version.
  */
-define('VERSION', '7.23');
+define('VERSION', '7.66');
 
 /**
  * Core API compatibility.
@@ -244,11 +244,25 @@
 /**
  * Regular expression to match PHP function names.
  *
- * @see http://php.net/manual/en/language.functions.php
+ * @see http://php.net/manual/language.functions.php
  */
 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
+ *
+ * This constant was introduced in PHP 7.0.19 and PHP 7.1.5 but needs to be
+ * defined by Drupal for earlier PHP versions.
+ */
+if (!defined('DATE_RFC7231')) {
+  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.
  *
  * This class should be extended by systems that need to cache large amounts
@@ -278,7 +292,7 @@
  * error, and $var will be populated with the contents of $object['foo'], but
  * that data will be passed by value, not reference. For more information on
  * the PHP limitation, see the note in the official PHP documentation at·
- * http://php.net/manual/en/arrayaccess.offsetget.php on
+ * http://php.net/manual/arrayaccess.offsetget.php on
  * ArrayAccess::offsetGet().
  *
  * By default, the class accounts for caches where calling functions might
@@ -520,9 +534,8 @@
  * 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
@@ -683,13 +696,27 @@
   ini_set('session.use_only_cookies', '1');
   ini_set('session.use_trans_sid', '0');
   // Don't send HTTP headers using PHP's session handler.
-  ini_set('session.cache_limiter', 'none');
+  // An empty string is used here to disable the cache limiter.
+  ini_set('session.cache_limiter', '');
   // Use httponly session cookies.
   ini_set('session.cookie_httponly', '1');
 
   // Set sane locale settings, to ensure consistent string, dates, times and
   // numbers handling.
   setlocale(LC_ALL, 'C');
+
+  // PHP's built-in phar:// stream wrapper is not sufficiently secure. Override
+  // it with a more secure one, which requires PHP 5.3.3. For lower versions,
+  // unregister the built-in one without replacing it. Sites needing phar
+  // support for lower PHP versions must implement hook_stream_wrappers() to
+  // register their desired implementation.
+  if (in_array('phar', stream_get_wrappers(), TRUE)) {
+    stream_wrapper_unregister('phar');
+    if (version_compare(PHP_VERSION, '5.3.3', '>=')) {
+      include_once DRUPAL_ROOT . '/includes/file.phar.inc';
+      file_register_phar_wrapper();
+    }
+  }
 }
 
 /**
@@ -699,7 +726,24 @@
  *  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);
+}
+
+/**
+ * Checks whether an HTTPS request is being served.
+ *
+ * @return bool
+ *   TRUE if the request is HTTPS, FALSE otherwise.
+ */
+function drupal_is_https() {
+  return isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
 }
 
 /**
@@ -715,7 +759,7 @@
   if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
     include_once DRUPAL_ROOT . '/' . conf_path() . '/settings.php';
   }
-  $is_https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
+  $is_https = drupal_is_https();
 
   if (isset($base_url)) {
     // Parse fixed base URL from settings.php.
@@ -812,14 +856,21 @@
  * @param $filename
  *   The filename of the item if it is to be set explicitly rather
  *   than by consulting the database.
+ * @param bool $trigger_error
+ *   Whether to trigger an error when a file is missing or has unexpectedly
+ *   moved. This defaults to TRUE, but can be set to FALSE by calling code that
+ *   merely wants to check whether an item exists in the filesystem.
  *
  * @return
  *   The filename of the requested item or NULL if the item is not found.
  */
-function drupal_get_filename($type, $name, $filename = NULL) {
+function drupal_get_filename($type, $name, $filename = NULL, $trigger_error = TRUE) {
+  // The $files static variable will hold the locations of all requested files.
+  // We can be sure that any file listed in this static variable actually
+  // exists as all additions have gone through a file_exists() check.
   // The location of files will not change during the request, so do not use
   // drupal_static().
-  static $files = array(), $dirs = array();
+  static $files = array();
 
   // Profiles are a special case: they have a fixed location and naming.
   if ($type == 'profile') {
@@ -831,64 +882,296 @@
   }
 
   if (!empty($filename) && file_exists($filename)) {
+    // Prime the static cache with the provided filename.
     $files[$type][$name] = $filename;
   }
   elseif (isset($files[$type][$name])) {
-    // nothing
+    // This item had already been found earlier in the request, either through
+    // priming of the static cache (for example, in system_list()), through a
+    // lookup in the {system} table, or through a file scan (cached or not). Do
+    // nothing.
   }
-  // Verify that we have an active database connection, before querying
-  // the database. This is required because this function is called both
-  // before we have a database connection (i.e. during installation) and
-  // when a database connection fails.
   else {
+    // Look for the filename listed in the {system} table. Verify that we have
+    // an active database connection before doing so, since this function is
+    // called both before we have a database connection (i.e. during
+    // installation) and when a database connection fails.
+    $database_unavailable = TRUE;
     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;
         }
+        $database_unavailable = FALSE;
       }
     }
     catch (Exception $e) {
       // The database table may not exist because Drupal is not yet installed,
-      // or the database might be down. We have a fallback for this case so we
-      // hide the error completely.
+      // the database might be down, or we may have done a non-database cache
+      // flush while $conf['page_cache_without_database'] = TRUE and
+      // $conf['page_cache_invoke_hooks'] = TRUE. We have a fallback for these
+      // cases so we hide the error completely.
     }
-    // Fallback to searching the filesystem if the database could not find the
-    // file or the file returned by the database is not found.
+    // Fall back to searching the filesystem if the database could not find the
+    // file or the file does not exist at the path returned by the database.
     if (!isset($files[$type][$name])) {
-      // We have a consistent directory naming: modules, themes...
-      $dir = $type . 's';
-      if ($type == 'theme_engine') {
-        $dir = 'themes/engines';
-        $extension = 'engine';
-      }
-      elseif ($type == 'theme') {
-        $extension = 'info';
-      }
-      else {
-        $extension = $type;
-      }
+      $files[$type][$name] = _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable);
+    }
+  }
 
-      if (!isset($dirs[$dir][$extension])) {
-        $dirs[$dir][$extension] = TRUE;
-        if (!function_exists('drupal_system_listing')) {
-          require_once DRUPAL_ROOT . '/includes/common.inc';
-        }
-        // Scan the appropriate directories for all files with the requested
-        // extension, not just the file we are currently looking for. This
-        // prevents unnecessary scans from being repeated when this function is
-        // called more than once in the same page request.
-        $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0);
-        foreach ($matches as $matched_name => $file) {
-          $files[$type][$matched_name] = $file->uri;
+  if (isset($files[$type][$name])) {
+    return $files[$type][$name];
+  }
+}
+
+/**
+ * Performs a cached file system scan as a fallback when searching for a file.
+ *
+ * This function looks for the requested file by triggering a file scan,
+ * caching the new location if the file has moved and caching the miss
+ * if the file is missing. If a file had been marked as missing in a previous
+ * file scan, or if it has been marked as moved and is still in the last known
+ * location, no new file scan will be performed.
+ *
+ * @param string $type
+ *   The type of the item (theme, theme_engine, module, profile).
+ * @param string $name
+ *   The name of the item for which the filename is requested.
+ * @param bool $trigger_error
+ *   Whether to trigger an error when a file is missing or has unexpectedly
+ *   moved.
+ * @param bool $database_unavailable
+ *   Whether this function is being called because the Drupal database could
+ *   not be queried for the file's location.
+ *
+ * @return
+ *   The filename of the requested item or NULL if the item is not found.
+ *
+ * @see drupal_get_filename()
+ */
+function _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable) {
+  $file_scans = &_drupal_file_scan_cache();
+  $filename = NULL;
+
+  // If the cache indicates that the item is missing, or we can verify that the
+  // item exists in the location the cache says it exists in, use that.
+  if (isset($file_scans[$type][$name]) && ($file_scans[$type][$name] === FALSE || file_exists($file_scans[$type][$name]))) {
+    $filename = $file_scans[$type][$name];
+  }
+  // Otherwise, perform a new file scan to find the item.
+  else {
+    $filename = _drupal_get_filename_perform_file_scan($type, $name);
+    // Update the static cache, and mark the persistent cache for updating at
+    // the end of the page request. See drupal_file_scan_write_cache().
+    $file_scans[$type][$name] = $filename;
+    $file_scans['#write_cache'] = TRUE;
+  }
+
+  // If requested, trigger a user-level warning about the missing or
+  // unexpectedly moved file. If the database was unavailable, do not trigger a
+  // warning in the latter case, though, since if the {system} table could not
+  // be queried there is no way to know if the location found here was
+  // "unexpected" or not.
+  if ($trigger_error) {
+    $error_type = $filename === FALSE ? 'missing' : 'moved';
+    if ($error_type == 'missing' || !$database_unavailable) {
+      _drupal_get_filename_fallback_trigger_error($type, $name, $error_type);
+    }
+  }
+
+  // The cache stores FALSE for files that aren't found (to be able to
+  // distinguish them from files that have not yet been searched for), but
+  // drupal_get_filename() expects NULL for these instead, so convert to NULL
+  // before returning.
+  if ($filename === FALSE) {
+    $filename = NULL;
+  }
+  return $filename;
+}
+
+/**
+ * Returns the current list of cached file system scan results.
+ *
+ * @return
+ *   An associative array tracking the most recent file scan results for all
+ *   files that have had scans performed. The keys are the type and name of the
+ *   item that was searched for, and the values can be either:
+ *   - Boolean FALSE if the item was not found in the file system.
+ *   - A string pointing to the location where the item was found.
+ */
+function &_drupal_file_scan_cache() {
+  $file_scans = &drupal_static(__FUNCTION__, array());
+
+  // The file scan results are stored in a persistent cache (in addition to the
+  // static cache) but because this function can be called before the
+  // persistent cache is available, we must merge any items that were found
+  // earlier in the page request into the results from the persistent cache.
+  if (!isset($file_scans['#cache_merge_done'])) {
+    try {
+      if (function_exists('cache_get')) {
+        $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap');
+        if (!empty($cache->data)) {
+          // File scan results from the current request should take precedence
+          // over the results from the persistent cache, since they are newer.
+          $file_scans = drupal_array_merge_deep($cache->data, $file_scans);
         }
+        // Set a flag to indicate that the persistent cache does not need to be
+        // merged again.
+        $file_scans['#cache_merge_done'] = TRUE;
       }
     }
+    catch (Exception $e) {
+      // Hide the error.
+    }
   }
 
-  if (isset($files[$type][$name])) {
-    return $files[$type][$name];
+  return $file_scans;
+}
+
+/**
+ * Performs a file system scan to search for a system resource.
+ *
+ * @param $type
+ *   The type of the item (theme, theme_engine, module, profile).
+ * @param $name
+ *   The name of the item for which the filename is requested.
+ *
+ * @return
+ *   The filename of the requested item or FALSE if the item is not found.
+ *
+ * @see drupal_get_filename()
+ * @see _drupal_get_filename_fallback()
+ */
+function _drupal_get_filename_perform_file_scan($type, $name) {
+  // The location of files will not change during the request, so do not use
+  // drupal_static().
+  static $dirs = array(), $files = array();
+
+  // We have a consistent directory naming: modules, themes...
+  $dir = $type . 's';
+  if ($type == 'theme_engine') {
+    $dir = 'themes/engines';
+    $extension = 'engine';
+  }
+  elseif ($type == 'theme') {
+    $extension = 'info';
+  }
+  else {
+    $extension = $type;
+  }
+
+  // Check if we had already scanned this directory/extension combination.
+  if (!isset($dirs[$dir][$extension])) {
+    // Log that we have now scanned this directory/extension combination
+    // into a static variable so as to prevent unnecessary file scans.
+    $dirs[$dir][$extension] = TRUE;
+    if (!function_exists('drupal_system_listing')) {
+      require_once DRUPAL_ROOT . '/includes/common.inc';
+    }
+    // Scan the appropriate directories for all files with the requested
+    // extension, not just the file we are currently looking for. This
+    // prevents unnecessary scans from being repeated when this function is
+    // called more than once in the same page request.
+    $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0);
+    foreach ($matches as $matched_name => $file) {
+      // Log the locations found in the file scan into a static variable.
+      $files[$type][$matched_name] = $file->uri;
+    }
+  }
+
+  // Return the results of the file system scan, or FALSE to indicate the file
+  // was not found.
+  return isset($files[$type][$name]) ? $files[$type][$name] : FALSE;
+}
+
+/**
+ * Triggers a user-level warning for missing or unexpectedly moved files.
+ *
+ * @param $type
+ *   The type of the item (theme, theme_engine, module, profile).
+ * @param $name
+ *   The name of the item for which the filename is requested.
+ * @param $error_type
+ *   The type of the error ('missing' or 'moved').
+ *
+ * @see drupal_get_filename()
+ * @see _drupal_get_filename_fallback()
+ */
+function _drupal_get_filename_fallback_trigger_error($type, $name, $error_type) {
+  // Hide messages due to known bugs that will appear on a lot of sites.
+  // @todo Remove this in https://www.drupal.org/node/2383823
+  if (empty($name)) {
+    return;
+  }
+
+  // Make sure we only show any missing or moved file errors only once per
+  // request.
+  static $errors_triggered = array();
+  if (empty($errors_triggered[$type][$name][$error_type])) {
+    // Use _drupal_trigger_error_with_delayed_logging() here since these are
+    // triggered during low-level operations that cannot necessarily be
+    // interrupted by a watchdog() call.
+    if ($error_type == 'missing') {
+      _drupal_trigger_error_with_delayed_logging(format_string('The following @type is missing from the file system: %name. For information about how to fix this, see the documentation page.', array('@type' => $type, '%name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING);
+    }
+    elseif ($error_type == 'moved') {
+      _drupal_trigger_error_with_delayed_logging(format_string('The following @type has moved within the file system: %name. In order to fix this, clear caches or put the @type back in its original location. For more information, see the documentation page.', array('@type' => $type, '%name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING);
+    }
+    $errors_triggered[$type][$name][$error_type] = TRUE;
+  }
+}
+
+/**
+ * Invokes trigger_error() with logging delayed until the end of the request.
+ *
+ * This is an alternative to PHP's trigger_error() function which can be used
+ * during low-level Drupal core operations that need to avoid being interrupted
+ * by a watchdog() call.
+ *
+ * Normally, Drupal's error handler calls watchdog() in response to a
+ * trigger_error() call. However, this invokes hook_watchdog() which can run
+ * arbitrary code. If the trigger_error() happens in the middle of an
+ * operation such as a rebuild operation which should not be interrupted by
+ * arbitrary code, that could potentially break or trigger the rebuild again.
+ * This function protects against that by delaying the watchdog() call until
+ * the end of the current page request.
+ *
+ * This is an internal function which should only be called by low-level Drupal
+ * core functions. It may be removed in a future Drupal 7 release.
+ *
+ * @param string $error_msg
+ *   The error message to trigger. As with trigger_error() itself, this is
+ *   limited to 1024 bytes; additional characters beyond that will be removed.
+ * @param int $error_type
+ *   (optional) The type of error. This should be one of the E_USER family of
+ *   constants. As with trigger_error() itself, this defaults to E_USER_NOTICE
+ *   if not provided.
+ *
+ * @see _drupal_log_error()
+ */
+function _drupal_trigger_error_with_delayed_logging($error_msg, $error_type = E_USER_NOTICE) {
+  $delay_logging = &drupal_static(__FUNCTION__, FALSE);
+  $delay_logging = TRUE;
+  trigger_error($error_msg, $error_type);
+  $delay_logging = FALSE;
+}
+
+/**
+ * Writes the file scan cache to the persistent cache.
+ *
+ * This cache stores all files marked as missing or moved after a file scan
+ * to prevent unnecessary file scans in subsequent requests. This cache is
+ * cleared in system_list_reset() (i.e. after a module/theme rebuild).
+ */
+function drupal_file_scan_write_cache() {
+  // Only write to the persistent cache if requested, and if we know that any
+  // data previously in the cache was successfully loaded and merged in by
+  // _drupal_file_scan_cache().
+  $file_scans = &_drupal_file_scan_cache();
+  if (isset($file_scans['#write_cache']) && isset($file_scans['#cache_merge_done'])) {
+    unset($file_scans['#write_cache']);
+    cache_set('_drupal_file_scan_cache', $file_scans, 'cache_bootstrap');
   }
 }
 
@@ -1039,7 +1322,7 @@
  * Determines the cacheability of the current page.
  *
  * @param $allow_caching
- *   Set to FALSE if you want to prevent this page to get cached.
+ *   Set to FALSE if you want to prevent this page from being cached.
  *
  * @return
  *   TRUE if the current page can be cached, FALSE otherwise.
@@ -1229,23 +1512,10 @@
  * 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()
  */
@@ -1258,9 +1528,11 @@
 
   $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 . '"',
+    'Cache-Control' => 'no-cache, must-revalidate',
+    // Prevent browsers from sniffing a response and picking a MIME type
+    // different from the declared content-type, since that can lead to
+    // XSS and other vulnerabilities.
+    'X-Content-Type-Options' => 'nosniff',
   );
   drupal_send_headers($default_headers);
 }
@@ -1278,7 +1550,7 @@
  */
 function drupal_serve_page_from_cache(stdClass $cache) {
   // Negotiate whether to use compression.
-  $page_compression = variable_get('page_compression', TRUE) && extension_loaded('zlib');
+  $page_compression = !empty($cache->data['page_compressed']);
   $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE;
 
   // Get headers set in hook_boot(). Keys are lower-case.
@@ -1328,7 +1600,7 @@
     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
@@ -1434,6 +1706,23 @@
  * available to code that needs localization. See st() and get_t() for
  * alternatives.
  *
+ * @section sec_context String context
+ * Matching source strings are normally only translated once, and the same
+ * translation is used everywhere that has a matching string. However, in some
+ * cases, a certain English source string needs to have multiple translations.
+ * One example of this is the string "May", which could be used as either a
+ * full month name or a 3-letter abbreviated month. In other languages where
+ * the month name for May has more than 3 letters, you would need to provide
+ * two different translations (one for the full name and one abbreviated), and
+ * the correct form would need to be chosen, depending on how "May" is being
+ * used. To facilitate this, the "May" string should be provided with two
+ * different contexts in the $options parameter when calling t(). For example:
+ * @code
+ * t('May', array(), array('context' => 'Long month name')
+ * t('May', array(), array('context' => 'Abbreviated month name')
+ * @endcode
+ * See https://localize.drupal.org/node/2109 for more information.
+ *
  * @param $string
  *   A string containing the English string to translate.
  * @param $args
@@ -1444,8 +1733,9 @@
  *   An associative array of additional options, with the following elements:
  *   - 'langcode' (defaults to the current language): The language code to
  *     translate to a language other than what is used to display the page.
- *   - 'context' (defaults to the empty context): The context the source string
- *     belongs to.
+ *   - 'context' (defaults to the empty context): A string giving the context
+ *     that the source string belongs to. See @ref sec_context above for more
+ *     information.
  *
  * @return
  *   The translated string.
@@ -1551,12 +1841,13 @@
  * 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
@@ -1641,14 +1932,14 @@
  *   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) {
 
@@ -1774,7 +2065,7 @@
  * @see theme_status_messages()
  */
 function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) {
-  if ($message) {
+  if ($message || $message === '0' || $message === 0) {
     if (!isset($_SESSION['messages'][$type])) {
       $_SESSION['messages'][$type] = array();
     }
@@ -1933,6 +2224,33 @@
 }
 
 /**
+ * Returns a URL-safe, base64 encoded string of highly randomized bytes (over the full 8-bit range).
+ *
+ * @param $byte_count
+ *   The number of random bytes to fetch and base64 encode.
+ *
+ * @return string
+ *   The base64 encoded result will have a length of up to 4 * $byte_count.
+ */
+function drupal_random_key($byte_count = 32) {
+  return drupal_base64_encode(drupal_random_bytes($byte_count));
+}
+
+/**
+ * Returns a URL-safe, base64 encoded version of the supplied string.
+ *
+ * @param $string
+ *   The string to convert to base64.
+ *
+ * @return string
+ */
+function drupal_base64_encode($string) {
+  $data = base64_encode($string);
+  // Modify the output so it's safe to use in URLs.
+  return strtr($data, array('+' => '-', '/' => '_', '=' => ''));
+}
+
+/**
  * Returns a string of highly randomized bytes (over the full 8-bit range).
  *
  * This function is better than simply calling mt_rand() or any other built-in
@@ -1945,38 +2263,34 @@
  */
 function drupal_random_bytes($count)  {
   // $random_state does not use drupal_static as it stores random bytes.
-  static $random_state, $bytes, $php_compatible;
-  // Initialize on the first call. The contents of $_SERVER includes a mix of
-  // user-specific and system information that varies a little with each page.
-  if (!isset($random_state)) {
-    $random_state = print_r($_SERVER, TRUE);
-    if (function_exists('getmypid')) {
-      // Further initialize with the somewhat random PHP process ID.
-      $random_state .= getmypid();
-    }
-    $bytes = '';
-  }
-  if (strlen($bytes) < $count) {
+  static $random_state, $bytes, $has_openssl;
+
+  $missing_bytes = $count - strlen($bytes);
+
+  if ($missing_bytes > 0) {
     // PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
     // locking on Windows and rendered it unusable.
-    if (!isset($php_compatible)) {
-      $php_compatible = version_compare(PHP_VERSION, '5.3.4', '>=');
+    if (!isset($has_openssl)) {
+      $has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=') && function_exists('openssl_random_pseudo_bytes');
+    }
+
+    // openssl_random_pseudo_bytes() will find entropy in a system-dependent
+    // way.
+    if ($has_openssl) {
+      $bytes .= openssl_random_pseudo_bytes($missing_bytes);
     }
-    // /dev/urandom is available on many *nix systems and is considered the
-    // best commonly available pseudo-random source.
-    if ($fh = @fopen('/dev/urandom', 'rb')) {
+
+    // Else, read directly from /dev/urandom, which is available on many *nix
+    // systems and is considered cryptographically secure.
+    elseif ($fh = @fopen('/dev/urandom', 'rb')) {
       // PHP only performs buffered reads, so in reality it will always read
       // at least 4096 bytes. Thus, it costs nothing extra to read and store
       // that much so as to speed any additional invocations.
-      $bytes .= fread($fh, max(4096, $count));
+      $bytes .= fread($fh, max(4096, $missing_bytes));
       fclose($fh);
     }
-    // openssl_random_pseudo_bytes() will find entropy in a system-dependent
-    // way.
-    elseif ($php_compatible && function_exists('openssl_random_pseudo_bytes')) {
-      $bytes .= openssl_random_pseudo_bytes($count - strlen($bytes));
-    }
-    // If /dev/urandom is not available or returns no bytes, this loop will
+
+    // If we couldn't get enough entropy, this simple hash-based PRNG will
     // generate a good set of pseudo-random bytes on any system.
     // Note that it may be important that our $random_state is passed
     // through hash() prior to being rolled into $output, that the two hash()
@@ -1984,9 +2298,23 @@
     // the microtime() - is prepended rather than appended. This is to avoid
     // directly leaking $random_state via the $output stream, which could
     // allow for trivial prediction of further "random" numbers.
-    while (strlen($bytes) < $count) {
-      $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
-      $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
+    if (strlen($bytes) < $count) {
+      // Initialize on the first call. The contents of $_SERVER includes a mix of
+      // user-specific and system information that varies a little with each page.
+      if (!isset($random_state)) {
+        $random_state = print_r($_SERVER, TRUE);
+        if (function_exists('getmypid')) {
+          // Further initialize with the somewhat random PHP process ID.
+          $random_state .= getmypid();
+        }
+        $bytes = '';
+      }
+
+      do {
+        $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
+        $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
+      }
+      while (strlen($bytes) < $count);
     }
   }
   $output = substr($bytes, 0, $count);
@@ -1997,17 +2325,21 @@
 /**
  * Calculates a base-64 encoded, URL-safe sha-256 hmac.
  *
- * @param $data
+ * @param string $data
  *   String to be validated with the hmac.
- * @param $key
+ * @param string $key
  *   A secret string key.
  *
- * @return
+ * @return string
  *   A base-64 encoded sha-256 hmac, with + replaced with -, / with _ and
  *   any = padding characters removed.
  */
 function drupal_hmac_base64($data, $key) {
-  $hmac = base64_encode(hash_hmac('sha256', $data, $key, TRUE));
+  // Casting $data and $key to strings here is necessary to avoid empty string
+  // results of the hash function if they are not scalar values. As this
+  // function is used in security-critical contexts like token validation it is
+  // important that it never returns an empty string.
+  $hmac = base64_encode(hash_hmac('sha256', (string) $data, (string) $key, TRUE));
   // Modify the hmac so it's safe to use in URLs.
   return strtr($hmac, array('+' => '-', '/' => '_', '=' => ''));
 }
@@ -2108,7 +2440,7 @@
  * @return Object - the user object.
  */
 function drupal_anonymous_user() {
-  $user = new stdClass();
+  $user = variable_get('drupal_anonymous_user_object', new stdClass);
   $user->uid = 0;
   $user->hostname = ip_address();
   $user->roles = array();
@@ -2127,7 +2459,7 @@
  *   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:
@@ -2140,11 +2472,11 @@
  *   - 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) {
@@ -2166,12 +2498,13 @@
   // 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) {
@@ -2312,6 +2645,10 @@
   timer_start('page');
   // Initialize the configuration, including variables from settings.php.
   drupal_settings_initialize();
+
+  // Sanitize unsafe keys from the request.
+  require_once DRUPAL_ROOT . '/includes/request-sanitizer.inc';
+  DrupalRequestSanitizer::sanitize();
 }
 
 /**
@@ -2420,6 +2757,9 @@
   // the install or upgrade process.
   spl_autoload_register('drupal_autoload_class');
   spl_autoload_register('drupal_autoload_interface');
+  if (version_compare(PHP_VERSION, '5.4') >= 0) {
+    spl_autoload_register('drupal_autoload_trait');
+  }
 }
 
 /**
@@ -2437,6 +2777,31 @@
   // 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']);
+    }
+    // Use the DrupalRequestSanitizer to ensure that the destination's query
+    // parameters are not dangerous.
+    if (isset($_GET['destination'])) {
+      DrupalRequestSanitizer::cleanDestination();
+    }
+    // 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']);
+    }
+  }
 }
 
 /**
@@ -2459,7 +2824,7 @@
  * @see drupal_bootstrap()
  */
 function drupal_get_bootstrap_phase() {
-  return drupal_bootstrap();
+  return drupal_bootstrap(NULL, FALSE);
 }
 
 /**
@@ -2573,7 +2938,7 @@
  *
  * 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:
@@ -2715,10 +3080,14 @@
 }
 
 /**
- * Returns the default language used on the site
+ * Returns the default language, as an object, or one of its properties.
  *
  * @param $property
- *   Optional property of the language object to return
+ *   (optional) The property of the language object to return.
+ *
+ * @return
+ *   Either the language object for the default language used on the site,
+ *   or the property of that object named in the $property parameter.
  */
 function language_default($property = NULL) {
   $language = variable_get('language_default', (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => ''));
@@ -2870,8 +3239,15 @@
         // Eliminate all trusted IPs.
         $untrusted = array_diff($forwarded, $reverse_proxy_addresses);
 
-        // The right-most IP is the most specific we can trust.
-        $ip_address = array_pop($untrusted);
+        if (!empty($untrusted)) {
+          // The right-most IP is the most specific we can trust.
+          $ip_address = array_pop($untrusted);
+        }
+        else {
+          // All IP addresses in the forwarded array are configured proxy IPs
+          // (and thus trusted). We take the leftmost IP.
+          $ip_address = array_shift($forwarded);
+        }
       }
     }
   }
@@ -2888,7 +3264,9 @@
  * Gets the schema definition of a table, or the whole database schema.
  *
  * The returned schema will include any modifications made by any
- * module that implements hook_schema_alter().
+ * module that implements hook_schema_alter(). To get the schema without
+ * modifications, use drupal_get_schema_unprocessed().
+ *
  *
  * @param $table
  *   The name of the table. If not given, the schema of all tables is returned.
@@ -3044,6 +3422,22 @@
 }
 
 /**
+ * Confirms that a trait is available.
+ *
+ * This function is rarely called directly. Instead, it is registered as an
+ * spl_autoload() handler, and PHP calls it for us when necessary.
+ *
+ * @param string $trait
+ *   The name of the trait to check or load.
+ *
+ * @return bool
+ *   TRUE if the trait is currently available, FALSE otherwise.
+ */
+function drupal_autoload_trait($trait) {
+  return _registry_check_code('trait', $trait);
+}
+
+/**
  * Checks for a resource in the registry.
  *
  * @param $type
@@ -3061,7 +3455,7 @@
 function _registry_check_code($type, $name = NULL) {
   static $lookup_cache, $cache_update_needed;
 
-  if ($type == 'class' && class_exists($name) || $type == 'interface' && interface_exists($name)) {
+  if ($type == 'class' && class_exists($name) || $type == 'interface' && interface_exists($name) || $type == 'trait' && trait_exists($name)) {
     return TRUE;
   }
 
@@ -3094,7 +3488,7 @@
   $cache_key = $type[0] . $name;
   if (isset($lookup_cache[$cache_key])) {
     if ($lookup_cache[$cache_key]) {
-      require_once DRUPAL_ROOT . '/' . $lookup_cache[$cache_key];
+      include_once DRUPAL_ROOT . '/' . $lookup_cache[$cache_key];
     }
     return (bool) $lookup_cache[$cache_key];
   }
@@ -3102,10 +3496,13 @@
   // 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.
@@ -3116,7 +3513,7 @@
   $lookup_cache[$cache_key] = $file;
 
   if ($file) {
-    require_once DRUPAL_ROOT . '/' . $file;
+    include_once DRUPAL_ROOT . '/' . $file;
     return TRUE;
   }
   else {
@@ -3253,8 +3650,8 @@
  * However, the above line of code does not work, because PHP only allows static
  * variables to be initializied by literal values, and does not allow static
  * variables to be assigned to references.
- * - http://php.net/manual/en/language.variables.scope.php#language.variables.scope.static
- * - http://php.net/manual/en/language.variables.scope.php#language.variables.scope.references
+ * - http://php.net/manual/language.variables.scope.php#language.variables.scope.static
+ * - http://php.net/manual/language.variables.scope.php#language.variables.scope.references
  * The example below shows the syntax needed to work around both limitations.
  * For benchmarks and more information, see http://drupal.org/node/619666.
  *
@@ -3279,11 +3676,9 @@
  * @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.
@@ -3328,6 +3723,8 @@
  *
  * @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);
@@ -3401,8 +3798,12 @@
   chdir(DRUPAL_ROOT);
 
   try {
-    while (list($key, $callback) = each($callbacks)) {
+    // Manually iterate over the array instead of using a foreach loop.
+    // A foreach operates on a copy of the array, so any shutdown functions that
+    // were added from other shutdown functions would never be called.
+    while ($callback = current($callbacks)) {
       call_user_func_array($callback['callback'], $callback['arguments']);
+      next($callbacks);
     }
   }
   catch (Exception $exception) {
@@ -3443,3 +3844,34 @@
   // - 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 -Naur drupal-7.23/includes/cache.inc drupal-7.66/includes/cache.inc
--- drupal-7.23/includes/cache.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/includes/cache.inc	2019-04-17 22:20:46.000000000 +0200
@@ -14,6 +14,7 @@
  *
  * @param $bin
  *   The cache bin for which the cache object should be returned.
+ *
  * @return DrupalCacheInterface
  *   The cache object associated with the specified bin.
  *
@@ -98,9 +99,11 @@
  * @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 +122,12 @@
  *     the administrator panel.
  *   - cache_path: Stores the system paths that have an alias.
  * @param $expire
- *   One of the following values:
+ *   (optional) Controls the maximum lifetime of this cache entry. Note that
+ *   caches might be subject to clearing at any time, so this setting does not
+ *   guarantee a minimum lifetime. With this in mind, the cache should not be
+ *   used for data that must be kept during a cache clear, like sessions.
+ *
+ *   Use 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 +262,17 @@
    *   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) Controls the maximum lifetime of this cache entry. Note that
+   *   caches might be subject to clearing at any time, so this setting does not
+   *   guarantee a minimum lifetime. With this in mind, the cache should not be
+   *   used for data that must be kept during a cache clear, like sessions.
+   *
+   *   Use 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 -Naur drupal-7.23/includes/common.inc drupal-7.66/includes/common.inc
--- drupal-7.23/includes/common.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/includes/common.inc	2019-04-17 22:20:46.000000000 +0200
@@ -458,7 +458,7 @@
   $result = array();
   if (!empty($query)) {
     foreach (explode('&', $query) as $param) {
-      $param = explode('=', $param);
+      $param = explode('=', $param, 2);
       $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : '';
     }
   }
@@ -487,7 +487,7 @@
   $params = array();
 
   foreach ($query as $key => $value) {
-    $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
+    $key = $parent ? $parent . rawurlencode('[' . $key . ']') : rawurlencode($key);
 
     // Recurse into children.
     if (is_array($value)) {
@@ -544,37 +544,32 @@
 }
 
 /**
- * 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
- *
- * 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'].
+ * Note that, unlike the RFC, when passed an external URL, this function
+ * groups the scheme, authority, and path together into the path component.
  *
- * @param $url
- *   The URL string to parse, f.e. $_GET['destination'].
+ * @param string $url
+ *   The internal path or external URL string to parse.
  *
- * @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.
+ * @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.
  *
- * @see url()
  * @see drupal_goto()
+ * @see l()
+ * @see url()
+ * @see http://tools.ietf.org/html/rfc3986
+ *
  * @ingroup php_wrappers
  */
 function drupal_parse_url($url) {
@@ -616,8 +611,9 @@
   }
   // The 'q' parameter contains the path of the current page if clean URLs are
   // disabled. It overrides the 'path' of the URL when present, even if clean
-  // URLs are enabled, due to how Apache rewriting rules work.
-  if (isset($options['query']['q'])) {
+  // URLs are enabled, due to how Apache rewriting rules work. The path
+  // parameter must be a string.
+  if (isset($options['query']['q']) && is_string($options['query']['q'])) {
     $options['path'] = $options['query']['q'];
     unset($options['query']['q']);
   }
@@ -693,6 +689,13 @@
     $options['fragment'] = $destination['fragment'];
   }
 
+  // In some cases modules call drupal_goto(current_path()). We need to ensure
+  // that such a redirect is not to an external URL.
+  if ($path === current_path() && empty($options['external']) && url_is_external($path)) {
+    // Force url() to generate a non-external URL.
+    $options['external'] = FALSE;
+  }
+
   drupal_alter('drupal_goto', $path, $options, $http_response_code);
 
   // The 'Location' HTTP header must be absolute.
@@ -758,7 +761,8 @@
  *   - headers: An array containing request headers to send as name/value pairs.
  *   - method: A string containing the request method. Defaults to 'GET'.
  *   - data: A string containing the request body, formatted as
- *     'param=value¶m=value&...'. Defaults to NULL.
+ *     'param=value¶m=value&...'; to generate this, use http_build_query().
+ *     Defaults to NULL.
  *   - max_redirects: An integer representing how many times a redirect
  *     may be followed. Defaults to 3.
  *   - timeout: A float representing the maximum number of seconds the function
@@ -783,6 +787,8 @@
  *     HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
  *     easy access the array keys are returned in lower case.
  *   - data: A string containing the response body that was received.
+ *
+ * @see http_build_query()
  */
 function drupal_http_request($url, array $options = array()) {
   // Allow an alternate HTTP client library to replace Drupal's default
@@ -861,8 +867,10 @@
       // Make the socket connection to a proxy server.
       $socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080);
       // The Host header still needs to match the real request.
-      $options['headers']['Host'] = $uri['host'];
-      $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
+      if (!isset($options['headers']['Host'])) {
+        $options['headers']['Host'] = $uri['host'];
+        $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
+      }
       break;
 
     case 'http':
@@ -872,14 +880,18 @@
       // RFC 2616: "non-standard ports MUST, default ports MAY be included".
       // We don't add the standard port to prevent from breaking rewrite rules
       // checking the host that do not take into account the port number.
-      $options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
+      if (!isset($options['headers']['Host'])) {
+        $options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
+      }
       break;
 
     case 'https':
       // Note: Only works when PHP is compiled with OpenSSL support.
       $port = isset($uri['port']) ? $uri['port'] : 443;
       $socket = 'ssl://' . $uri['host'] . ':' . $port;
-      $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
+      if (!isset($options['headers']['Host'])) {
+        $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
+      }
       break;
 
     default:
@@ -929,7 +941,7 @@
 
   // If the server URL has a user then attempt to use basic authentication.
   if (isset($uri['user'])) {
-    $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ''));
+    $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ':'));
   }
 
   // If the database prefix is being used by SimpleTest to run the tests in a copied
@@ -990,9 +1002,10 @@
   $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();
 
@@ -1061,6 +1074,12 @@
 
   switch ($code) {
     case 200: // OK
+    case 201: // Created
+    case 202: // Accepted
+    case 203: // Non-Authoritative Information
+    case 204: // No Content
+    case 205: // Reset Content
+    case 206: // Partial Content
     case 304: // Not modified
       break;
     case 301: // Moved permanently
@@ -1075,6 +1094,11 @@
       elseif ($options['max_redirects']) {
         // Redirect to the new location.
         $options['max_redirects']--;
+
+        // We need to unset the 'Host' header
+        // as we are redirecting to a new location.
+        unset($options['headers']['Host']);
+
         $result = drupal_http_request($location, $options);
         $result->redirect_code = $code;
       }
@@ -1083,13 +1107,44 @@
       }
       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.
  *
  * @return
@@ -1134,7 +1189,7 @@
  * @param $key
  *   The key for the item within $_FILES.
  *
- * @see http://php.net/manual/en/features.file-upload.php#42280
+ * @see http://php.net/manual/features.file-upload.php#42280
  */
 function _fix_gpc_magic_files(&$item, $key) {
   if ($key != 'tmp_name') {
@@ -1426,7 +1481,6 @@
  *   valid UTF-8.
  *
  * @see drupal_validate_utf8()
- * @ingroup sanitization
  */
 function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) {
   // Only operate on valid UTF-8 strings. This is necessary to prevent cross
@@ -1496,7 +1550,7 @@
     return '<';
   }
 
-  if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|()$%', $string, $matches)) {
+  if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)([^>]*)>?|()$%', $string, $matches)) {
     // Seriously malformed.
     return '';
   }
@@ -1728,9 +1782,15 @@
  *     - 'key': element name
  *     - 'value': element contents
  *     - 'attributes': associative array of element attributes
+ *     - 'encoded': TRUE if 'value' is already encoded
  *
  * In both cases, 'value' can be a simple string, or it can be another array
  * with the same format as $array itself for nesting.
+ *
+ * If 'encoded' is TRUE it is up to the caller to ensure that 'value' is either
+ * entity-encoded or CDATA-escaped. Using this option is not recommended when
+ * working with untrusted user input, since failing to escape the data
+ * correctly has security implications.
  */
 function format_xml_elements($array) {
   $output = '';
@@ -1743,7 +1803,7 @@
         }
 
         if (isset($value['value']) && $value['value'] != '') {
-          $output .= '>' . (is_array($value['value']) ? format_xml_elements($value['value']) : check_plain($value['value'])) . '\n";
+          $output .= '>' . (is_array($value['value']) ? format_xml_elements($value['value']) : (!empty($value['encoded']) ? $value['value'] : check_plain($value['value']))) . '\n";
         }
         else {
           $output .= " />\n";
@@ -1950,7 +2010,7 @@
  *   get interpreted as date format characters.
  * @param $timezone
  *   (optional) Time zone identifier, as described at
- *   http://php.net/manual/en/timezones.php Defaults to the time zone used to
+ *   http://php.net/manual/timezones.php Defaults to the time zone used to
  *   display the page.
  * @param $langcode
  *   (optional) Language code to translate to. Defaults to the language used to
@@ -2188,14 +2248,11 @@
     'prefix' => ''
   );
 
+  // Determine whether this is an external link, but ensure that the current
+  // path is always treated as internal by default (to prevent external link
+  // injection vulnerabilities).
   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.
-    $colonpos = strpos($path, ':');
-    $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path);
+    $options['external'] = $path === $_GET['q'] ? FALSE : url_is_external($path);
   }
 
   // Preserve the original path before altering or aliasing.
@@ -2233,6 +2290,11 @@
     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.
@@ -2260,7 +2322,10 @@
     $language = isset($options['language']) && isset($options['language']->language) ? $options['language']->language : '';
     $alias = drupal_get_path_alias($original_path, $language);
     if ($alias != $original_path) {
-      $path = $alias;
+      // Strip leading slashes from internal path aliases to prevent them
+      // becoming external URLs without protocol. /example.com should not be
+      // turned into //example.com.
+      $path = ltrim($alias, '/');
     }
   }
 
@@ -2310,10 +2375,21 @@
  */
 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;
+  // Some browsers treat \ as / so normalize to forward slashes.
+  $path = str_replace('\\', '/', $path);
+  // If the path starts with 2 slashes then it is always considered an external
+  // URL without an explicit protocol part.
+  return (strpos($path, '//') === 0)
+    // Leading control characters may be ignored or mishandled by browsers, so
+    // assume such a path may lead to an external location. The \p{C} character
+    // class matches all UTF-8 control, unassigned, and private characters.
+    || (preg_match('/^\p{C}/u', $path) !== 0)
+    // 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.
+    || ($colonpos !== FALSE
+      && !preg_match('![/?#]!', substr($path, 0, $colonpos))
+      && drupal_strip_dangerous_protocols($path) == $path);
 }
 
 /**
@@ -2595,6 +2671,15 @@
   global $language;
   drupal_add_http_header('Content-Language', $language->language);
 
+  // By default, do not allow the site to be rendered in an iframe on another
+  // domain, but provide a variable to override this. If the code running for
+  // this page request already set the X-Frame-Options header earlier, don't
+  // overwrite it here.
+  $frame_options = variable_get('x_frame_options', 'SAMEORIGIN');
+  if ($frame_options && is_null(drupal_get_http_header('X-Frame-Options'))) {
+    drupal_add_http_header('X-Frame-Options', $frame_options);
+  }
+
   // Menu status constants are integers; page content is a string or array.
   if (is_int($page_callback_result)) {
     // @todo: Break these up into separate functions?
@@ -2610,7 +2695,10 @@
 
         // 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', ''));
@@ -2639,7 +2727,10 @@
 
         // 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', ''));
@@ -2703,6 +2794,7 @@
   _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE);
   drupal_cache_system_paths();
   module_implements_write_cache();
+  drupal_file_scan_write_cache();
   system_run_automated_cron();
 }
 
@@ -2764,11 +2856,11 @@
  * into script execution a call such as set_time_limit(20) is made, the
  * script will run for a total of 45 seconds before timing out.
  *
- * It also means that it is possible to decrease the total time limit if
- * the sum of the new time limit and the current time spent running the
- * script is inferior to the original time limit. It is inherent to the way
- * set_time_limit() works, it should rather be called with an appropriate
- * value every time you need to allocate a certain amount of time
+ * If the current time limit is not unlimited it is possible to decrease the
+ * total time limit if the sum of the new time limit and the current time spent
+ * running the script is inferior to the original time limit. It is inherent to
+ * the way set_time_limit() works, it should rather be called with an
+ * appropriate value every time you need to allocate a certain amount of time
  * to execute a task than only once at the beginning of the script.
  *
  * Before calling set_time_limit(), we check if this function is available
@@ -2785,7 +2877,11 @@
  */
 function drupal_set_time_limit($time_limit) {
   if (function_exists('set_time_limit')) {
-    @set_time_limit($time_limit);
+    $current = ini_get('max_execution_time');
+    // Do not set time limit if it is currently unlimited.
+    if ($current != 0) {
+      @set_time_limit($time_limit);
+    }
   }
 }
 
@@ -2966,6 +3062,13 @@
  */
 function drupal_add_css($data = NULL, $options = NULL) {
   $css = &drupal_static(__FUNCTION__, array());
+  $count = &drupal_static(__FUNCTION__ . '_count', 0);
+
+  // If the $css variable has been reset with drupal_static_reset(), there is
+  // no longer any CSS being tracked, so set the counter back to 0 also.
+  if (count($css) === 0) {
+    $count = 0;
+  }
 
   // Construct the options, taking the defaults into consideration.
   if (isset($options)) {
@@ -3001,7 +3104,8 @@
     }
 
     // Always add a tiny value to the weight, to conserve the insertion order.
-    $options['weight'] += count($css) / 1000;
+    $options['weight'] += $count / 1000;
+    $count++;
 
     // Add the data to the CSS array depending on the type.
     switch ($options['type']) {
@@ -3448,7 +3552,11 @@
             $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;
@@ -3673,17 +3781,23 @@
   if ($basepath && !file_uri_scheme($file)) {
     $file = $basepath . '/' . $file;
   }
+  // Store the parent base path to restore it later.
+  $parent_base_path = $basepath;
+  // Set the current base path to process possible child imports.
   $basepath = dirname($file);
 
   // Load the CSS stylesheet. We suppress errors because themes may specify
   // stylesheets in their .info file that don't exist in the theme's path,
   // but are merely there to disable certain module CSS files.
+  $content = '';
   if ($contents = @file_get_contents($file)) {
     // Return the processed stylesheet.
-    return drupal_load_stylesheet_content($contents, $_optimize);
+    $content = drupal_load_stylesheet_content($contents, $_optimize);
   }
 
-  return '';
+  // Restore the parent base path as the file and its childen are processed.
+  $basepath = $parent_base_path;
+  return $content;
 }
 
 /**
@@ -3700,7 +3814,7 @@
  */
 function drupal_load_stylesheet_content($contents, $optimize = FALSE) {
   // Remove multiple charset declarations for standards compliance (and fixing Safari problems).
-  $contents = preg_replace('/^@charset\s+[\'"](\S*)\b[\'"];/i', '', $contents);
+  $contents = preg_replace('/^@charset\s+[\'"](\S*?)\b[\'"];/i', '', $contents);
 
   if ($optimize) {
     // Perform some safe CSS optimizations.
@@ -3719,7 +3833,7 @@
     // Remove certain whitespace.
     // There are different conditions for removing leading and trailing
     // whitespace.
-    // @see http://php.net/manual/en/regexp.reference.subpatterns.php
+    // @see http://php.net/manual/regexp.reference.subpatterns.php
     $contents = preg_replace('<
       # Strip leading and trailing whitespace.
         \s*([@{};,])\s*
@@ -3744,7 +3858,7 @@
 
   // Replaces @import commands with the actual stylesheet content.
   // This happens recursively but omits external files.
-  $contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)([^\'"\()]+)[\'"]?\s*\)?\s*;/', '_drupal_load_stylesheet', $contents);
+  $contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)(?!\/\/)([^\'"\()]+)[\'"]?\s*\)?\s*;/', '_drupal_load_stylesheet', $contents);
   return $contents;
 }
 
@@ -3768,7 +3882,7 @@
   // 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);
 }
 
 /**
@@ -3804,6 +3918,21 @@
  *   The cleaned identifier.
  */
 function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '')) {
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['allow_css_double_underscores'] = &drupal_static(__FUNCTION__ . ':allow_css_double_underscores');
+  }
+  $allow_css_double_underscores = &$drupal_static_fast['allow_css_double_underscores'];
+  if (!isset($allow_css_double_underscores)) {
+    $allow_css_double_underscores = variable_get('allow_css_double_underscores', FALSE);
+  }
+
+  // Preserve BEM-style double-underscores depending on custom setting.
+  if ($allow_css_double_underscores) {
+    $filter['__'] = '__';
+  }
+
   // By default, we filter using Drupal's coding standards.
   $identifier = strtr($identifier, $filter);
 
@@ -3833,7 +3962,14 @@
  *   The cleaned class name.
  */
 function drupal_html_class($class) {
-  return drupal_clean_css_identifier(drupal_strtolower($class));
+  // The output of this function will never change, so this uses a normal
+  // static instead of drupal_static().
+  static $classes = array();
+
+  if (!isset($classes[$class])) {
+    $classes[$class] = drupal_clean_css_identifier(drupal_strtolower($class));
+  }
+  return $classes[$class];
 }
 
 /**
@@ -3868,7 +4004,11 @@
   // be merged with content already on the base page. The HTML IDs must be
   // unique for the fully merged content. Therefore, initialize $seen_ids to
   // take into account IDs that are already in use on the base page.
-  $seen_ids_init = &drupal_static(__FUNCTION__ . ':init');
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast['seen_ids_init'])) {
+    $drupal_static_fast['seen_ids_init'] = &drupal_static(__FUNCTION__ . ':init');
+  }
+  $seen_ids_init = &$drupal_static_fast['seen_ids_init'];
   if (!isset($seen_ids_init)) {
     // Ideally, Drupal would provide an API to persist state information about
     // prior page requests in the database, and we'd be able to add this
@@ -3913,7 +4053,10 @@
       }
     }
   }
-  $seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init);
+  if (!isset($drupal_static_fast['seen_ids'])) {
+    $drupal_static_fast['seen_ids'] = &drupal_static(__FUNCTION__, $seen_ids_init);
+  }
+  $seen_ids = &$drupal_static_fast['seen_ids'];
 
   $id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
 
@@ -4097,7 +4240,14 @@
  *       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.
- *   - defer: If set to TRUE, the defer attribute is set on the <script>
+ *   - 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  Lorem ipsum",
+      'variables' => NULL,
+      'severity' => WATCHDOG_NOTICE,
+      'link' => 'foo/bar',
+      'request_uri' => 'http://example.com?dblog=1',
+      'referer' => 'http://example.org?dblog=2',
+      'ip' => '0.0.1.0',
+      'timestamp' => REQUEST_TIME,
+    );
+    dblog_watchdog($log);
+
+    $wid = db_query('SELECT MAX(wid) FROM {watchdog}')->fetchField();
+    $this->drupalGet('admin/reports/event/' . $wid);
+    $this->assertResponse(200);
+    $this->assertNoRaw("");
+    $this->assertRaw("alert('foo'); Lorem ipsum");
+  }
+}
diff -Naur drupal-7.23/modules/field/field.api.php drupal-7.66/modules/field/field.api.php
--- drupal-7.23/modules/field/field.api.php	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/field.api.php	2019-04-17 22:20:46.000000000 +0200
@@ -1,4 +1,8 @@
 type == 'article' && !empty($entity->field_image)) {
+    foreach ($entity->field_image as $langcode => $items) {
+      foreach ($items as $delta => $item) {
+        if (!empty($item['fid']) && empty($item['alt'])) {
+          $errors['field_image'][$langcode][$delta][] = array(
+            'error' => 'field_example_invalid',
+            'message' => t('All images in articles need to have an alternative text set.'),
+          );
+        }
+      }
+    }
+  }
 }
 
 /**
@@ -1510,6 +1550,8 @@
  *   - entity_type: The type of the entity to be displayed.
  *   - entity: The entity with fields to render.
  *   - langcode: The language code $entity has to be displayed in.
+ *
+ * @ingroup field_language
  */
 function hook_field_language_alter(&$display_language, $context) {
   // Do not apply core language fallback rules if they are disabled or if Locale
@@ -1531,6 +1573,8 @@
  *   An associative array containing:
  *   - entity_type: The type of the entity the field is attached to.
  *   - field: A field data structure.
+ *
+ * @ingroup field_language
  */
 function hook_field_available_languages_alter(&$languages, $context) {
   // Add an unavailable language.
@@ -1581,7 +1625,7 @@
  * @param $entity_type
  *   The type of entity; for example, 'node' or 'user'.
  * @param $bundle
- *   The bundle that was just deleted.
+ *   The name of the bundle that was just deleted.
  * @param $instances
  *   An array of all instances that existed for the bundle before it was
  *   deleted.
@@ -1596,7 +1640,7 @@
 }
 
 /**
- * @} End of "defgroup field_attach".
+ * @} End of "addtogroup field_attach".
  */
 
 /**
@@ -1853,7 +1897,7 @@
       $items = (array) $entity->{$field_name}[$langcode];
       $delta_count = 0;
       foreach ($items as $delta => $item) {
-        // We now know we have someting to insert.
+        // We now know we have something to insert.
         $do_insert = TRUE;
         $record = array(
           'entity_type' => $entity_type,
@@ -2257,6 +2301,10 @@
 }
 
 /**
+ * @} End of "addtogroup field_storage
+ */
+
+/**
  * Returns the maximum weight for the entity components handled by the module.
  *
  * Field API takes care of fields and 'extra_fields'. This hook is intended for
@@ -2269,9 +2317,12 @@
  * @param $context
  *   The context for which the maximum weight is requested. Either 'form', or
  *   the name of a view mode.
+ *
  * @return
  *   The maximum weight of the entity's components, or NULL if no components
  *   were found.
+ *
+ * @ingroup field_info
  */
 function hook_field_info_max_weight($entity_type, $bundle, $context) {
   $weights = array();
@@ -2284,6 +2335,11 @@
 }
 
 /**
+ * @addtogroup field_types
+ * @{
+ */
+
+/**
  * Alters the display settings of a field before it gets displayed.
  *
  * Note that instead of hook_field_display_alter(), which is called for all
@@ -2350,6 +2406,10 @@
 }
 
 /**
+ * @} End of "addtogroup field_types
+ */
+
+/**
  * Alters the display settings of pseudo-fields before an entity is displayed.
  *
  * This hook is called once per displayed entity. If the result of the hook
@@ -2364,6 +2424,8 @@
  *   - entity_type: The entity type; e.g., 'node' or 'user'.
  *   - bundle: The bundle name.
  *   - view_mode: The view mode, e.g. 'full', 'teaser'...
+ *
+ * @ingroup field_types
  */
 function hook_field_extra_fields_display_alter(&$displays, $context) {
   if ($context['entity_type'] == 'taxonomy_term' && $context['view_mode'] == 'full') {
@@ -2393,6 +2455,8 @@
  *   - instance: The instance of the field.
  *
  * @see hook_field_widget_properties_alter()
+ *
+ * @ingroup field_widget
  */
 function hook_field_widget_properties_ENTITY_TYPE_alter(&$widget, $context) {
   // Change a widget's type according to the time of day.
@@ -2404,10 +2468,6 @@
 }
 
 /**
- * @} End of "addtogroup field_storage".
- */
-
-/**
  * @addtogroup field_crud
  * @{
  */
@@ -2602,6 +2662,8 @@
  *
  * @param $field
  *   The field being purged.
+ *
+ * @ingroup field_storage
  */
 function hook_field_storage_purge_field($field) {
   $table_name = _field_sql_storage_tablename($field);
@@ -2619,6 +2681,8 @@
  *
  * @param $instance
  *   The instance being purged.
+ *
+ * @ingroup field_storage
  */
 function hook_field_storage_purge_field_instance($instance) {
   db_delete('my_module_field_instance_info')
@@ -2640,6 +2704,8 @@
  *   The (possibly deleted) field whose data is being purged.
  * @param $instance
  *   The deleted field instance whose data is being purged.
+ *
+ * @ingroup field_storage
  */
 function hook_field_storage_purge($entity_type, $entity, $field, $instance) {
   list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
@@ -2679,6 +2745,8 @@
  *
  * @return
  *   TRUE if the operation is allowed, and FALSE if the operation is denied.
+ *
+ * @ingroup field_types
  */
 function hook_field_access($op, $field, $entity_type, $entity, $account) {
   if ($field['field_name'] == 'field_of_interest' && $op == 'edit') {
diff -Naur drupal-7.23/modules/field/field.attach.inc drupal-7.66/modules/field/field.attach.inc
--- drupal-7.23/modules/field/field.attach.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/field.attach.inc	2019-04-17 22:20:46.000000000 +0200
@@ -318,7 +318,7 @@
         // Unless a language suggestion is provided we iterate on all the
         // available languages.
         $available_languages = field_available_languages($entity_type, $field);
-        $language = !empty($options['language'][$id]) ? $options['language'][$id] : $options['language'];
+        $language = is_array($options['language']) && !empty($options['language'][$id]) ? $options['language'][$id] : $options['language'];
         $languages = _field_language_suggestion($available_languages, $language, $field_name);
         foreach ($languages as $langcode) {
           $grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
@@ -976,6 +976,12 @@
 /**
  * Save field data for an existing entity.
  *
+ * When calling this function outside an entity save operation be sure to
+ * clear caches for the entity:
+ * @code
+ * entity_get_controller($entity_type)->resetCache(array($entity_id))
+ * @endcode
+ *
  * @param $entity_type
  *   The type of $entity; e.g. 'node' or 'user'.
  * @param $entity
diff -Naur drupal-7.23/modules/field/field.crud.inc drupal-7.66/modules/field/field.crud.inc
--- drupal-7.23/modules/field/field.crud.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/field.crud.inc	2019-04-17 22:20:46.000000000 +0200
@@ -60,11 +60,11 @@
   }
   // Field type is required.
   if (empty($field['type'])) {
-    throw new FieldException('Attempt to create a field with no type.');
+    throw new FieldException(format_string('Attempt to create field @field_name with no type.', array('@field_name' => $field['field_name'])));
   }
   // Field name cannot contain invalid characters.
   if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $field['field_name'])) {
-    throw new FieldException('Attempt to create a field with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character');
+    throw new FieldException(format_string('Attempt to create a field @field_name with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character', array('@field_name' => $field['field_name'])));
   }
 
   // Field name cannot be longer than 32 characters. We use drupal_strlen()
@@ -189,7 +189,7 @@
   }
 
   // Clear caches
-  field_cache_clear(TRUE);
+  field_cache_clear();
 
   // Invoke external hooks after the cache is cleared for API consistency.
   module_invoke_all('field_create_field', $field);
@@ -288,7 +288,7 @@
   drupal_write_record('field_config', $field, $primary_key);
 
   // Clear caches
-  field_cache_clear(TRUE);
+  field_cache_clear();
 
   // Invoke external hooks after the cache is cleared for API consistency.
   module_invoke_all('field_update_field', $field, $prior_field, $has_data);
@@ -430,7 +430,7 @@
     ->execute();
 
   // Clear the cache.
-  field_cache_clear(TRUE);
+  field_cache_clear();
 
   module_invoke_all('field_delete_field', $field);
 }
@@ -540,9 +540,9 @@
  *   // Fetch an instance info array.
  *   $instance_info = field_info_instance($entity_type, $field_name, $bundle_name);
  *   // Change a single property in the instance definition.
- *   $instance_info['definition']['required'] = TRUE;
+ *   $instance_info['required'] = TRUE;
  *   // Write the changed definition back.
- *   field_update_instance($instance_info['definition']);
+ *   field_update_instance($instance_info);
  *   @endcode
  *
  * @throws FieldException
diff -Naur drupal-7.23/modules/field/field.info drupal-7.66/modules/field/field.info
--- drupal-7.23/modules/field/field.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/field.info	2019-04-17 22:39:36.000000000 +0200
@@ -11,8 +11,7 @@
 required = TRUE
 stylesheets[all][] = theme/field.css
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/field.info.class.inc drupal-7.66/modules/field/field.info.class.inc
--- drupal-7.23/modules/field/field.info.class.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/field.info.class.inc	2019-04-17 22:20:46.000000000 +0200
@@ -146,7 +146,10 @@
 
     // Save in "static" and persistent caches.
     $this->fieldMap = $map;
-    cache_set('field_info:field_map', $map, 'cache_field');
+    if (lock_acquire('field_info:field_map')) {
+      cache_set('field_info:field_map', $map, 'cache_field');
+      lock_release('field_info:field_map');
+    }
 
     return $map;
   }
@@ -174,7 +177,10 @@
       }
 
       // Store in persistent cache.
-      cache_set('field_info:fields', $this->fieldsById, 'cache_field');
+      if (lock_acquire('field_info:fields')) {
+        cache_set('field_info:fields', $this->fieldsById, 'cache_field');
+        lock_release('field_info:fields');
+      }
     }
 
     // Fill the name/ID map.
@@ -231,7 +237,10 @@
         }
 
         // Store in persistent cache.
-        cache_set('field_info:instances', $this->bundleInstances, 'cache_field');
+        if (lock_acquire('field_info:instances')) {
+          cache_set('field_info:instances', $this->bundleInstances, 'cache_field');
+          lock_release('field_info:instances');
+        }
       }
 
       $this->loadedAllInstances = TRUE;
@@ -419,7 +428,11 @@
     foreach ($instances as $instance) {
       $cache['fields'][] = $this->fieldsById[$instance['field_id']];
     }
-    cache_set("field_info:bundle:$entity_type:$bundle", $cache, 'cache_field');
+
+    if (lock_acquire("field_info:bundle:$entity_type:$bundle")) {
+      cache_set("field_info:bundle:$entity_type:$bundle", $cache, 'cache_field');
+      lock_release("field_info:bundle:$entity_type:$bundle");
+    }
 
     return $instances;
   }
@@ -460,7 +473,10 @@
 
     // Store in the 'static' and persistent caches.
     $this->bundleExtraFields[$entity_type][$bundle] = $info;
-    cache_set("field_info:bundle_extra:$entity_type:$bundle", $info, 'cache_field');
+    if (lock_acquire("field_info:bundle_extra:$entity_type:$bundle")) {
+      cache_set("field_info:bundle_extra:$entity_type:$bundle", $info, 'cache_field');
+      lock_release("field_info:bundle_extra:$entity_type:$bundle");
+    }
 
     return $this->bundleExtraFields[$entity_type][$bundle];
   }
@@ -596,10 +612,12 @@
     // Fill in default values.
     $display += array(
       'label' => 'above',
-      'type' => $field_type_info['default_formatter'],
       'settings' => array(),
       'weight' => 0,
     );
+    if (empty($display['type'])) {
+      $display['type'] = $field_type_info['default_formatter'];
+    }
     if ($display['type'] != 'hidden') {
       $formatter_type_info = field_info_formatter_types($display['type']);
       // Fall back to default formatter if formatter type is not available.
diff -Naur drupal-7.23/modules/field/field.info.inc drupal-7.66/modules/field/field.info.inc
--- drupal-7.23/modules/field/field.info.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/field.info.inc	2019-04-17 22:20:46.000000000 +0200
@@ -223,7 +223,11 @@
       }
       drupal_alter('field_storage_info', $info['storage types']);
 
-      cache_set("field_info_types:$langcode", $info, 'cache_field');
+      // Set the cache if we can acquire a lock.
+      if (lock_acquire("field_info_types:$langcode")) {
+        cache_set("field_info_types:$langcode", $info, 'cache_field');
+        lock_release("field_info_types:$langcode");
+      }
     }
   }
 
diff -Naur drupal-7.23/modules/field/field.install drupal-7.66/modules/field/field.install
--- drupal-7.23/modules/field/field.install	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/field.install	2019-04-17 22:20:46.000000000 +0200
@@ -162,6 +162,7 @@
     ),
   );
   $schema['cache_field'] = drupal_get_schema_unprocessed('system', 'cache');
+  $schema['cache_field']['description'] = 'Cache table for the Field module to store already built field information.';
 
   return $schema;
 }
@@ -467,5 +468,26 @@
 }
 
 /**
+ * Grant the new "administer fields" permission to trusted users.
+ */
+function field_update_7004() {
+  // Assign the permission to anyone that already has a trusted core permission
+  // that would have previously let them administer fields on an entity type.
+  $rids = array();
+  $permissions = array(
+    'administer site configuration',
+    'administer content types',
+    'administer users',
+  );
+  foreach ($permissions as $permission) {
+    $rids = array_merge($rids, array_keys(user_roles(FALSE, $permission)));
+  }
+  $rids = array_unique($rids);
+  foreach ($rids as $rid) {
+    _update_7000_user_role_grant_permissions($rid, array('administer fields'), 'field');
+  }
+}
+
+/**
  * @} End of "addtogroup updates-7.x-extra".
  */
diff -Naur drupal-7.23/modules/field/field.module drupal-7.66/modules/field/field.module
--- drupal-7.23/modules/field/field.module	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/field.module	2019-04-17 22:20:46.000000000 +0200
@@ -317,6 +317,21 @@
 }
 
 /**
+ * Implements hook_permission().
+ */
+function field_permission() {
+  return array(
+    'administer fields' => array(
+      'title' => t('Administer fields'),
+      'description' => t('Additional permissions are required based on what the fields are attached to (for example, administer content types to manage fields attached to content).', array(
+        '@url' => '#module-node',
+      )),
+      'restrict access' => TRUE,
+    ),
+  );
+}
+
+/**
  * Implements hook_theme().
  */
 function field_theme() {
@@ -343,17 +358,6 @@
 }
 
 /**
- * Implements hook_modules_uninstalled().
- */
-function field_modules_uninstalled($modules) {
-  module_load_include('inc', 'field', 'field.crud');
-  foreach ($modules as $module) {
-    // TODO D7: field_module_delete is not yet implemented
-    // field_module_delete($module);
-  }
-}
-
-/**
  * Implements hook_system_info_alter().
  *
  * Goes through a list of all modules that provide a field type, and makes them
@@ -819,9 +823,9 @@
  *
  * This function can be used by third-party modules that need to output an
  * isolated field.
- * - Do not use inside node (or other entities) templates, use
+ * - Do not use inside node (or any other entity) templates; use
  *   render($content[FIELD_NAME]) instead.
- * - Do not use to display all fields in an entity, use
+ * - Do not use to display all fields in an entity; use
  *   field_attach_prepare_view() and field_attach_view() instead.
  * - The field_view_value() function can be used to output a single formatted
  *   field value, without label or wrapping field markup.
@@ -905,6 +909,7 @@
       'entity' => $entity,
       'view_mode' => '_custom',
       'display' => $display,
+      'language' => $langcode,
     );
     drupal_alter('field_attach_view', $result, $context);
 
@@ -947,20 +952,30 @@
  */
 function field_has_data($field) {
   $query = new EntityFieldQuery();
-  return (bool) $query
-    ->fieldCondition($field)
+  $query = $query->fieldCondition($field)
     ->range(0, 1)
     ->count()
     // Neutralize the 'entity_field_access' query tag added by
     // field_sql_storage_field_storage_query(). The result cannot depend on the
     // access grants of the current user.
-    ->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT')
+    ->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT');
+
+  return (bool) $query
+    ->execute() || (bool) $query
+    ->age(FIELD_LOAD_REVISION)
     ->execute();
 }
 
 /**
  * Determine whether the user has access to a given field.
  *
+ * This function does not determine whether access is granted to the entity
+ * itself, only the specific field. Callers are responsible for ensuring that
+ * entity access is also respected. For example, when checking field access for
+ * nodes, check node_access() before checking field_access(), and when checking
+ * field access for entities using the Entity API contributed module,
+ * check entity_access() before checking field_access().
+ *
  * @param $op
  *   The operation to be performed. Possible values:
  *   - 'edit'
diff -Naur drupal-7.23/modules/field/modules/field_sql_storage/field_sql_storage.info drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.info
--- drupal-7.23/modules/field/modules/field_sql_storage/field_sql_storage.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.info	2019-04-17 22:39:36.000000000 +0200
@@ -7,8 +7,7 @@
 files[] = field_sql_storage.test
 required = TRUE
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/modules/field_sql_storage/field_sql_storage.module drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.module
--- drupal-7.23/modules/field/modules/field_sql_storage/field_sql_storage.module	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.module	2019-04-17 22:20:46.000000000 +0200
@@ -65,6 +65,49 @@
 }
 
 /**
+ * Generates a table alias for a field data table.
+ *
+ * The table alias is unique for each unique combination of field name
+ * (represented by $tablename), delta_group and language_group.
+ *
+ * @param $tablename
+ *   The name of the data table for this field.
+ * @param $field_key
+ *   The numeric key of this field in this query.
+ * @param $query
+ *   The EntityFieldQuery that is executed.
+ *
+ * @return
+ *   A string containing the generated table alias.
+ */
+function _field_sql_storage_tablealias($tablename, $field_key, EntityFieldQuery $query) {
+  // No conditions present: use a unique alias.
+  if (empty($query->fieldConditions[$field_key])) {
+    return $tablename . $field_key;
+  }
+
+  // Find the delta and language condition values and append them to the alias.
+  $condition = $query->fieldConditions[$field_key];
+  $alias = $tablename;
+  $has_group_conditions = FALSE;
+
+  foreach (array('delta', 'language') as $column) {
+    if (isset($condition[$column . '_group'])) {
+      $alias .= '_' . $column . '_' . $condition[$column . '_group'];
+      $has_group_conditions = TRUE;
+    }
+  }
+
+  // Return the alias when it has delta/language group conditions.
+  if ($has_group_conditions) {
+    return $alias;
+  }
+
+  // Return a unique alias in other cases.
+  return $tablename . $field_key;
+}
+
+/**
  * Generate a column name for a field data table.
  *
  * @param $name
@@ -180,7 +223,17 @@
   foreach ($field['indexes'] as $index_name => $columns) {
     $real_name = _field_sql_storage_indexname($field['field_name'], $index_name);
     foreach ($columns as $column_name) {
-      $current['indexes'][$real_name][] = _field_sql_storage_columnname($field['field_name'], $column_name);
+      // Indexes can be specified as either a column name or an array with
+      // column name and length. Allow for either case.
+      if (is_array($column_name)) {
+        $current['indexes'][$real_name][] = array(
+          _field_sql_storage_columnname($field['field_name'], $column_name[0]),
+          $column_name[1],
+        );
+      }
+      else {
+        $current['indexes'][$real_name][] = _field_sql_storage_columnname($field['field_name'], $column_name);
+      }
     }
   }
 
@@ -289,7 +342,17 @@
         $real_name = _field_sql_storage_indexname($field['field_name'], $name);
         $real_columns = array();
         foreach ($columns as $column_name) {
-          $real_columns[] = _field_sql_storage_columnname($field['field_name'], $column_name);
+          // Indexes can be specified as either a column name or an array with
+          // column name and length. Allow for either case.
+          if (is_array($column_name)) {
+            $real_columns[] = array(
+              _field_sql_storage_columnname($field['field_name'], $column_name[0]),
+              $column_name[1],
+            );
+          }
+          else {
+            $real_columns[] = _field_sql_storage_columnname($field['field_name'], $column_name);
+          }
         }
         db_add_index($table, $real_name, $real_columns);
         db_add_index($revision_table, $real_name, $real_columns);
@@ -422,7 +485,7 @@
       $items = (array) $entity->{$field_name}[$langcode];
       $delta_count = 0;
       foreach ($items as $delta => $item) {
-        // We now know we have someting to insert.
+        // We now know we have something to insert.
         $do_insert = TRUE;
         $record = array(
           'entity_type' => $entity_type,
@@ -504,17 +567,21 @@
     $id_key = 'revision_id';
   }
   $table_aliases = array();
+  $query_tables = NULL;
   // Add tables for the fields used.
   foreach ($query->fields as $key => $field) {
     $tablename = $tablename_function($field);
-    // Every field needs a new table.
-    $table_alias = $tablename . $key;
+    $table_alias = _field_sql_storage_tablealias($tablename, $key, $query);
     $table_aliases[$key] = $table_alias;
     if ($key) {
-      $select_query->join($tablename, $table_alias, "$table_alias.entity_type = $field_base_table.entity_type AND $table_alias.$id_key = $field_base_table.$id_key");
+      if (!isset($query_tables[$table_alias])) {
+        $select_query->join($tablename, $table_alias, "$table_alias.entity_type = $field_base_table.entity_type AND $table_alias.$id_key = $field_base_table.$id_key");
+      }
     }
     else {
       $select_query = db_select($tablename, $table_alias);
+      // Store a reference to the list of joined tables.
+      $query_tables =& $select_query->getTables();
       // Allow queries internal to the Field API to opt out of the access
       // check, for situations where the query's results should not depend on
       // the access grants for the current user.
diff -Naur drupal-7.23/modules/field/modules/field_sql_storage/field_sql_storage.test drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.test
--- drupal-7.23/modules/field/modules/field_sql_storage/field_sql_storage.test	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.test	2019-04-17 22:20:46.000000000 +0200
@@ -355,14 +355,14 @@
     field_attach_insert('test_entity', $entity);
 
     // Add an index
-    $field = array('field_name' => $field_name, 'indexes' => array('value' => array('value')));
+    $field = array('field_name' => $field_name, 'indexes' => array('value' => array(array('value', 255))));
     field_update_field($field);
     foreach ($tables as $table) {
       $this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), format_string("Index on value created in %table", array('%table' => $table)));
     }
 
     // Add a different index, removing the existing custom one.
-    $field = array('field_name' => $field_name, 'indexes' => array('value_format' => array('value', 'format')));
+    $field = array('field_name' => $field_name, 'indexes' => array('value_format' => array(array('value', 127), array('format', 127))));
     field_update_field($field);
     foreach ($tables as $table) {
       $this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value_format"), format_string("Index on value_format created in %table", array('%table' => $table)));
@@ -438,4 +438,149 @@
     $this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema');
     $this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema');
   }
+
+  /**
+   * Test handling multiple conditions on one column of a field.
+   *
+   * Tests both the result and the complexity of the query.
+   */
+  function testFieldSqlStorageMultipleConditionsSameColumn() {
+    $entity = field_test_create_stub_entity(NULL, NULL);
+    $entity->{$this->field_name}[LANGUAGE_NONE][0] = array('value' => 1);
+    field_test_entity_save($entity);
+
+    $entity = field_test_create_stub_entity(NULL, NULL);
+    $entity->{$this->field_name}[LANGUAGE_NONE][0] = array('value' => 2);
+    field_test_entity_save($entity);
+
+    $entity = field_test_create_stub_entity(NULL, NULL);
+    $entity->{$this->field_name}[LANGUAGE_NONE][0] = array('value' => 3);
+    field_test_entity_save($entity);
+
+    $query = new EntityFieldQuery();
+    // This tag causes field_test_query_store_global_test_query_alter() to be
+    // invoked so that the query can be tested.
+    $query->addTag('store_global_test_query');
+    $query->entityCondition('entity_type', 'test_entity');
+    $query->entityCondition('bundle', 'test_bundle');
+    $query->fieldCondition($this->field_name, 'value', 1, '<>', 0, LANGUAGE_NONE);
+    $query->fieldCondition($this->field_name, 'value', 2, '<>', 0, LANGUAGE_NONE);
+    $result = field_sql_storage_field_storage_query($query);
+
+    // Test the results.
+    $this->assertEqual(1, count($result), format_string('One result should be returned, got @count', array('@count' => count($result))));
+
+    // Test the complexity of the query.
+    $query = $GLOBALS['test_query'];
+    $this->assertNotNull($query, 'Precondition: the query should be available');
+    $tables = $query->getTables();
+    $this->assertEqual(1, count($tables), 'The query contains just one table.');
+
+    // Clean up.
+    unset($GLOBALS['test_query']);
+  }
+
+  /**
+   * Test handling multiple conditions on multiple columns of one field.
+   *
+   * Tests both the result and the complexity of the query.
+   */
+  function testFieldSqlStorageMultipleConditionsDifferentColumns() {
+    // Create the multi-column shape field
+    $field_name = strtolower($this->randomName());
+    $field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4);
+    $field = field_create_field($field);
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle'
+    );
+    $instance = field_create_instance($instance);
+
+    $entity = field_test_create_stub_entity(NULL, NULL);
+    $entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'A', 'color' => 'X');
+    field_test_entity_save($entity);
+
+    $entity = field_test_create_stub_entity(NULL, NULL);
+    $entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'B', 'color' => 'X');
+    field_test_entity_save($entity);
+
+    $entity = field_test_create_stub_entity(NULL, NULL);
+    $entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'A', 'color' => 'Y');
+    field_test_entity_save($entity);
+
+    $query = new EntityFieldQuery();
+    // This tag causes field_test_query_store_global_test_query_alter() to be
+    // invoked so that the query can be tested.
+    $query->addTag('store_global_test_query');
+    $query->entityCondition('entity_type', 'test_entity');
+    $query->entityCondition('bundle', 'test_bundle');
+    $query->fieldCondition($field_name, 'shape', 'B', '=', 'something', LANGUAGE_NONE);
+    $query->fieldCondition($field_name, 'color', 'X', '=', 'something', LANGUAGE_NONE);
+    $result = field_sql_storage_field_storage_query($query);
+
+    // Test the results.
+    $this->assertEqual(1, count($result), format_string('One result should be returned, got @count', array('@count' => count($result))));
+
+    // Test the complexity of the query.
+    $query = $GLOBALS['test_query'];
+    $this->assertNotNull($query, 'Precondition: the query should be available');
+    $tables = $query->getTables();
+    $this->assertEqual(1, count($tables), 'The query contains just one table.');
+
+    // Clean up.
+    unset($GLOBALS['test_query']);
+  }
+
+  /**
+   * Test handling multiple conditions on multiple columns of one field for multiple languages.
+   *
+   * Tests both the result and the complexity of the query.
+   */
+  function testFieldSqlStorageMultipleConditionsDifferentColumnsMultipleLanguages() {
+    field_test_entity_info_translatable('test_entity', TRUE);
+
+    // Create the multi-column shape field
+    $field_name = strtolower($this->randomName());
+    $field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4, 'translatable' => TRUE);
+    $field = field_create_field($field);
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle',
+      'settings' => array(
+        // Prevent warning from field_test_field_load().
+        'test_hook_field_load' => FALSE,
+      ),
+    );
+    $instance = field_create_instance($instance);
+
+    $entity = field_test_create_stub_entity(NULL, NULL);
+    $entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'A', 'color' => 'X');
+    $entity->{$field_name}['en'][0] = array('shape' => 'B', 'color' => 'Y');
+    field_test_entity_save($entity);
+    $entity = field_test_entity_test_load($entity->ftid);
+
+    $query = new EntityFieldQuery();
+    // This tag causes field_test_query_store_global_test_query_alter() to be
+    // invoked so that the query can be tested.
+    $query->addTag('store_global_test_query');
+    $query->entityCondition('entity_type', 'test_entity');
+    $query->entityCondition('bundle', 'test_bundle');
+    $query->fieldCondition($field_name, 'color', 'X', '=', NULL, LANGUAGE_NONE);
+    $query->fieldCondition($field_name, 'shape', 'B', '=', NULL, 'en');
+    $result = field_sql_storage_field_storage_query($query);
+
+    // Test the results.
+    $this->assertEqual(1, count($result), format_string('One result should be returned, got @count', array('@count' => count($result))));
+
+    // Test the complexity of the query.
+    $query = $GLOBALS['test_query'];
+    $this->assertNotNull($query, 'Precondition: the query should be available');
+    $tables = $query->getTables();
+    $this->assertEqual(2, count($tables), 'The query contains two tables.');
+
+    // Clean up.
+    unset($GLOBALS['test_query']);
+  }
 }
diff -Naur drupal-7.23/modules/field/modules/list/list.info drupal-7.66/modules/field/modules/list/list.info
--- drupal-7.23/modules/field/modules/list/list.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/modules/list/list.info	2019-04-17 22:39:36.000000000 +0200
@@ -7,8 +7,7 @@
 dependencies[] = options
 files[] = tests/list.test
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/modules/list/list.install drupal-7.66/modules/field/modules/list/list.install
--- drupal-7.23/modules/field/modules/list/list.install	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/list/list.install	2019-04-17 22:20:46.000000000 +0200
@@ -61,7 +61,7 @@
 
       // Additionally, float keys need to be disambiguated ('.5' is '0.5').
       if ($field['type'] == 'list_number' && !empty($allowed_values)) {
-        $keys = array_map(create_function('$a', 'return (string) (float) $a;'), array_keys($allowed_values));
+        $keys = array_map('_list_update_7001_float_string_cast', array_keys($allowed_values));
         $allowed_values = array_combine($keys, array_values($allowed_values));
       }
 
@@ -89,6 +89,13 @@
 }
 
 /**
+ * Helper callback function to cast the array element.
+ */
+function _list_update_7001_float_string_cast($element) {
+  return (string) (float) $element;
+}
+
+/**
  * Helper function for list_update_7001: extract allowed values from a string.
  *
  * This reproduces the parsing logic in use before D7 RC2.
diff -Naur drupal-7.23/modules/field/modules/list/tests/list.test drupal-7.66/modules/field/modules/list/tests/list.test
--- drupal-7.23/modules/field/modules/list/tests/list.test	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/list/tests/list.test	2019-04-17 22:20:46.000000000 +0200
@@ -212,7 +212,7 @@
     parent::setUp('field_test', 'field_ui');
 
     // Create test user.
-    $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy'));
+    $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields'));
     $this->drupalLogin($admin_user);
 
     // Create content type, with underscores.
diff -Naur drupal-7.23/modules/field/modules/list/tests/list_test.info drupal-7.66/modules/field/modules/list/tests/list_test.info
--- drupal-7.23/modules/field/modules/list/tests/list_test.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/modules/list/tests/list_test.info	2019-04-17 22:39:36.000000000 +0200
@@ -5,8 +5,7 @@
 version = VERSION
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/modules/number/number.info drupal-7.66/modules/field/modules/number/number.info
--- drupal-7.23/modules/field/modules/number/number.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/modules/number/number.info	2019-04-17 22:39:36.000000000 +0200
@@ -6,8 +6,7 @@
 dependencies[] = field
 files[] = number.test
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/modules/number/number.module drupal-7.66/modules/field/modules/number/number.module
--- drupal-7.23/modules/field/modules/number/number.module	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/number/number.module	2019-04-17 22:20:46.000000000 +0200
@@ -164,6 +164,15 @@
       }
     }
   }
+  if ($field['type'] == 'number_float') {
+    // Remove the decimal point from float values with decimal
+    // point but no decimal numbers.
+    foreach ($items as $delta => $item) {
+      if (isset($item['value'])) {
+        $items[$delta]['value'] = floatval($item['value']);
+      }
+    }
+  }
 }
 
 /**
@@ -188,7 +197,7 @@
       'label' => t('Default'),
       'field types' => array('number_integer'),
       'settings' =>  array(
-        'thousand_separator' => ' ',
+        'thousand_separator' => '',
         // The 'decimal_separator' and 'scale' settings are not configurable
         // through the UI, and will therefore keep their default values. They
         // are only present so that the 'number_integer' and 'number_decimal'
@@ -202,7 +211,7 @@
       'label' => t('Default'),
       'field types' => array('number_decimal', 'number_float'),
       'settings' =>  array(
-        'thousand_separator' => ' ',
+        'thousand_separator' => '',
         'decimal_separator' => '.',
         'scale' => 2,
         'prefix_suffix' => TRUE,
@@ -222,6 +231,8 @@
   $display = $instance['display'][$view_mode];
   $settings = $display['settings'];
 
+  $element = array();
+
   if ($display['type'] == 'number_decimal' || $display['type'] == 'number_integer') {
     $options = array(
       ''  => t(''),
diff -Naur drupal-7.23/modules/field/modules/number/number.test drupal-7.66/modules/field/modules/number/number.test
--- drupal-7.23/modules/field/modules/number/number.test	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/number/number.test	2019-04-17 22:20:46.000000000 +0200
@@ -23,7 +23,7 @@
 
   function setUp() {
     parent::setUp('field_test');
-    $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer content types'));
+    $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer content types', 'administer fields'));
     $this->drupalLogin($this->web_user);
   }
 
@@ -69,7 +69,7 @@
     preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
     $id = $match[1];
     $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
-    $this->assertRaw(round($value, 2), 'Value is displayed.');
+    $this->assertRaw($value, 'Value is displayed.');
 
     // Try to create entries with more than one decimal separator; assert fail.
     $wrong_entries = array(
@@ -152,4 +152,50 @@
     );
     $this->drupalPost(NULL, $edit, t('Save'));
   }
+
+  /**
+   * Test number_float field.
+   */
+  function testNumberFloatField() {
+    $this->field = array(
+      'field_name' => drupal_strtolower($this->randomName()),
+      'type' => 'number_float',
+      'settings' => array(
+        'precision' => 8, 'scale' => 4, 'decimal_separator' => '.',
+      )
+    );
+    field_create_field($this->field);
+    $this->instance = array(
+      'field_name' => $this->field['field_name'],
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle',
+      'widget' => array(
+        'type' => 'number',
+      ),
+      'display' => array(
+        'default' => array(
+          'type' => 'number_float',
+        ),
+      ),
+    );
+    field_create_instance($this->instance);
+
+    $langcode = LANGUAGE_NONE;
+    $value = array(
+      '9.' => '9',
+      '.' => '0',
+      '123.55' => '123.55',
+      '.55' => '0.55',
+      '-0.55' => '-0.55',
+    );
+    foreach($value as $key => $value) {
+      $edit = array(
+        "{$this->field['field_name']}[$langcode][0][value]" => $key,
+      );
+      $this->drupalPost('test-entity/add/test-bundle', $edit, t('Save'));
+      $this->assertNoText("PDOException");
+      $this->assertRaw($value, 'Correct value is displayed.');
+    }
+  }
+
 }
diff -Naur drupal-7.23/modules/field/modules/options/options.info drupal-7.66/modules/field/modules/options/options.info
--- drupal-7.23/modules/field/modules/options/options.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/modules/options/options.info	2019-04-17 22:39:36.000000000 +0200
@@ -6,8 +6,7 @@
 dependencies[] = field
 files[] = options.test
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/modules/options/options.module drupal-7.66/modules/field/modules/options/options.module
--- drupal-7.23/modules/field/modules/options/options.module	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/options/options.module	2019-04-17 22:20:46.000000000 +0200
@@ -185,6 +185,7 @@
   $base = array(
     'filter_xss' => FALSE,
     'strip_tags' => FALSE,
+    'strip_tags_and_unescape' => FALSE,
     'empty_option' => FALSE,
     'optgroups' => FALSE,
   );
@@ -195,7 +196,7 @@
     case 'select':
       $properties = array(
         // Select boxes do not support any HTML tag.
-        'strip_tags' => TRUE,
+        'strip_tags_and_unescape' => TRUE,
         'optgroups' => TRUE,
       );
       if ($multiple) {
@@ -271,9 +272,16 @@
       _options_prepare_options($options[$value], $properties);
     }
     else {
+      // The 'strip_tags' option is deprecated. Use 'strip_tags_and_unescape'
+      // when plain text is required (and where the output will be run through
+      // check_plain() before being inserted back into HTML) or 'filter_xss'
+      // when HTML is required.
       if ($properties['strip_tags']) {
         $options[$value] = strip_tags($label);
       }
+      if ($properties['strip_tags_and_unescape']) {
+        $options[$value] = decode_entities(strip_tags($label));
+      }
       if ($properties['filter_xss']) {
         $options[$value] = field_filter_xss($label);
       }
diff -Naur drupal-7.23/modules/field/modules/options/options.test drupal-7.66/modules/field/modules/options/options.test
--- drupal-7.23/modules/field/modules/options/options.test	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/options/options.test	2019-04-17 22:20:46.000000000 +0200
@@ -23,8 +23,15 @@
       'type' => 'list_integer',
       'cardinality' => 1,
       'settings' => array(
-        // Make sure that 0 works as an option.
-        'allowed_values' => array(0 => 'Zero', 1 => 'One', 2 => 'Some  & unescaped markup'),
+        'allowed_values' => array(
+          // Make sure that 0 works as an option.
+          0 => 'Zero',
+          1 => 'One',
+          // Make sure that option text is properly sanitized.
+          2 => 'Some  & unescaped markup',
+          // Make sure that HTML entities in option text are not double-encoded.
+          3 => 'Some HTML encoded markup with < & >',
+        ),
       ),
     );
     $this->card_1 = field_create_field($this->card_1);
@@ -35,8 +42,13 @@
       'type' => 'list_integer',
       'cardinality' => 2,
       'settings' => array(
-        // Make sure that 0 works as an option.
-        'allowed_values' => array(0 => 'Zero', 1 => 'One', 2 => 'Some  & unescaped markup'),
+        'allowed_values' => array(
+          // Make sure that 0 works as an option.
+          0 => 'Zero',
+          1 => 'One',
+          // Make sure that option text is properly sanitized.
+          2 => 'Some  & unescaped markup',
+        ),
       ),
     );
     $this->card_2 = field_create_field($this->card_2);
@@ -47,14 +59,18 @@
       'type' => 'list_boolean',
       'cardinality' => 1,
       'settings' => array(
-        // Make sure that 0 works as a 'on' value'.
-        'allowed_values' => array(1 => 'Zero', 0 => 'Some  & unescaped markup'),
+        'allowed_values' => array(
+          // Make sure that 1 works as a 'on' value'.
+          1 => 'Zero',
+          // Make sure that option text is properly sanitized.
+          0 => 'Some  & unescaped markup',
+        ),
       ),
     );
     $this->bool = field_create_field($this->bool);
 
     // Create a web user.
-    $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
+    $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer fields'));
     $this->drupalLogin($this->web_user);
   }
 
@@ -233,6 +249,7 @@
     $this->assertNoOptionSelected("edit-card-1-$langcode", 1);
     $this->assertNoOptionSelected("edit-card-1-$langcode", 2);
     $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.');
+    $this->assertRaw('Some HTML encoded markup with < & >', 'HTML entities in option text were properly handled and not double-encoded');
 
     // Submit form: select invalid 'none' option.
     $edit = array("card_1[$langcode]" => '_none');
@@ -359,7 +376,7 @@
 
     // Test the 'None' option.
 
-    // Check that the 'none' option has no efect if actual options are selected
+    // Check that the 'none' option has no effect if actual options are selected
     // as well.
     $edit = array("card_2[$langcode][]" => array('_none' => '_none', 0 => 0));
     $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save'));
@@ -459,7 +476,7 @@
     $this->assertNoFieldChecked("edit-bool-$langcode");
 
     // Create admin user.
-    $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy'));
+    $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields'));
     $this->drupalLogin($admin_user);
 
     // Create a test field instance.
diff -Naur drupal-7.23/modules/field/modules/text/text.info drupal-7.66/modules/field/modules/text/text.info
--- drupal-7.23/modules/field/modules/text/text.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/modules/text/text.info	2019-04-17 22:39:36.000000000 +0200
@@ -7,8 +7,7 @@
 files[] = text.test
 required = TRUE
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/modules/text/text.js drupal-7.66/modules/field/modules/text/text.js
--- drupal-7.23/modules/field/modules/text/text.js	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/text/text.js	2019-04-17 22:20:46.000000000 +0200
@@ -12,9 +12,9 @@
 
       $summaries.once('text-summary-wrapper').each(function(index) {
         var $summary = $(this);
-        var $summaryLabel = $summary.find('label');
+        var $summaryLabel = $summary.find('label').first();
         var $full = $widget.find('.text-full').eq(index).closest('.form-item');
-        var $fullLabel = $full.find('label');
+        var $fullLabel = $full.find('label').first();
 
         // Create a placeholder label when the field cardinality is
         // unlimited or greater than 1.
@@ -23,24 +23,28 @@
         }
 
         // Setup the edit/hide summary link.
-        var $link = $('(' + Drupal.t('Hide summary') + ')').toggle(
-          function () {
+        var $link = $('(' + Drupal.t('Hide summary') + ')');
+        var $a = $link.find('a');
+        var toggleClick = true;
+        $link.bind('click', function (e) {
+          if (toggleClick) {
             $summary.hide();
-            $(this).find('a').html(Drupal.t('Edit summary')).end().appendTo($fullLabel);
-            return false;
-          },
-          function () {
+            $a.html(Drupal.t('Edit summary'));
+            $link.appendTo($fullLabel);
+          }
+          else {
             $summary.show();
-            $(this).find('a').html(Drupal.t('Hide summary')).end().appendTo($summaryLabel);
-            return false;
+            $a.html(Drupal.t('Hide summary'));
+            $link.appendTo($summaryLabel);
           }
-        ).appendTo($summaryLabel);
+          toggleClick = !toggleClick;
+          return false;
+        }).appendTo($summaryLabel);
 
         // If no summary is set, hide the summary field.
         if ($(this).find('.text-summary').val() == '') {
           $link.click();
         }
-        return;
       });
     });
   }
diff -Naur drupal-7.23/modules/field/modules/text/text.module drupal-7.66/modules/field/modules/text/text.module
--- drupal-7.23/modules/field/modules/text/text.module	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/text/text.module	2019-04-17 22:20:46.000000000 +0200
@@ -223,11 +223,13 @@
 
   if (strpos($display['type'], '_trimmed') !== FALSE) {
     $element['trim_length'] = array(
-      '#title' => t('Trim length'),
+      '#title' => t('Trimmed limit'),
       '#type' => 'textfield',
+      '#field_suffix' => t('characters'),
       '#size' => 10,
       '#default_value' => $settings['trim_length'],
       '#element_validate' => array('element_validate_integer_positive'),
+      '#description' => t('If the summary is not set, the trimmed %label field will be shorter than this character limit.', array('%label' => $instance['label'])),
       '#required' => TRUE,
     );
   }
@@ -245,7 +247,7 @@
   $summary = '';
 
   if (strpos($display['type'], '_trimmed') !== FALSE) {
-    $summary = t('Trim length') . ': ' . $settings['trim_length'];
+    $summary = t('Trimmed limit: @trim_length characters', array('@trim_length' => $settings['trim_length']));
   }
 
   return $summary;
diff -Naur drupal-7.23/modules/field/modules/text/text.test drupal-7.66/modules/field/modules/text/text.test
--- drupal-7.23/modules/field/modules/text/text.test	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/modules/text/text.test	2019-04-17 22:20:46.000000000 +0200
@@ -424,6 +424,7 @@
       'administer content types',
       'access administration pages',
       'bypass node access',
+      'administer fields',
       filter_permission_name($full_html_format),
     ));
     $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content'));
diff -Naur drupal-7.23/modules/field/tests/field.test drupal-7.66/modules/field/tests/field.test
--- drupal-7.23/modules/field/tests/field.test	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/tests/field.test	2019-04-17 22:20:46.000000000 +0200
@@ -485,6 +485,66 @@
   }
 
   /**
+   * Test field_has_data().
+   */
+  function testFieldHasData() {
+    $entity_type = 'test_entity';
+    $langcode = LANGUAGE_NONE;
+
+    $field_name = 'field_1';
+    $field = array('field_name' => $field_name, 'type' => 'test_field');
+    $field = field_create_field($field);
+
+    $this->assertFalse(field_has_data($field), "No data should be detected.");
+
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle'
+    );
+    $instance = field_create_instance($instance);
+    $table = _field_sql_storage_tablename($field);
+    $revision_table = _field_sql_storage_revision_tablename($field);
+
+    $columns = array('entity_type', 'entity_id', 'revision_id', 'delta', 'language', $field_name . '_value');
+
+    $eid = 0;
+
+    // Insert values into the field revision table.
+    $query = db_insert($revision_table)->fields($columns);
+    $query->values(array($entity_type, $eid, 0, 0, $langcode, 1));
+    $query->execute();
+
+    $this->assertTrue(field_has_data($field), "Revision data only should be detected.");
+
+    $field_name = 'field_2';
+    $field = array('field_name' => $field_name, 'type' => 'test_field');
+    $field = field_create_field($field);
+
+    $this->assertFalse(field_has_data($field), "No data should be detected.");
+
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle'
+    );
+    $instance = field_create_instance($instance);
+    $table = _field_sql_storage_tablename($field);
+    $revision_table = _field_sql_storage_revision_tablename($field);
+
+    $columns = array('entity_type', 'entity_id', 'revision_id', 'delta', 'language', $field_name . '_value');
+
+    $eid = 1;
+
+    // Insert values into the field table.
+    $query = db_insert($table)->fields($columns);
+    $query->values(array($entity_type, $eid, 0, 0, $langcode, 1));
+    $query->execute();
+
+    $this->assertTrue(field_has_data($field), "Values only in field table should be detected.");
+  }
+
+  /**
    * Test field_attach_delete().
    */
   function testFieldAttachDelete() {
@@ -2146,11 +2206,12 @@
         'alter' => TRUE,
       ),
     );
-    $output = field_view_field('test_entity', $this->entity, $this->field_name, $display);
+    $output = field_view_field('test_entity', $this->entity, $this->field_name, $display, LANGUAGE_NONE);
     $this->drupalSetContent(drupal_render($output));
     $setting = $display['settings']['test_formatter_setting_multiple'];
     $this->assertNoText($this->label, 'Label was not displayed.');
     $this->assertText('field_test_field_attach_view_alter', 'Alter fired, display passed.');
+    $this->assertText('field language is ' . LANGUAGE_NONE, 'Language is placed onto the context.');
     $array = array();
     foreach ($this->values as $delta => $value) {
       $array[] = $delta . ':' . $value['value'];
diff -Naur drupal-7.23/modules/field/tests/field_test.info drupal-7.66/modules/field/tests/field_test.info
--- drupal-7.23/modules/field/tests/field_test.info	2013-08-08 04:17:18.000000000 +0200
+++ drupal-7.66/modules/field/tests/field_test.info	2019-04-17 22:39:36.000000000 +0200
@@ -6,8 +6,7 @@
 version = VERSION
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2013-08-08
-version = "7.23"
+; Information added by Drupal.org packaging script on 2019-04-17
+version = "7.66"
 project = "drupal"
-datestamp = "1375928238"
-
+datestamp = "1555533576"
diff -Naur drupal-7.23/modules/field/tests/field_test.install drupal-7.66/modules/field/tests/field_test.install
--- drupal-7.23/modules/field/tests/field_test.install	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/tests/field_test.install	2019-04-17 22:20:46.000000000 +0200
@@ -60,7 +60,7 @@
     'description' => 'The base table for test entities with a bundle key.',
     'fields' => array(
       'ftid' => array(
-        'description' => 'The primary indentifier for a test_entity_bundle_key.',
+        'description' => 'The primary identifier for a test_entity_bundle_key.',
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
@@ -79,7 +79,7 @@
     'description' => 'The base table for test entities with a bundle.',
     'fields' => array(
       'ftid' => array(
-        'description' => 'The primary indentifier for a test_entity_bundle.',
+        'description' => 'The primary identifier for a test_entity_bundle.',
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
diff -Naur drupal-7.23/modules/field/tests/field_test.module drupal-7.66/modules/field/tests/field_test.module
--- drupal-7.23/modules/field/tests/field_test.module	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/tests/field_test.module	2019-04-17 22:20:46.000000000 +0200
@@ -220,6 +220,10 @@
   if (!empty($context['display']['settings']['alter'])) {
     $output['test_field'][] = array('#markup' => 'field_test_field_attach_view_alter');
   }
+
+  if (isset($output['test_field'])) {
+    $output['test_field'][] = array('#markup' => 'field language is ' . $context['language']);
+  }
 }
 
 /**
@@ -267,3 +271,14 @@
   // exception if the EFQ does not properly prefix the base table.
   $query->join('test_entity','te2','%alias.ftid = test_entity.ftid');
 }
+
+/**
+ * Implements hook_query_TAG_alter() for tag 'store_global_test_query'.
+ */
+function field_test_query_store_global_test_query_alter($query) {
+  // Save the query in a global variable so that it can be examined by tests.
+  // This can be used by any test which needs to check a query, but see
+  // FieldSqlStorageTestCase::testFieldSqlStorageMultipleConditionsSameColumn()
+  // for an example.
+  $GLOBALS['test_query'] = $query;
+}
diff -Naur drupal-7.23/modules/field/theme/field.tpl.php drupal-7.66/modules/field/theme/field.tpl.php
--- drupal-7.23/modules/field/theme/field.tpl.php	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/field/theme/field.tpl.php	2019-04-17 22:20:46.000000000 +0200
@@ -4,8 +4,10 @@
  * @file field.tpl.php
  * Default template implementation to display the value of a field.
  *
- * This file is not used and is here as a starting point for customization only.
- * @see theme_field()
+ * This file is not used by Drupal core, which uses theme functions instead for
+ * performance reasons. The markup is the same, though, so if you want to use
+ * template files rather than functions to extend field theming, copy this to
+ * your custom theme. See theme_field() for a discussion of performance.
  *
  * Available variables:
  * - $items: An array of field values. Use render() to output them.
@@ -45,7 +47,7 @@
  */
 ?>
 ' placeholder.
   if ($mode) {
     $content = $match[1];
-    $hash = md5($content);
+    $hash = hash('sha256', $content);
     $comments[$hash] = $content;
     return "";
   }
diff -Naur drupal-7.23/modules/filter/filter.pages.inc drupal-7.66/modules/filter/filter.pages.inc
--- drupal-7.23/modules/filter/filter.pages.inc	2013-08-08 04:04:26.000000000 +0200
+++ drupal-7.66/modules/filter/filter.pages.inc	2019-04-17 22:20:46.000000000 +0200
@@ -14,10 +14,9 @@
  * @see filter_menu()
  * @see theme_filter_tips()
  */
-function filter_tips_long() {
-  $format_id = arg(2);
-  if ($format_id) {
-    $output = theme('filter_tips', array('tips' => _filter_tips($format_id, TRUE), 'long' => TRUE));
+function filter_tips_long($format = NULL) {
+  if (!empty($format)) {
+    $output = theme('filter_tips', array('tips' => _filter_tips($format->format, TRUE), 'long' => TRUE));
   }
   else {
     $output = theme('filter_tips', array('tips' => _filter_tips(-1, TRUE), 'long' => TRUE));
@@ -68,7 +67,7 @@
     foreach ($tips as $name => $tiplist) {
       if ($multiple) {
         $output .= '
'; - $output .= '

' . $name . '

'; + $output .= '

' . check_plain($name) . '

'; } if (count($tiplist) > 0) { diff -Naur drupal-7.23/modules/filter/filter.test drupal-7.66/modules/filter/filter.test --- drupal-7.23/modules/filter/filter.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/filter/filter.test 2019-04-17 22:20:46.000000000 +0200 @@ -70,6 +70,15 @@ $this->assertFalse($db_format->status, 'Database: Disabled text format is marked as disabled.'); $formats = filter_formats(); $this->assertTrue(!isset($formats[$format->format]), 'filter_formats: Disabled text format no longer exists.'); + + // Add a new format to check for Xss in format name. + $format = new stdClass(); + $format->format = 'xss_format'; + $format->name = ''; + 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.'); } /** @@ -546,6 +555,27 @@ $this->assertTrue(isset($options[$this->allowed_format->format]), 'The allowed text format appears as an option when adding a new node.'); $this->assertFalse(isset($options[$this->disallowed_format->format]), 'The disallowed text format does not appear as an option when adding a new node.'); $this->assertTrue(isset($options[filter_fallback_format()]), 'The fallback format appears as an option when adding a new node.'); + + // Check regular user access to the filter tips pages. + $this->drupalGet('filter/tips/' . $this->allowed_format->format); + $this->assertResponse(200); + $this->drupalGet('filter/tips/' . $this->disallowed_format->format); + $this->assertResponse(403); + $this->drupalGet('filter/tips/' . filter_fallback_format()); + $this->assertResponse(200); + $this->drupalGet('filter/tips/invalid-format'); + $this->assertResponse(404); + + // Check admin user access to the filter tips pages. + $this->drupalLogin($this->admin_user); + $this->drupalGet('filter/tips/' . $this->allowed_format->format); + $this->assertResponse(200); + $this->drupalGet('filter/tips/' . $this->disallowed_format->format); + $this->assertResponse(200); + $this->drupalGet('filter/tips/' . filter_fallback_format()); + $this->assertResponse(200); + $this->drupalGet('filter/tips/invalid-format'); + $this->assertResponse(404); } /** @@ -1090,8 +1120,12 @@ $f = filter_xss("", array('img')); $this->assertNoNormalized($f, 'cript', 'HTML scheme clearing evasion -- embedded nulls.'); - $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- spaces and metacharacters before scheme.'); + // @todo This dataset currently fails under 5.4 because of + // https://www.drupal.org/node/1210798. Restore after it's fixed. + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + $f = filter_xss('', array('img')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- spaces and metacharacters before scheme.'); + } $f = filter_xss('', array('img')); $this->assertNoNormalized($f, 'vbscript', 'HTML scheme clearing evasion -- another scheme.'); @@ -1139,7 +1173,7 @@ // Setup dummy filter object. $filter = new stdClass(); $filter->settings = array( - 'allowed_html' => '
    1. ', + 'allowed_html' => '
        1. ', 'filter_html_help' => 1, 'filter_html_nofollow' => 0, ); @@ -1175,6 +1209,10 @@ $f = _filter_html(' ', $filter); $this->assertNoNormalized($f, 'onerror', 'HTML filter should remove empty on* attributes on default.'); + + // Custom tags are supported and should be allowed through. + $f = _filter_html('', $filter); + $this->assertNormalized($f, 'test-element', 'HTML filter should allow custom elements.'); } /** @@ -1260,6 +1298,7 @@ // Create a e-mail that is too long. $long_email = str_repeat('a', 254) . '@example.com'; $too_long_email = str_repeat('b', 255) . '@example.com'; + $email_with_plus_sign = 'one+two@example.com'; // Filter selection/pattern matching. @@ -1273,12 +1312,13 @@ ), // MAILTO URLs. ' -person@example.com or mailto:person2@example.com or ' . $long_email . ' but not ' . $too_long_email . ' +person@example.com or mailto:person2@example.com or ' . $email_with_plus_sign . ' or ' . $long_email . ' but not ' . $too_long_email . ' ' => array( 'person@example.com' => TRUE, 'mailto:person2@example.com' => TRUE, '' . $long_email . '' => TRUE, '' . $too_long_email . '' => FALSE, + '' . $email_with_plus_sign . '' => TRUE, ), // URI parts and special characters. ' @@ -1970,3 +2010,26 @@ } } } + +/** + * Tests DOMDocument serialization. + */ +class FilterDOMSerializeTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Serialization', + 'description' => 'Test serialization of DOMDocument objects.', + 'group' => 'Filter', + ); + } + + /** + * Tests empty DOMDocument object. + */ + function testFilterEmptyDOMSerialization() { + $document = new DOMDocument(); + $result = filter_dom_serialize($document); + $this->assertEqual('', $result); + } +} diff -Naur drupal-7.23/modules/forum/forum.info drupal-7.66/modules/forum/forum.info --- drupal-7.23/modules/forum/forum.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/forum/forum.info 2019-04-17 22:39:36.000000000 +0200 @@ -9,8 +9,7 @@ configure = admin/structure/forum stylesheets[all][] = forum.css -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/forum/forum.module drupal-7.66/modules/forum/forum.module --- drupal-7.23/modules/forum/forum.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/forum/forum.module 2019-04-17 22:20:46.000000000 +0200 @@ -263,10 +263,10 @@ * 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 -Naur drupal-7.23/modules/help/help.api.php drupal-7.66/modules/help/help.api.php --- drupal-7.23/modules/help/help.api.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/help/help.api.php 1970-01-01 01:00:00.000000000 +0100 @@ -1,63 +0,0 @@ -' . 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.') . '

          '; - } -} - -/** - * @} End of "addtogroup hooks". - */ diff -Naur drupal-7.23/modules/help/help.info drupal-7.66/modules/help/help.info --- drupal-7.23/modules/help/help.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/help/help.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x files[] = help.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/image/image.admin.inc drupal-7.66/modules/image/image.admin.inc --- drupal-7.23/modules/image/image.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/image/image.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -592,15 +592,15 @@ '#type' => 'radios', '#title' => t('Anchor'), '#options' => array( - 'left-top' => t('Top') . ' ' . t('Left'), - 'center-top' => t('Top') . ' ' . t('Center'), - 'right-top' => t('Top') . ' ' . t('Right'), - 'left-center' => t('Center') . ' ' . t('Left'), + 'left-top' => t('Top left'), + 'center-top' => t('Top center'), + 'right-top' => t('Top right'), + 'left-center' => t('Center left'), 'center-center' => t('Center'), - 'right-center' => t('Center') . ' ' . t('Right'), - 'left-bottom' => t('Bottom') . ' ' . t('Left'), - 'center-bottom' => t('Bottom') . ' ' . t('Center'), - 'right-bottom' => t('Bottom') . ' ' . t('Right'), + 'right-center' => t('Center right'), + 'left-bottom' => t('Bottom left'), + 'center-bottom' => t('Bottom center'), + 'right-bottom' => t('Bottom right'), ), '#theme' => 'image_anchor', '#default_value' => $data['anchor'], @@ -736,7 +736,8 @@ if (!isset($form[$key]['#access']) || $form[$key]['#access']) { $rows[] = array( 'data' => $row, - 'class' => !empty($form[$key]['weight']['#access']) || $key == 'new' ? array('draggable') : array(), + // Use a strict (===) comparison since $key can be 0. + 'class' => !empty($form[$key]['weight']['#access']) || $key === 'new' ? array('draggable') : array(), ); } } diff -Naur drupal-7.23/modules/image/image.field.inc drupal-7.66/modules/image/image.field.inc --- drupal-7.23/modules/image/image.field.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/image/image.field.inc 2019-04-17 22:20:46.000000000 +0200 @@ -351,7 +351,7 @@ if ($field['cardinality'] == 1) { // If there's only one field, return it as delta 0. if (empty($elements[0]['#default_value']['fid'])) { - $elements[0]['#description'] = theme('file_upload_help', array('description' => $instance['description'], 'upload_validators' => $elements[0]['#upload_validators'])); + $elements[0]['#description'] = theme('file_upload_help', array('description' => field_filter_xss($instance['description']), 'upload_validators' => $elements[0]['#upload_validators'])); } } else { diff -Naur drupal-7.23/modules/image/image.info drupal-7.66/modules/image/image.info --- drupal-7.23/modules/image/image.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/image/image.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ files[] = image.test configure = admin/config/media/image-styles -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/image/image.module drupal-7.66/modules/image/image.module --- drupal-7.23/modules/image/image.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/image/image.module 2019-04-17 22:20:46.000000000 +0200 @@ -64,7 +64,7 @@ $effect = image_effect_definition_load($arg[7]); return isset($effect['help']) ? ('

          ' . $effect['help'] . '

          ') : NULL; case 'admin/config/media/image-styles/edit/%/effects/%': - $effect = ($arg[5] == 'add') ? image_effect_definition_load($arg[6]) : image_effect_load($arg[6], $arg[4]); + $effect = ($arg[5] == 'add') ? image_effect_definition_load($arg[6]) : image_effect_load($arg[7], $arg[5]); return isset($effect['help']) ? ('

          ' . $effect['help'] . '

          ') : NULL; } } @@ -801,6 +801,8 @@ * * @param $style * The image style + * @param $scheme + * The file scheme, for example 'public' for public files. */ function image_style_deliver($style, $scheme) { $args = func_get_args(); @@ -833,9 +835,9 @@ file_download($scheme, file_uri_target($derivative_uri)); } else { - $headers = module_invoke_all('file_download', $image_uri); - if (in_array(-1, $headers) || empty($headers)) { - return drupal_access_denied(); + $headers = file_download_headers($image_uri); + if (empty($headers)) { + return MENU_ACCESS_DENIED; } if (count($headers)) { foreach ($headers as $name => $value) { @@ -845,6 +847,12 @@ } } + // 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 +862,7 @@ // 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 +884,7 @@ 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(); } @@ -972,7 +982,9 @@ // Delete the style directory in each registered wrapper. $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE); foreach ($wrappers as $wrapper => $wrapper_data) { - file_unmanaged_delete_recursive($wrapper . '://styles/' . $style['name']); + if (file_exists($directory = $wrapper . '://styles/' . $style['name'])) { + file_unmanaged_delete_recursive($directory); + } } // Let other modules update as necessary on flush. @@ -1010,10 +1022,22 @@ */ function image_style_url($style_name, $path) { $uri = image_style_path($style_name, $path); + + // The passed-in $path variable can be either a relative path or a full URI. + $original_uri = file_uri_scheme($path) ? file_stream_wrapper_uri_normalize($path) : file_build_uri($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, file_stream_wrapper_uri_normalize($path))); + // 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 @@ -1025,8 +1049,12 @@ } $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 -Naur drupal-7.23/modules/image/image.test drupal-7.66/modules/image/image.test --- drupal-7.23/modules/image/image.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/image/image.test 2019-04-17 22:20:46.000000000 +0200 @@ -32,7 +32,7 @@ function setUp() { parent::setUp('image'); - $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles')); + $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles', 'administer fields')); $this->drupalLogin($this->admin_user); } @@ -78,6 +78,24 @@ } /** + * Create a random style. + * + * @return array + * A list containing the details of the generated image style. + */ + function createRandomStyle() { + $style_name = strtolower($this->randomName(10)); + $style_label = $this->randomString(); + image_style_save(array('name' => $style_name, 'label' => $style_label)); + $style_path = 'admin/config/media/image-styles/edit/' . $style_name; + return array( + 'name' => $style_name, + 'label' => $style_label, + 'path' => $style_path, + ); + } + + /** * Upload an image to a node. * * @param $image @@ -174,6 +192,32 @@ } /** + * 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 that we do not pass an array to drupal_add_http_header. + */ + function testImageContentTypeHeaders() { + $files = $this->drupalGetTestFiles('image'); + $file = array_shift($files); + // Copy the test file to private folder. + $private_file = file_copy($file, 'private://', FILE_EXISTS_RENAME); + // Tell image_module_test module to return the headers we want to test. + variable_set('image_module_test_invalid_headers', $private_file->uri); + // Invoke image_style_deliver so it will try to set headers. + $generated_url = image_style_url($this->style_name, $private_file->uri); + $this->drupalGet($generated_url); + variable_del('image_module_test_invalid_headers'); + } + + /** * Test image_style_url(). */ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FALSE) { @@ -216,10 +260,20 @@ } // Add some extra chars to the token. $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); - $this->assertResponse(403, 'Image was inaccessible at the URL wih an invalid token.'); + $this->assertResponse(403, 'Image was inaccessible at the URL with an invalid token.'); // Change the parameter name so the token is missing. $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $generate_url)); - $this->assertResponse(403, 'Image was inaccessible at the URL wih a missing token.'); + $this->assertResponse(403, 'Image was inaccessible at the URL with a missing token.'); + + // Check that the generated URL is the same when we pass in a relative path + // rather than a URI. We need to temporarily switch the default scheme to + // match the desired scheme before testing this, then switch it back to the + // "temporary" scheme used throughout this test afterwards. + variable_set('file_default_scheme', $scheme); + $relative_path = file_uri_target($original_uri); + $generate_url_from_relative_path = image_style_url($this->style_name, $relative_path); + $this->assertEqual($generate_url, $generate_url_from_relative_path, 'Generated URL is the same regardless of whether it came from a relative path or a file URI.'); + variable_set('file_default_scheme', 'temporary'); // Fetch the URL that generates the file. $this->drupalGet($generate_url); @@ -231,7 +285,7 @@ $this->assertEqual($this->drupalGetHeader('Content-Length'), $generated_image_info['file_size'], 'Expected Content-Length was reported.'); if ($scheme == 'private') { $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.'); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', 'Cache-Control header was set to prevent caching.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate', 'Cache-Control header was set to prevent caching.'); $this->assertEqual($this->drupalGetHeader('X-Image-Owned-By'), 'image_module_test', 'Expected custom header has been added.'); // Make sure that a second request to the already existing derivate works @@ -268,7 +322,7 @@ elseif ($clean_url) { // Add some extra chars to the token. $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); - $this->assertResponse(200, 'Existing image was accessible at the URL wih an invalid token.'); + $this->assertResponse(200, 'Existing image was accessible at the URL with an invalid token.'); } // Allow insecure image derivatives to be created for the remainder of this @@ -310,6 +364,15 @@ $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(); @@ -441,6 +504,58 @@ } /** + * Tests the administrative user interface. + */ +class ImageAdminUiTestCase extends ImageFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Administrative user interface', + 'description' => 'Tests the forms used in the administrative user interface.', + 'group' => 'Image', + ); + } + + function setUp() { + parent::setUp(array('image')); + } + + /** + * Test if the help text is available on the add effect form. + */ + function testAddEffectHelpText() { + // Create a random image style. + $style = $this->createRandomStyle(); + + // Open the add effect form and check for the help text. + $this->drupalGet($style['path'] . '/add/image_crop'); + $this->assertText(t('Cropping will remove portions of an image to make it the specified dimensions.'), 'The image style effect help text was displayed on the add effect page.'); + } + + /** + * Test if the help text is available on the edit effect form. + */ + function testEditEffectHelpText() { + // Create a random image style. + $random_style = $this->createRandomStyle(); + + // Add the crop effect to the image style. + $edit = array(); + $edit['data[width]'] = 20; + $edit['data[height]'] = 20; + $this->drupalPost($random_style['path'] . '/add/image_crop', $edit, t('Add effect')); + + // Open the edit effect form and check for the help text. + drupal_static_reset('image_styles'); + $style = image_style_load($random_style['name']); + + foreach ($style['effects'] as $ieid => $effect) { + $this->drupalGet($random_style['path'] . '/effects/' . $ieid); + $this->assertText(t('Cropping will remove portions of an image to make it the specified dimensions.'), 'The image style effect help text was displayed on the edit effect page.'); + } + } +} + +/** * Tests creation, deletion, and editing of image styles and effects. */ class ImageAdminStylesUnitTest extends ImageFieldTestCase { diff -Naur drupal-7.23/modules/image/tests/image_module_test.info drupal-7.66/modules/image/tests/image_module_test.info --- drupal-7.23/modules/image/tests/image_module_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/image/tests/image_module_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = image_module_test.module hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/image/tests/image_module_test.module drupal-7.66/modules/image/tests/image_module_test.module --- drupal-7.23/modules/image/tests/image_module_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/image/tests/image_module_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -9,6 +9,9 @@ if (variable_get('image_module_test_file_download', FALSE) == $uri) { return array('X-Image-Owned-By' => 'image_module_test'); } + if (variable_get('image_module_test_invalid_headers', FALSE) == $uri) { + return array('Content-Type' => 'image/png'); + } } /** diff -Naur drupal-7.23/modules/locale/locale.admin.inc drupal-7.66/modules/locale/locale.admin.inc --- drupal-7.23/modules/locale/locale.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/locale/locale.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1139,11 +1139,11 @@ '#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); @@ -1194,7 +1194,7 @@ $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField(); if (!empty($value)) { // Only update or insert if we have a value to use. - if (!empty($translation)) { + if (is_string($translation)) { db_update('locales_target') ->fields(array( 'translation' => $value, @@ -1242,9 +1242,7 @@ if ($source = db_query('SELECT lid, source FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject()) { return drupal_get_form('locale_translate_delete_form', $source); } - else { - return drupal_not_found(); - } + return MENU_NOT_FOUND; } /** diff -Naur drupal-7.23/modules/locale/locale.info drupal-7.66/modules/locale/locale.info --- drupal-7.23/modules/locale/locale.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/locale/locale.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = locale.test configure = admin/config/regional/language -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/locale/locale.test drupal-7.66/modules/locale/locale.test --- drupal-7.23/modules/locale/locale.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/locale/locale.test 2019-04-17 22:20:46.000000000 +0200 @@ -393,6 +393,16 @@ // The indicator should not be here. $this->assertNoRaw($language_indicator, 'String is translated.'); + // Verify that a translation set which has an empty target string can be + // updated without any database error. + db_update('locales_target') + ->fields(array('translation' => '')) + ->condition('language', $langcode, '=') + ->condition('lid', $lid, '=') + ->execute(); + $this->drupalPost('admin/config/regional/translate/edit/' . $lid, $edit, t('Save translations')); + $this->assertText(t('The string has been saved.'), 'The string has been saved.'); + // Try to edit a non-existent string and ensure we're redirected correctly. // Assuming we don't have 999,999 strings already. $random_lid = 999999; @@ -809,7 +819,7 @@ * Additional options to pass to the translation import form. */ function importPoFile($contents, array $options = array()) { - $name = tempnam('temporary://', "po_") . '.po'; + $name = drupal_tempnam('temporary://', "po_") . '.po'; file_put_contents($name, $contents); $options['files[file]'] = $name; $this->drupalPost('admin/config/regional/translate/import', $options, t('Import')); @@ -1103,7 +1113,7 @@ * Additional options to pass to the translation import form. */ function importPoFile($contents, array $options = array()) { - $name = tempnam('temporary://', "po_") . '.po'; + $name = drupal_tempnam('temporary://', "po_") . '.po'; file_put_contents($name, $contents); $options['files[file]'] = $name; $this->drupalPost('admin/config/regional/translate/import', $options, t('Import')); @@ -1202,7 +1212,7 @@ * 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 "" @@ -1330,7 +1340,7 @@ function testExportTranslation() { // First import some known translations. // This will also automatically enable the 'fr' language. - $name = tempnam('temporary://', "po_") . '.po'; + $name = drupal_tempnam('temporary://', "po_") . '.po'; file_put_contents($name, $this->getPoFile()); $this->drupalPost('admin/config/regional/translate/import', array( 'langcode' => 'fr', @@ -1685,7 +1695,7 @@ ); $test_cases = array( - // Equal qvalue for each language, choose the site prefered one. + // Equal qvalue for each language, choose the site preferred one. 'en,en-US,fr-CA,fr,es-MX' => 'en', 'en-US,en,fr-CA,fr,es-MX' => 'en', 'fr,en' => 'en', @@ -2237,6 +2247,37 @@ $this->drupalLogout(); } + + /** + * Verifies that nodes may be created with different languages. + */ + function testNodeCreationWithLanguage() { + // Create an admin user and log them in. + $perms = array( + // Standard node permissions. + 'create page content', + 'administer content types', + 'administer nodes', + 'bypass node access', + // Locale. + 'administer languages', + ); + $web_user = $this->drupalCreateUser($perms); + $this->drupalLogin($web_user); + + // Create some test nodes using different langcodes. + foreach (array(LANGUAGE_NONE, 'en', 'fr') as $langcode) { + $node_args = array( + 'type' => 'page', + 'promote' => 1, + 'language' => $langcode, + ); + $node = $this->drupalCreateNode($node_args); + $node_reloaded = node_load($node->nid, NULL, TRUE); + $this->assertEqual($node_reloaded->language, $langcode, format_string('The language code of the node was successfully set to @langcode.', array('@langcode' => $langcode))); + } + } + } /** @@ -2629,6 +2670,68 @@ $this->drupalGet("$prefix/$path"); $this->assertResponse(404, $message2); } + + /** + * Check URL rewriting when using a domain name and a non-standard port. + */ + function testDomainNameNegotiationPort() { + $language_domain = 'example.fr'; + $edit = array( + 'locale_language_negotiation_url_part' => 1, + ); + $this->drupalPost('admin/config/regional/language/configure/url', $edit, t('Save configuration')); + $edit = array( + 'prefix' => '', + 'domain' => $language_domain + ); + $this->drupalPost('admin/config/regional/language/edit/fr', $edit, t('Save language')); + + // Enable domain configuration. + variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN); + + // Reset static caching. + drupal_static_reset('language_list'); + drupal_static_reset('language_url_outbound_alter'); + drupal_static_reset('language_url_rewrite_url'); + + // In case index.php is part of the URLs, we need to adapt the asserted + // URLs as well. + $index_php = strpos(url('', array('absolute' => TRUE)), 'index.php') !== FALSE; + + // Remember current HTTP_HOST. + $http_host = $_SERVER['HTTP_HOST']; + + // Fake a different port. + $_SERVER['HTTP_HOST'] .= ':88'; + + // Create an absolute French link. + $languages = language_list(); + $language = $languages['fr']; + $url = url('', array( + 'absolute' => TRUE, + 'language' => $language + )); + + $expected = 'http://example.fr:88/'; + $expected .= $index_php ? 'index.php/' : ''; + + $this->assertEqual($url, $expected, 'The right port is used.'); + + // If we set the port explicitly in url(), it should not be overriden. + $url = url('', array( + 'absolute' => TRUE, + 'language' => $language, + 'base_url' => $GLOBALS['base_url'] . ':90', + )); + + $expected = 'http://example.fr:90/'; + $expected .= $index_php ? 'index.php/' : ''; + + $this->assertEqual($url, $expected, 'A given port is not overriden.'); + + // Restore HTTP_HOST. + $_SERVER['HTTP_HOST'] = $http_host; + } } /** @@ -3085,11 +3188,7 @@ foreach (language_types_info() as $type => $info) { if (isset($info['fixed'])) { $negotiation = variable_get("language_negotiation_$type", array()); - $equal = count($info['fixed']) == count($negotiation); - while ($equal && list($id) = each($negotiation)) { - list(, $info_id) = each($info['fixed']); - $equal = $info_id == $id; - } + $equal = array_keys($negotiation) === array_values($info['fixed']); $this->assertTrue($equal, format_string('language negotiation for %type is properly set up', array('%type' => $type))); } } @@ -3141,3 +3240,46 @@ $this->assertRaw('@import url("' . $base_url . '/modules/system/system.messages.css' . $query_string . '");' . "\n" . '@import url("' . $base_url . '/modules/system/system.messages-rtl.css' . $query_string . '");' . "\n", 'CSS: system.messages-rtl.css is added directly after system.messages.css.'); } } + +/** + * Tests locale translation safe string handling. + */ +class LocaleStringIsSafeTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Test if a string is safe', + 'description' => 'Tests locale translation safe string handling.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + } + + /** + * Tests for locale_string_is_safe(). + */ + public function testLocaleStringIsSafe() { + // Check a translatable string without HTML. + $string = 'Hello world!'; + $result = locale_string_is_safe($string); + $this->assertTrue($result); + + // Check a translatable string which includes trustable HTML. + $string = 'Hello world!'; + $result = locale_string_is_safe($string); + $this->assertTrue($result); + + // Check an untranslatable string which includes untrustable HTML (according + // to the locale_string_is_safe() function definition). + $string = 'Hello world!'; + $result = locale_string_is_safe($string); + $this->assertFalse($result); + + // Check a translatable string which includes a token in an href attribute. + $string = 'Hi user'; + $result = locale_string_is_safe($string); + $this->assertTrue($result); + } +} diff -Naur drupal-7.23/modules/locale/tests/locale_test.info drupal-7.66/modules/locale/tests/locale_test.info --- drupal-7.23/modules/locale/tests/locale_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/locale/tests/locale_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/menu/menu.admin.inc drupal-7.66/modules/menu/menu.admin.inc --- drupal-7.23/modules/menu/menu.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/menu/menu.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -281,6 +281,7 @@ $form['link_title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), + '#maxlength' => 255, '#default_value' => $item['link_title'], '#description' => t('The text to be used for this link in the menu.'), '#required' => TRUE, @@ -305,7 +306,7 @@ '#title' => t('Path'), '#maxlength' => 255, '#default_value' => $path, - '#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')), + '#description' => t('The path for this menu link. This can be an internal path such as %add-node or an external URL such as %example. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%example' => 'http://example.com')), '#required' => TRUE, ); $form['actions']['delete'] = array( @@ -512,8 +513,7 @@ // System-defined menus may not be deleted. $system_menus = menu_list_system_menus(); if (isset($system_menus[$menu['menu_name']])) { - drupal_access_denied(); - return; + return MENU_ACCESS_DENIED; } return drupal_get_form('menu_delete_menu_confirm', $menu); } @@ -622,8 +622,7 @@ // Links defined via hook_menu may not be deleted. Updated items are an // exception, as they can be broken. if ($item['module'] == 'system' && !$item['updated']) { - drupal_access_denied(); - return; + return MENU_ACCESS_DENIED; } return drupal_get_form('menu_item_delete_form', $item); } diff -Naur drupal-7.23/modules/menu/menu.info drupal-7.66/modules/menu/menu.info --- drupal-7.23/modules/menu/menu.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/menu/menu.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = menu.test configure = admin/structure/menu -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/menu/menu.module drupal-7.66/modules/menu/menu.module --- drupal-7.23/modules/menu/menu.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/menu/menu.module 2019-04-17 22:20:46.000000000 +0200 @@ -69,7 +69,7 @@ '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', @@ -674,6 +674,7 @@ $form['menu']['link']['link_title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), + '#maxlength' => 255, '#default_value' => $link['link_title'], ); diff -Naur drupal-7.23/modules/menu/menu.test drupal-7.66/modules/menu/menu.test --- drupal-7.23/modules/menu/menu.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/menu/menu.test 2019-04-17 22:20:46.000000000 +0200 @@ -72,6 +72,17 @@ $saved_item = menu_link_load($item['mlid']); $this->assertEqual($description, $saved_item['options']['attributes']['title'], 'Saving an existing link updates the description (title attribute)'); $this->resetMenuLink($item, $old_title); + + // Test that the page title is correct when a local task appears in a + // top-level menu item. See https://www.drupal.org/node/1973262. + $item = $this->addMenuLink(0, 'user/register', 'user-menu'); + $this->drupalGet('user/password'); + $this->assertNoTitle('Home | Drupal'); + $this->drupalLogout(); + $this->drupalGet('user/register'); + $this->assertTitle($item['link_title'] . ' | Drupal'); + $this->drupalGet('user'); + $this->assertNoTitle('Home | Drupal'); } /** @@ -514,6 +525,23 @@ } /** + * 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. */ private function getStandardMenuLink() { @@ -620,7 +648,12 @@ ); $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); - // Create a node. + // Verify that the menu link title on the node add form has the correct + // maxlength. + $this->drupalGet('node/add/page'); + $this->assertPattern('//', 'Menu link title field has correct maxlength in node add form.'); + + // Create a node with menu link disabled. $node_title = $this->randomName(); $language = LANGUAGE_NONE; $edit = array( @@ -656,6 +689,10 @@ $this->drupalGet('node/' . $node->nid . '/edit'); $this->assertOptionSelected('edit-menu-weight', 17, 'Menu weight correct in edit form'); + // Verify that the menu link title on the node edit form has the correct + // maxlength. + $this->assertPattern('//', 'Menu link title field has correct maxlength in node edit form.'); + // Edit the node and remove the menu link. $edit = array( 'menu[enabled]' => FALSE, diff -Naur drupal-7.23/modules/node/content_types.inc drupal-7.66/modules/node/content_types.inc --- drupal-7.23/modules/node/content_types.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/content_types.inc 2019-04-17 22:20:46.000000000 +0200 @@ -11,7 +11,7 @@ function node_overview_types() { $types = node_type_get_types(); $names = node_type_get_names(); - $field_ui = module_exists('field_ui'); + $field_ui = module_exists('field_ui') && user_access('administer fields'); $header = array(t('Name'), array('data' => t('Operations'), 'colspan' => $field_ui ? '4' : '2')); $rows = array(); @@ -255,11 +255,11 @@ */ function node_type_form_validate($form, &$form_state) { $type = new stdClass(); - $type->type = trim($form_state['values']['type']); + $type->type = $form_state['values']['type']; $type->name = trim($form_state['values']['name']); // Work out what the type was before the user submitted this form - $old_type = trim($form_state['values']['old_type']); + $old_type = $form_state['values']['old_type']; $types = node_type_get_names(); @@ -288,7 +288,7 @@ $type = node_type_set_defaults(); - $type->type = trim($form_state['values']['type']); + $type->type = $form_state['values']['type']; $type->name = trim($form_state['values']['name']); $type->orig_type = trim($form_state['values']['orig_type']); $type->old_type = isset($form_state['values']['old_type']) ? $form_state['values']['old_type'] : $type->type; diff -Naur drupal-7.23/modules/node/node.admin.inc drupal-7.66/modules/node/node.admin.inc --- drupal-7.23/modules/node/node.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/node.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -329,6 +329,8 @@ } /** + * Implements callback_batch_operation(). + * * Executes a batch operation for node_mass_update(). * * @param array $nodes @@ -367,7 +369,9 @@ } /** - * Menu callback: Reports the status of batch operation for node_mass_update(). + * Implements callback_batch_finished(). + * + * Reports the status of batch operation for node_mass_update(). * * @param bool $success * A boolean indicating whether the batch mass update operation successfully @@ -471,6 +475,7 @@ $header['operations'] = array('data' => t('Operations')); $query = db_select('node', 'n')->extend('PagerDefault')->extend('TableSort'); + $query->addTag('node_admin_filter'); node_build_filter_query($query); if (!user_access('bypass node access')) { @@ -503,14 +508,17 @@ $options = array(); foreach ($nodes as $node) { $langcode = entity_language('node', $node); - $l_options = $langcode != LANGUAGE_NONE && isset($languages[$langcode]) ? array('language' => $languages[$langcode]) : array(); + $uri = entity_uri('node', $node); + if ($langcode != LANGUAGE_NONE && isset($languages[$langcode])) { + $uri['options']['language'] = $languages[$langcode]; + } $options[$node->nid] = array( 'title' => array( 'data' => array( '#type' => 'link', '#title' => $node->title, - '#href' => 'node/' . $node->nid, - '#options' => $l_options, + '#href' => $uri['path'], + '#options' => $uri['options'], '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))), ), ), @@ -695,6 +703,7 @@ function node_multiple_delete_confirm_submit($form, &$form_state) { if ($form_state['values']['confirm']) { node_delete_multiple(array_keys($form_state['values']['nodes'])); + cache_clear_all(); $count = count($form_state['values']['nodes']); watchdog('content', 'Deleted @count posts.', array('@count' => $count)); drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.')); diff -Naur drupal-7.23/modules/node/node.api.php drupal-7.66/modules/node/node.api.php --- drupal-7.23/modules/node/node.api.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/node.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -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 @@ 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 @@ * 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. @@ -948,7 +950,7 @@ * 'recent', or 'comments'. The values should be arrays themselves, with the * following keys available: * - title: (required) The human readable name of the ranking mechanism. - * - join: (optional) The part of a query string to join to any additional + * - join: (optional) An array with information to join any additional * necessary table. This is not necessary if the table required is already * joined to by the base query, such as for the {node} table. Other tables * should use the full table name as an alias to avoid naming collisions. @@ -972,7 +974,12 @@ 'title' => t('Average vote'), // Note that we use i.sid, the search index's search item id, rather than // n.nid. - 'join' => 'LEFT JOIN {vote_node_data} vote_node_data ON vote_node_data.nid = i.sid', + 'join' => array( + 'type' => 'LEFT', + 'table' => 'vote_node_data', + 'alias' => 'vote_node_data', + 'on' => 'vote_node_data.nid = i.sid', + ), // The highest possible score should be 1, and the lowest possible score, // always 0, should be 0. 'score' => 'vote_node_data.average / CAST(%f AS DECIMAL)', @@ -1030,12 +1037,23 @@ /** * 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 hook is invoked from node_delete_multiple() after the node has been - * removed from the node table in the database, before hook_node_delete() is - * invoked, and before field_attach_delete() is called. + * 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. + * + * 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 delete operation is entirely + * completed and node_delete_multiple() 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 node_delete_multiple() and + * db_transaction() for more info. * * @param $node * The node that is being deleted. @@ -1051,8 +1069,11 @@ /** * 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. @@ -1063,26 +1084,21 @@ * @ingroup node_api_hooks */ function hook_prepare($node) { - if ($file = file_check_upload($field_name)) { - $file = file_save_upload($field_name, _image_filename($file->filename, NULL, TRUE)); - if ($file) { - if (!image_get_info($file->uri)) { - form_set_error($field_name, t('Uploaded file is not a valid image')); - return; - } - } - else { - return; - } - $node->images['_original'] = $file->uri; - _image_build_derivatives($node, TRUE); - $node->new_file = TRUE; + if (!isset($node->mymodule_value)) { + $node->mymodule_value = 'foo'; } } /** * 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. @@ -1138,8 +1154,11 @@ /** * 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 @@ -1162,8 +1181,11 @@ /** * 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 @@ -1196,8 +1218,11 @@ /** * 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 @@ -1218,8 +1243,11 @@ /** * 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 @@ -1252,8 +1280,11 @@ /** * 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 @@ -1263,6 +1294,10 @@ * 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 @@ -1276,7 +1311,7 @@ * * @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 -Naur drupal-7.23/modules/node/node.info drupal-7.66/modules/node/node.info --- drupal-7.23/modules/node/node.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/node/node.info 2019-04-17 22:39:36.000000000 +0200 @@ -9,8 +9,7 @@ configure = admin/structure/types stylesheets[all][] = node.css -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/node/node.install drupal-7.66/modules/node/node.install --- drupal-7.23/modules/node/node.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/node.install 2019-04-17 22:20:46.000000000 +0200 @@ -114,6 +114,7 @@ 'uid' => array('uid'), 'tnid' => array('tnid'), 'translate' => array('translate'), + 'language' => array('language'), ), 'unique keys' => array( 'vid' => array('vid'), @@ -409,6 +410,7 @@ 'nid' => array( 'description' => 'The {node}.nid that was read.', 'type' => 'int', + 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), @@ -926,5 +928,39 @@ } /** + * Add an index on {node}.language. + */ +function node_update_7014() { + db_add_index('node', 'language', array('language')); +} + +/** + * Enable node types that may have been erroneously disabled in Drupal 7.36. + */ +function node_update_7015() { + db_update('node_type') + ->fields(array('disabled' => 0)) + ->condition('base', 'node_content') + ->execute(); +} + +/** + * Change {history}.nid to an unsigned int in order to match {node}.nid. + */ +function node_update_7016() { + db_drop_primary_key('history'); + db_drop_index('history', 'nid'); + db_change_field('history', 'nid', 'nid', array( + 'description' => 'The {node}.nid that was read.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + )); + db_add_primary_key('history', array('uid', 'nid')); + db_add_index('history', 'nid', array('nid')); +} + +/** * @} End of "addtogroup updates-7.x-extra". */ diff -Naur drupal-7.23/modules/node/node.module drupal-7.66/modules/node/node.module --- drupal-7.23/modules/node/node.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/node.module 2019-04-17 22:20:46.000000000 +0200 @@ -210,7 +210,7 @@ 'custom settings' => FALSE, ), 'search_result' => array( - 'label' => t('Search result'), + 'label' => t('Search result highlighting input'), 'custom settings' => FALSE, ), ); @@ -506,7 +506,8 @@ * - 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. @@ -1179,10 +1180,8 @@ module_invoke_all('node_' . $op, $node); module_invoke_all('entity_' . $op, $node, 'node'); - // Update the node access table for this node. There's no need to delete - // existing records if the node is new. - $delete = $op == 'update'; - node_access_acquire_grants($node, $delete); + // Update the node access table for this node. + node_access_acquire_grants($node); // Clear internal properties. unset($node->is_new); @@ -1399,12 +1398,7 @@ $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. @@ -1587,9 +1581,7 @@ ), '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'), @@ -1617,7 +1609,7 @@ } /** - * 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. @@ -1685,7 +1677,7 @@ ); $form['content_ranking']['#theme'] = 'node_search_admin'; $form['content_ranking']['info'] = array( - '#value' => '' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '' + '#markup' => '

          ' . t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '

          ' ); // Note: reversed to reflect that higher number = higher ranking. @@ -1872,7 +1864,7 @@ $output = drupal_render($form['info']); - $header = array(t('Factor'), t('Weight')); + $header = array(t('Factor'), t('Influence')); foreach (element_children($form['factors']) as $key) { $row = array(); $row[] = $form['factors'][$key]['#title']; @@ -2224,8 +2216,8 @@ /** * Returns a list of all the existing revision numbers. * - * @param Drupal\node\Node $node - * The node entity. + * @param $node + * The node object. * * @return * An associative array keyed by node revision number. @@ -2606,9 +2598,10 @@ $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')) ); @@ -2666,15 +2659,26 @@ * 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; } @@ -2949,7 +2953,10 @@ * system. When adding a node listing to your module, be sure to use a dynamic * query created by db_select() and add a tag of "node_access". This will allow * modules dealing with node access to ensure only nodes to which the user has - * access are retrieved, through the use of hook_query_TAG_alter(). + * access are retrieved, through the use of hook_query_TAG_alter(). Tagging a + * query with "node_access" does not check the published/unpublished status of + * nodes, so the base query is responsible for ensuring that unpublished nodes + * are not displayed to inappropriate users. * * Note: Even a single module returning NODE_ACCESS_DENY from hook_node_access() * will block access to the node. Therefore, implementers should take care to @@ -3292,6 +3299,17 @@ /** * Helper for node access functions. * + * Queries tagged with 'node_access' that are not against the {node} table + * should add the base table as metadata. For example: + * @code + * $query + * ->addTag('node_access') + * ->addMetaData('base_table', 'taxonomy_index'); + * @endcode + * If the query is not against the {node} table, an attempt is made to guess + * the table, but is not recommended to rely on this as it is deprecated and not + * allowed in Drupal 8. It is always safer to provide the table. + * * @param $query * The query to add conditions to. * @param $type @@ -3620,7 +3638,8 @@ // 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 @@ -3653,6 +3672,8 @@ } /** + * Implements callback_batch_operation(). + * * Performs batch operation for node_access_rebuild(). * * This is a multistep operation: we go through all nodes by packs of 20. The @@ -3667,7 +3688,7 @@ // Initiate multistep processing. $context['sandbox']['progress'] = 0; $context['sandbox']['current_node'] = 0; - $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField(); + $context['sandbox']['max'] = db_query('SELECT COUNT(nid) FROM {node}')->fetchField(); } // Process the next 20 nodes. @@ -3691,6 +3712,8 @@ } /** + * Implements callback_batch_finished(). + * * Performs post-processing for node_access_rebuild(). * * @param bool $success diff -Naur drupal-7.23/modules/node/node.pages.inc drupal-7.66/modules/node/node.pages.inc --- drupal-7.23/modules/node/node.pages.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/node.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -371,35 +371,37 @@ * @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); - field_attach_prepare_view('node', $nodes, 'full'); + $cloned_node->changed = REQUEST_TIME; + $nodes = array($cloned_node->nid => $cloned_node); // 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); @@ -542,6 +544,7 @@ if ($form_state['values']['confirm']) { $node = node_load($form_state['values']['nid']); node_delete($form_state['values']['nid']); + cache_clear_all(); watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title)); drupal_set_message(t('@type %title has been deleted.', array('@type' => node_type_get_name($node), '%title' => $node->title))); } diff -Naur drupal-7.23/modules/node/node.test drupal-7.66/modules/node/node.test --- drupal-7.23/modules/node/node.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/node.test 2019-04-17 22:20:46.000000000 +0200 @@ -457,10 +457,70 @@ } function setUp() { - parent::setUp(); + parent::setUp(array('taxonomy', 'node')); $web_user = $this->drupalCreateUser(array('edit own page content', 'create page content')); $this->drupalLogin($web_user); + + // Add a vocabulary so we can test different view modes. + $vocabulary = (object) array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + 'machine_name' => drupal_strtolower($this->randomName()), + 'help' => '', + 'nodes' => array('page' => 'page'), + ); + taxonomy_vocabulary_save($vocabulary); + + $this->vocabulary = $vocabulary; + + // Add a term to the vocabulary. + $term = (object) array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + // Use the first available text format. + 'format' => db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField(), + 'vid' => $this->vocabulary->vid, + 'vocabulary_machine_name' => $vocabulary->machine_name, + ); + taxonomy_term_save($term); + + $this->term = $term; + + // Set up a field and instance. + $this->field_name = drupal_strtolower($this->randomName()); + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'taxonomy_term_reference', + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => '0', + ), + ), + ) + ); + + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'node', + 'bundle' => 'page', + 'widget' => array( + 'type' => 'options_select', + ), + // Hide on full display but render on teaser. + 'display' => array( + 'default' => array( + 'type' => 'hidden', + ), + 'teaser' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance); } /** @@ -470,21 +530,26 @@ $langcode = LANGUAGE_NONE; $title_key = "title"; $body_key = "body[$langcode][0][value]"; + $term_key = "{$this->field_name}[$langcode]"; // Fill in node creation form and preview node. $edit = array(); $edit[$title_key] = $this->randomName(8); $edit[$body_key] = $this->randomName(16); + $edit[$term_key] = $this->term->tid; $this->drupalPost('node/add/page', $edit, t('Preview')); - // Check that the preview is displaying the title and body. + // Check that the preview is displaying the title, body, and term. $this->assertTitle(t('Preview | Drupal'), 'Basic page title is preview.'); $this->assertText($edit[$title_key], 'Title displayed.'); $this->assertText($edit[$body_key], 'Body displayed.'); + $this->assertText($this->term->name, 'Term displayed.'); - // Check that the title and body fields are displayed with the correct values. + // Check that the title, body, and term fields are displayed with the + // correct values. $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.'); $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.'); + $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.'); } /** @@ -494,6 +559,7 @@ $langcode = LANGUAGE_NONE; $title_key = "title"; $body_key = "body[$langcode][0][value]"; + $term_key = "{$this->field_name}[$langcode]"; // Force revision on "Basic page" content. variable_set('node_options_page', array('status', 'revision')); @@ -501,17 +567,21 @@ $edit = array(); $edit[$title_key] = $this->randomName(8); $edit[$body_key] = $this->randomName(16); + $edit[$term_key] = $this->term->tid; $edit['log'] = $this->randomName(32); $this->drupalPost('node/add/page', $edit, t('Preview')); - // Check that the preview is displaying the title and body. + // Check that the preview is displaying the title, body, and term. $this->assertTitle(t('Preview | Drupal'), 'Basic page title is preview.'); $this->assertText($edit[$title_key], 'Title displayed.'); $this->assertText($edit[$body_key], 'Body displayed.'); + $this->assertText($this->term->name, 'Term displayed.'); - // Check that the title and body fields are displayed with the correct values. + // Check that the title, body, and term fields are displayed with the + // correct values. $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.'); $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.'); + $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.'); // Check that the log field has the correct value. $this->assertFieldByName('log', $edit['log'], 'Log field displayed.'); @@ -571,6 +641,8 @@ ); try { + // An exception is generated by node_test_exception_node_insert() if the + // title is 'testing_transaction_exception'. node_save((object) $edit); $this->fail(t('Expected exception has not been thrown.')); } @@ -1363,6 +1435,22 @@ $node = node_load($node->nid); $this->assertEqual($node->title, 'updated_presave', 'Static cache has been cleared.'); } + + /** + * Tests saving a node on node insert. + * + * This test ensures that a node has been fully saved when hook_node_insert() + * is invoked, so that the node can be saved again in a hook implementation + * without errors. + * + * @see node_test_node_insert() + */ + function testNodeSaveOnInsert() { + // node_test_node_insert() triggers a save on insert if the title equals + // 'new'. + $node = $this->drupalCreateNode(array('title' => 'new')); + $this->assertEqual($node->title, 'Node ' . $node->nid, 'Node saved on node insert.'); + } } /** @@ -1430,7 +1518,7 @@ * Tests editing a node type using the UI. */ function testNodeTypeEditing() { - $web_user = $this->drupalCreateUser(array('bypass node access', 'administer content types')); + $web_user = $this->drupalCreateUser(array('bypass node access', 'administer content types', 'administer fields')); $this->drupalLogin($web_user); $instance = field_info_instance('node', 'body', 'page'); @@ -2426,6 +2514,35 @@ $output = token_replace($input, array('node' => $node), array('language' => $language, 'sanitize' => FALSE)); $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced.', array('%token' => $input))); } + + // Repeat for a node without a summary. + $settings['body'] = array(LANGUAGE_NONE => array(array('value' => $this->randomName(32), 'summary' => ''))); + $node = $this->drupalCreateNode($settings); + + // Load node (without summary) so that the body and summary fields are + // structured properly. + $node = node_load($node->nid); + $instance = field_info_instance('node', 'body', $node->type); + + // Generate and test sanitized token - use full body as expected value. + $tests = array(); + $tests['[node:summary]'] = _text_sanitize($instance, $langcode, $node->body[$langcode][0], 'value'); + + // Test to make sure that we generated something for each token. + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated for node without a summary.'); + + foreach ($tests as $input => $expected) { + $output = token_replace($input, array('node' => $node), array('language' => $language)); + $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced for node without a summary.', array('%token' => $input))); + } + + // Generate and test unsanitized tokens. + $tests['[node:summary]'] = $node->body[$langcode][0]['value']; + + foreach ($tests as $input => $expected) { + $output = token_replace($input, array('node' => $node), array('language' => $language, 'sanitize' => FALSE)); + $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced for node without a summary.', array('%token' => $input))); + } } } @@ -2651,8 +2768,8 @@ node_access_rebuild(); // Create some users. - $this->admin_user = $this->drupalCreateUser(array('access content', 'bypass node access')); - $this->content_admin_user = $this->drupalCreateUser(array('access content', 'administer content types')); + $this->admin_user = $this->drupalCreateUser(array('access content', 'bypass node access', 'administer fields')); + $this->content_admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer fields')); // Add a custom field to the page content type. $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); @@ -2735,8 +2852,8 @@ $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"]); @@ -2754,4 +2871,151 @@ $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.'); + } +} + +/** + * Tests the cache invalidation of node operations. + */ +class NodePageCacheTest extends NodeWebTestCase { + + /** + * An admin user with administrative permissions for nodes. + */ + protected $admin_user; + + public static function getInfo() { + return array( + 'name' => 'Node page cache test', + 'description' => 'Test cache invalidation of node operations.', + 'group' => 'Node', + ); + } + + function setUp() { + parent::setUp(); + + variable_set('cache', 1); + variable_set('page_cache_maximum_age', 300); + + $this->admin_user = $this->drupalCreateUser(array( + 'bypass node access', + 'access content overview', + 'administer nodes', + )); + } + + /** + * Tests deleting nodes clears page cache. + */ + public function testNodeDelete() { + $node_path = 'node/' . $this->drupalCreateNode()->nid; + + // Populate page cache. + $this->drupalGet($node_path); + + // Login and delete the node. + $this->drupalLogin($this->admin_user); + $this->drupalPost($node_path . '/delete', array(), t('Delete')); + + // Logout and check the node is not available. + $this->drupalLogout(); + $this->drupalGet($node_path); + $this->assertResponse(404); + + // Create two new nodes. + $nodes[0] = $this->drupalCreateNode(); + $nodes[1] = $this->drupalCreateNode(); + $node_path = 'node/' . $nodes[0]->nid; + + // Populate page cache. + $this->drupalGet($node_path); + + // Login and delete the nodes. + $this->drupalLogin($this->admin_user); + $this->drupalGet('admin/content'); + $edit = array( + 'operation' => 'delete', + 'nodes[' . $nodes[0]->nid . ']' => TRUE, + 'nodes[' . $nodes[1]->nid . ']' => TRUE, + ); + $this->drupalPost(NULL, $edit, t('Update')); + $this->drupalPost(NULL, array(), t('Delete')); + + // Logout and check the node is not available. + $this->drupalLogout(); + $this->drupalGet($node_path); + $this->assertResponse(404); + } +} + +/** + * Tests that multi-byte UTF-8 characters are stored and retrieved correctly. + */ +class NodeMultiByteUtf8Test extends NodeWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Multi-byte UTF-8', + 'description' => 'Test that multi-byte UTF-8 characters are stored and retrieved correctly.', + 'group' => 'Node', + ); + } + + /** + * Tests that multi-byte UTF-8 characters are stored and retrieved correctly. + */ + public function testMultiByteUtf8() { + $connection = Database::getConnection(); + // On MySQL, this test will only run if 'charset' is set to 'utf8mb4' in + // settings.php. + if (!($connection->utf8mb4IsSupported() && $connection->utf8mb4IsActive())) { + return; + } + $title = '🐙'; + $this->assertTrue(drupal_strlen($title, 'utf-8') < strlen($title), 'Title has multi-byte characters.'); + $node = $this->drupalCreateNode(array('title' => $title)); + $this->drupalGet('node/' . $node->nid); + $result = $this->xpath('//h1[@id="page-title"]'); + $this->assertEqual(trim((string) $result[0]), $title, 'The passed title was returned.'); + } + } diff -Naur drupal-7.23/modules/node/node.tokens.inc drupal-7.66/modules/node/node.tokens.inc --- drupal-7.23/modules/node/node.tokens.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/node.tokens.inc 2019-04-17 22:20:46.000000000 +0200 @@ -136,10 +136,29 @@ case 'body': case 'summary': if ($items = field_get_items('node', $node, 'body', $language_code)) { - $column = ($name == 'body') ? 'value' : 'summary'; $instance = field_info_instance('node', 'body', $node->type); $field_langcode = field_language('node', $node, 'body', $language_code); - $replacements[$original] = $sanitize ? _text_sanitize($instance, $field_langcode, $items[0], $column) : $items[0][$column]; + // If the summary was requested and is not empty, use it. + if ($name == 'summary' && !empty($items[0]['summary'])) { + $output = $sanitize ? _text_sanitize($instance, $field_langcode, $items[0], 'summary') : $items[0]['summary']; + } + // Attempt to provide a suitable version of the 'body' field. + else { + $output = $sanitize ? _text_sanitize($instance, $field_langcode, $items[0], 'value') : $items[0]['value']; + // A summary was requested. + if ($name == 'summary') { + if (isset($instance['display']['teaser']['settings']['trim_length'])) { + $trim_length = $instance['display']['teaser']['settings']['trim_length']; + } + else { + // Use default value. + $trim_length = NULL; + } + // Generate an optionally trimmed summary of the body field. + $output = text_summary($output, $instance['settings']['text_processing'] ? $items[0]['format'] : NULL, $trim_length); + } + } + $replacements[$original] = $output; } break; diff -Naur drupal-7.23/modules/node/tests/node_access_test.info drupal-7.66/modules/node/tests/node_access_test.info --- drupal-7.23/modules/node/tests/node_access_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/node/tests/node_access_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/node/tests/node_access_test.module drupal-7.66/modules/node/tests/node_access_test.module --- drupal-7.23/modules/node/tests/node_access_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/tests/node_access_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -211,7 +211,7 @@ } /** - * Implements hook_nodeapi_update(). + * Implements hook_node_update(). */ function node_access_test_node_update($node) { _node_access_test_node_write($node); diff -Naur drupal-7.23/modules/node/tests/node_test.info drupal-7.66/modules/node/tests/node_test.info --- drupal-7.23/modules/node/tests/node_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/node/tests/node_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/node/tests/node_test.module drupal-7.66/modules/node/tests/node_test.module --- drupal-7.23/modules/node/tests/node_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/node/tests/node_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -161,3 +161,21 @@ $view_mode = $change_view_mode; } } + +/** + * Implements hook_node_insert(). + * + * This tests saving a node on node insert. + * + * @see NodeSaveTest::testNodeSaveOnInsert() + */ +function node_test_node_insert($node) { + // Set the node title to the node ID and save. + if ($node->title == 'new') { + $node->title = 'Node '. $node->nid; + // Remove the is_new flag, so that the node is updated and not inserted + // again. + unset($node->is_new); + node_save($node); + } +} diff -Naur drupal-7.23/modules/node/tests/node_test_exception.info drupal-7.66/modules/node/tests/node_test_exception.info --- drupal-7.23/modules/node/tests/node_test_exception.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/node/tests/node_test_exception.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/openid/openid.inc drupal-7.66/modules/openid/openid.inc --- drupal-7.23/modules/openid/openid.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/openid/openid.inc 2019-04-17 22:20:46.000000000 +0200 @@ -158,6 +158,11 @@ 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) { @@ -380,6 +385,9 @@ /** * Return a nonce value - formatted per OpenID spec. + * + * NOTE: This nonce is not cryptographically secure and only suitable for use + * by the test framework. */ function _openid_nonce() { // YYYY-MM-DDThh:mm:ssZ, plus some optional extra unique characters. @@ -549,7 +557,7 @@ } do { - $bytes = "\x00" . _openid_get_bytes($nbytes); + $bytes = "\x00" . drupal_random_bytes($nbytes); $n = _openid_dh_binary_to_long($bytes); // Keep looping if this value is in the low duplicated range. } while (_openid_math_cmp($n, $duplicate) < 0); @@ -558,23 +566,7 @@ } function _openid_get_bytes($num_bytes) { - $f = &drupal_static(__FUNCTION__); - $bytes = ''; - if (!isset($f)) { - $f = @fopen(OPENID_RAND_SOURCE, "r"); - } - if (!$f) { - // pseudorandom used - $bytes = ''; - for ($i = 0; $i < $num_bytes; $i += 4) { - $bytes .= pack('L', mt_rand()); - } - $bytes = substr($bytes, 0, $num_bytes); - } - else { - $bytes = fread($f, $num_bytes); - } - return $bytes; + return drupal_random_bytes($num_bytes); } function _openid_response($str = NULL) { diff -Naur drupal-7.23/modules/openid/openid.info drupal-7.66/modules/openid/openid.info --- drupal-7.23/modules/openid/openid.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/openid/openid.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x files[] = openid.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/openid/openid.install drupal-7.66/modules/openid/openid.install --- drupal-7.23/modules/openid/openid.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/openid/openid.install 2019-04-17 22:20:46.000000000 +0200 @@ -15,13 +15,14 @@ 'idp_endpoint_uri' => array( 'type' => 'varchar', 'length' => 255, - 'description' => 'URI of the OpenID Provider endpoint.', + 'not null' => TRUE, + 'description' => 'Primary Key: URI of the OpenID Provider endpoint.', ), 'assoc_handle' => array( 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, - 'description' => 'Primary Key: Used to refer to this association in subsequent messages.', + 'description' => 'Used to refer to this association in subsequent messages.', ), 'assoc_type' => array( 'type' => 'varchar', @@ -51,7 +52,10 @@ 'description' => 'The lifetime, in seconds, of this association.', ), ), - 'primary key' => array('assoc_handle'), + 'primary key' => array('idp_endpoint_uri'), + 'unique keys' => array( + 'assoc_handle' => array('assoc_handle'), + ), ); $schema['openid_nonce'] = array( @@ -158,3 +162,69 @@ /** * @} End of "addtogroup updates-6.x-to-7.x". */ + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Bind associations to their providers. + */ +function openid_update_7000() { + db_drop_table('openid_association'); + + $schema = array( + 'description' => 'Stores temporary shared key association information for OpenID authentication.', + 'fields' => array( + 'idp_endpoint_uri' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'Primary Key: URI of the OpenID Provider endpoint.', + ), + 'assoc_handle' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'Used to refer to this association in subsequent messages.', + ), + 'assoc_type' => array( + 'type' => 'varchar', + 'length' => 32, + 'description' => 'The signature algorithm used: one of HMAC-SHA1 or HMAC-SHA256.', + ), + 'session_type' => array( + 'type' => 'varchar', + 'length' => 32, + 'description' => 'Valid association session types: "no-encryption", "DH-SHA1", and "DH-SHA256".', + ), + 'mac_key' => array( + 'type' => 'varchar', + 'length' => 255, + 'description' => 'The MAC key (shared secret) for this association.', + ), + 'created' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'UNIX timestamp for when the association was created.', + ), + 'expires_in' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The lifetime, in seconds, of this association.', + ), + ), + 'primary key' => array('idp_endpoint_uri'), + 'unique keys' => array( + 'assoc_handle' => array('assoc_handle'), + ), + ); + db_create_table('openid_association', $schema); +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ diff -Naur drupal-7.23/modules/openid/openid.module drupal-7.66/modules/openid/openid.module --- drupal-7.23/modules/openid/openid.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/openid/openid.module 2019-04-17 22:20:46.000000000 +0200 @@ -365,14 +365,20 @@ // to the OpenID Provider, we need to do discovery on the returned // identififer to make sure that the provider is authorized to // respond on behalf of this. - if ($response_claimed_id != $claimed_id) { + if ($response_claimed_id != $claimed_id || $response_claimed_id != $response['openid.identity']) { $discovery = openid_discovery($response['openid.claimed_id']); + $uris = array(); if ($discovery && !empty($discovery['services'])) { - $uris = array(); foreach ($discovery['services'] as $discovered_service) { - if (in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) || in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) { - $uris[] = $discovered_service['uri']; + if (!in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) && !in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) { + continue; } + // The OP-Local Identifier (if different than the Claimed + // Identifier) must be present in the XRDS document. + if ($response_claimed_id != $response['openid.identity'] && (!isset($discovered_service['identity']) || $discovered_service['identity'] != $response['openid.identity'])) { + continue; + } + $uris[] = $discovered_service['uri']; } } if (!in_array($service['uri'], $uris)) { @@ -839,7 +845,7 @@ // direct verification: ignore the openid.assoc_handle, even if present. // See http://openid.net/specs/openid-authentication-2_0.html#rfc.section.11.4.1 if (!empty($response['openid.assoc_handle']) && empty($response['openid.invalidate_handle'])) { - $association = db_query("SELECT * FROM {openid_association} WHERE assoc_handle = :assoc_handle", array(':assoc_handle' => $response['openid.assoc_handle']))->fetchObject(); + $association = db_query("SELECT * FROM {openid_association} WHERE idp_endpoint_uri = :endpoint AND assoc_handle = :assoc_handle", array(':endpoint' => $service['uri'], ':assoc_handle' => $response['openid.assoc_handle']))->fetchObject(); } if ($association && isset($association->session_type)) { @@ -871,6 +877,7 @@ // database to avoid reusing it again on a subsequent authentication request. // See http://openid.net/specs/openid-authentication-2_0.html#rfc.section.11.4.2.2 db_delete('openid_association') + ->condition('idp_endpoint_uri', $service['uri']) ->condition('assoc_handle', $response['invalidate_handle']) ->execute(); } diff -Naur drupal-7.23/modules/openid/openid.test drupal-7.66/modules/openid/openid.test --- drupal-7.23/modules/openid/openid.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/openid/openid.test 2019-04-17 22:20:46.000000000 +0200 @@ -94,7 +94,7 @@ $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE, 'fragment' => $this->randomName())); // Tell openid_test.module to respond with this identifier. If the fragment // part is present in the identifier, it should be retained. - variable_set('openid_test_response', array('openid.claimed_id' => $identity)); + variable_set('openid_test_response', array('openid.claimed_id' => $identity, 'openid.identity' => openid_normalize($identity))); $this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, 'http://specs.openid.net/auth/2.0/identifier_select', $identity); variable_set('openid_test_response', array()); @@ -680,11 +680,11 @@ * Test _openid_dh_XXX_to_XXX() functions. */ function testConversion() { - $this->assertEqual(_openid_dh_long_to_base64('12345678901234567890123456789012345678901234567890'), 'CHJ/Y2mq+DyhUCZ0evjH8ZbOPwrS', '_openid_dh_long_to_base64() returned expected result.'); - $this->assertEqual(_openid_dh_base64_to_long('BsH/g8Nrpn2dtBSdu/sr1y8hxwyx'), '09876543210987654321098765432109876543210987654321', '_openid_dh_base64_to_long() returned expected result.'); + $this->assertIdentical(_openid_dh_long_to_base64('12345678901234567890123456789012345678901234567890'), 'CHJ/Y2mq+DyhUCZ0evjH8ZbOPwrS', '_openid_dh_long_to_base64() returned expected result.'); + $this->assertIdentical(_openid_dh_base64_to_long('BsH/g8Nrpn2dtBSdu/sr1y8hxwyx'), '9876543210987654321098765432109876543210987654321', '_openid_dh_base64_to_long() returned expected result.'); - $this->assertEqual(_openid_dh_long_to_binary('12345678901234567890123456789012345678901234567890'), "\x08r\x7fci\xaa\xf8<\xa1P&tz\xf8\xc7\xf1\x96\xce?\x0a\xd2", '_openid_dh_long_to_binary() returned expected result.'); - $this->assertEqual(_openid_dh_binary_to_long("\x06\xc1\xff\x83\xc3k\xa6}\x9d\xb4\x14\x9d\xbb\xfb+\xd7/!\xc7\x0c\xb1"), '09876543210987654321098765432109876543210987654321', '_openid_dh_binary_to_long() returned expected result.'); + $this->assertIdentical(_openid_dh_long_to_binary('12345678901234567890123456789012345678901234567890'), "\x08r\x7fci\xaa\xf8<\xa1P&tz\xf8\xc7\xf1\x96\xce?\x0a\xd2", '_openid_dh_long_to_binary() returned expected result.'); + $this->assertIdentical(_openid_dh_binary_to_long("\x06\xc1\xff\x83\xc3k\xa6}\x9d\xb4\x14\x9d\xbb\xfb+\xd7/!\xc7\x0c\xb1"), '9876543210987654321098765432109876543210987654321', '_openid_dh_binary_to_long() returned expected result.'); } /** @@ -695,13 +695,6 @@ } /** - * Test _openid_get_bytes(). - */ - function testOpenidGetBytes() { - $this->assertEqual(strlen(_openid_get_bytes(20)), 20, '_openid_get_bytes() returned expected result.'); - } - - /** * Test _openid_signature(). */ function testOpenidSignature() { diff -Naur drupal-7.23/modules/openid/tests/openid_test.info drupal-7.66/modules/openid/tests/openid_test.info --- drupal-7.23/modules/openid/tests/openid_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/openid/tests/openid_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ dependencies[] = openid hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/openid/tests/openid_test.install drupal-7.66/modules/openid/tests/openid_test.install --- drupal-7.23/modules/openid/tests/openid_test.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/openid/tests/openid_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -13,5 +13,5 @@ // Generate a MAC key (Message Authentication Code) used for signing messages. // The variable is base64-encoded, because variables cannot contain non-UTF-8 // data. - variable_set('openid_test_mac_key', base64_encode(_openid_get_bytes(20))); + variable_set('openid_test_mac_key', drupal_random_key(20)); } diff -Naur drupal-7.23/modules/openid/tests/openid_test.module drupal-7.66/modules/openid/tests/openid_test.module --- drupal-7.23/modules/openid/tests/openid_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/openid/tests/openid_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -150,6 +150,7 @@ http://specs.openid.net/auth/2.0/server ' . url('openid-test/endpoint', array('absolute' => TRUE)) . ' + ' . url('openid-test/yadis/xrds/server', array('absolute' => TRUE)) . ' '; } elseif (arg(3) == 'delegate') { diff -Naur drupal-7.23/modules/overlay/overlay-parent.js drupal-7.66/modules/overlay/overlay-parent.js --- drupal-7.23/modules/overlay/overlay-parent.js 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/overlay/overlay-parent.js 2019-04-17 22:20:46.000000000 +0200 @@ -350,7 +350,7 @@ * TRUE if the URL represents an administrative link, FALSE otherwise. */ Drupal.overlay.isAdminLink = function (url) { - if (Drupal.overlay.isExternalLink(url)) { + if (!Drupal.urlIsLocal(url)) { return false; } @@ -378,6 +378,8 @@ /** * Determine whether a link is external to the site. * + * Deprecated. Use Drupal.urlIsLocal() instead. + * * @param url * The URL to be tested. * @@ -385,8 +387,28 @@ * TRUE if the URL is external to the site, FALSE otherwise. */ Drupal.overlay.isExternalLink = function (url) { - var re = RegExp('^((f|ht)tps?:)?//(?!' + window.location.host + ')'); - return re.test(url); + return !Drupal.urlIsLocal(url); +}; + +/** + * Constructs an internal URL (relative to this site) from the provided path. + * + * For example, if the provided path is 'admin' and the site is installed at + * http://example.com/drupal, this function will return '/drupal/admin'. + * + * @param path + * The internal path, without any leading slash. + * + * @return + * The internal URL derived from the provided path, or null if a valid + * internal path cannot be constructed (for example, if an attempt to create + * an external link is detected). + */ +Drupal.overlay.getInternalUrl = function (path) { + var url = Drupal.settings.basePath + path; + if (Drupal.urlIsLocal(url)) { + return url; + } }; /** @@ -577,7 +599,7 @@ // If the link contains the overlay-restore class and the overlay-context // state is set, also update the parent window's location. var parentLocation = ($target.hasClass('overlay-restore') && typeof $.bbq.getState('overlay-context') == 'string') - ? Drupal.settings.basePath + $.bbq.getState('overlay-context') + ? this.getInternalUrl($.bbq.getState('overlay-context')) : null; href = this.fragmentizeLink($target.get(0), parentLocation); // Only override default behavior when left-clicking and user is not @@ -657,11 +679,15 @@ } // Get the overlay URL from the current URL fragment. + var internalUrl = null; var state = $.bbq.getState('overlay'); if (state) { + internalUrl = this.getInternalUrl(state); + } + if (internalUrl) { // Append render variable, so the server side can choose the right // rendering and add child frame code to the page if needed. - var url = $.param.querystring(Drupal.settings.basePath + state, { render: 'overlay' }); + var url = $.param.querystring(internalUrl, { render: 'overlay' }); this.open(url); this.resetActiveClass(this.getPath(Drupal.settings.basePath + state)); diff -Naur drupal-7.23/modules/overlay/overlay.info drupal-7.66/modules/overlay/overlay.info --- drupal-7.23/modules/overlay/overlay.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/overlay/overlay.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ version = VERSION core = 7.x -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/overlay/overlay.module drupal-7.66/modules/overlay/overlay.module --- drupal-7.23/modules/overlay/overlay.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/overlay/overlay.module 2019-04-17 22:20:46.000000000 +0200 @@ -79,6 +79,20 @@ } /** + * Implements hook_form_alter(). + */ +function overlay_form_alter(&$form, &$form_state) { + // Add a hidden element to prevent dropping out of the overlay when a form is + // submitted inside the overlay using a GET method. + if (isset($form['#method']) && $form['#method'] == 'get' && isset($_REQUEST['render']) && $_REQUEST['render'] == 'overlay' && !isset($form['render'])) { + $form['render'] = array( + '#type' => 'hidden', + '#value' => 'overlay', + ); + } +} + +/** * Implements hook_form_FORM_ID_alter(). */ function overlay_form_user_profile_form_alter(&$form, &$form_state) { @@ -146,6 +160,10 @@ // If this page shouldn't be rendered inside the overlay, redirect to the // parent. elseif (!path_is_admin($current_path)) { + // Prevent open redirects by ensuring the current path is not an absolute URL. + if (url_is_external($current_path)) { + $current_path = ''; + } overlay_close_dialog($current_path, array('query' => drupal_get_query_parameters(NULL, array('q', 'render')))); } diff -Naur drupal-7.23/modules/path/path.info drupal-7.66/modules/path/path.info --- drupal-7.23/modules/path/path.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/path/path.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = path.test configure = admin/config/search/path -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/path/path.module drupal-7.66/modules/path/path.module --- drupal-7.23/modules/path/path.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/path/path.module 2019-04-17 22:20:46.000000000 +0200 @@ -185,7 +185,7 @@ * Implements hook_node_insert(). */ function path_node_insert($node) { - if (isset($node->path)) { + if (isset($node->path) && isset($node->path['alias'])) { $path = $node->path; $path['alias'] = trim($path['alias']); // Only save a non-empty alias. @@ -205,9 +205,9 @@ function path_node_update($node) { if (isset($node->path)) { $path = $node->path; - $path['alias'] = trim($path['alias']); + $path['alias'] = isset($path['alias']) ? trim($path['alias']) : ''; // Delete old alias if user erased it. - if (!empty($path['pid']) && empty($path['alias'])) { + if (!empty($path['pid']) && !$path['alias']) { path_delete($path['pid']); } path_node_insert($node); diff -Naur drupal-7.23/modules/path/path.test drupal-7.66/modules/path/path.test --- drupal-7.23/modules/path/path.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/path/path.test 2019-04-17 22:20:46.000000000 +0200 @@ -21,7 +21,7 @@ parent::setUp('path'); // Create test user and login. - $web_user = $this->drupalCreateUser(array('create page content', 'edit own page content', 'administer url aliases', 'create url aliases')); + $web_user = $this->drupalCreateUser(array('create page content', 'edit own page content', 'administer url aliases', 'create url aliases', 'access content overview')); $this->drupalLogin($web_user); } @@ -160,6 +160,34 @@ $this->drupalGet($edit['path[alias]']); $this->assertNoText($node1->title, 'Alias was successfully deleted.'); $this->assertResponse(404); + + // Create third test node. + $node3 = $this->drupalCreateNode(); + + // Create an invalid alias with a leading slash and verify that the slash + // is removed when the link is generated. This ensures that URL aliases + // cannot be used to inject external URLs. + // @todo The user interface should either display an error message or + // automatically trim these invalid aliases, rather than allowing them to + // be silently created, at which point the functional aspects of this + // test will need to be moved elsewhere and switch to using a + // programmatically-created alias instead. + $alias = $this->randomName(8); + $edit = array('path[alias]' => '/' . $alias); + $this->drupalPost('node/' . $node3->nid . '/edit', $edit, t('Save')); + $this->drupalGet('admin/content'); + // This checks the link href before clicking it, rather than using + // DrupalWebTestCase::assertUrl() after clicking it, because the test + // browser does not always preserve the correct number of slashes in the + // URL when it visits internal links; using DrupalWebTestCase::assertUrl() + // would actually make the test pass unconditionally on the testbot (or + // anywhere else where Drupal is installed in a subdirectory). + $link_xpath = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $node3->title)); + $link_href = (string) $link_xpath[0]['href']; + $link_prefix = base_path() . (variable_get('clean_url', 0) ? '' : '?q='); + $this->assertEqual($link_href, $link_prefix . $alias); + $this->clickLink($node3->title); + $this->assertResponse(404); } /** diff -Naur drupal-7.23/modules/php/php.info drupal-7.66/modules/php/php.info --- drupal-7.23/modules/php/php.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/php/php.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x files[] = php.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/php/php.module drupal-7.66/modules/php/php.module --- drupal-7.23/modules/php/php.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/php/php.module 2019-04-17 22:20:46.000000000 +0200 @@ -17,7 +17,7 @@ $output .= '

          ' . t('Uses') . '

          '; $output .= '
          '; $output .= '
          ' . t('Enabling execution of PHP in text fields') . '
          '; - $output .= '
          ' . t('The PHP filter module allows users with the proper permissions to include custom PHP code that will get executed when pages of your site are processed. While this is a powerful and flexible feature if used by a trusted user with PHP experience, it is a significant and dangerous security risk in the hands of a malicious or inexperienced user. Even a trusted user may accidentally compromise the site by entering malformed or incorrect PHP code. Only the most trusted users should be granted permission to use the PHP filter, and all PHP code added through the PHP filter should be carefully examined before use. Example PHP snippets can be found on Drupal.org.', array('@php-snippets' => url('http://http://drupal.org/documentation/customization/php-snippets'))) . '
          '; + $output .= '
          ' . t('The PHP filter module allows users with the proper permissions to include custom PHP code that will get executed when pages of your site are processed. While this is a powerful and flexible feature if used by a trusted user with PHP experience, it is a significant and dangerous security risk in the hands of a malicious or inexperienced user. Even a trusted user may accidentally compromise the site by entering malformed or incorrect PHP code. Only the most trusted users should be granted permission to use the PHP filter, and all PHP code added through the PHP filter should be carefully examined before use. Example PHP snippets can be found on Drupal.org.', array('@php-snippets' => url('http://drupal.org/documentation/customization/php-snippets'))) . '
          '; $output .= '
          '; return $output; } diff -Naur drupal-7.23/modules/poll/poll.info drupal-7.66/modules/poll/poll.info --- drupal-7.23/modules/poll/poll.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/poll/poll.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = poll.test stylesheets[all][] = poll.css -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/poll/poll.module drupal-7.66/modules/poll/poll.module --- drupal-7.23/modules/poll/poll.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/poll/poll.module 2019-04-17 22:20:46.000000000 +0200 @@ -191,7 +191,6 @@ 'base' => 'poll', 'description' => t('A poll is a question with a set of possible responses. A poll, once created, automatically provides a simple running count of the number of votes received for each response.'), 'title_label' => t('Question'), - 'has_body' => FALSE, ) ); } @@ -249,6 +248,7 @@ '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => $node->title, + '#maxlength' => 255, '#weight' => -5, ); @@ -631,9 +631,6 @@ * The node object to load. */ function poll_block_latest_poll_view($node) { - global $user; - $output = ''; - // This is necessary for shared objects because PHP doesn't copy objects, but // passes them by reference. So when the objects are cached it can result in // the wrong output being displayed on subsequent calls. The cloning and @@ -674,9 +671,6 @@ * Implements hook_view(). */ function poll_view($node, $view_mode) { - global $user; - $output = ''; - if (!empty($node->allowvotes) && empty($node->show_results)) { $node->content['poll_view_voting'] = drupal_get_form('poll_view_voting', $node); } @@ -694,7 +688,7 @@ function poll_teaser($node) { $teaser = NULL; if (is_array($node->choice)) { - foreach ($node->choice as $k => $choice) { + foreach ($node->choice as $choice) { if ($choice['chtext'] != '') { $teaser .= '* ' . check_plain($choice['chtext']) . "\n"; } @@ -720,7 +714,6 @@ '#type' => 'radios', '#title' => t('Choices'), '#title_display' => 'invisible', - '#default_value' => -1, '#options' => $list, ); } @@ -748,7 +741,7 @@ * 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 +918,6 @@ * * @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 -Naur drupal-7.23/modules/poll/poll.test drupal-7.66/modules/poll/poll.test --- drupal-7.23/modules/poll/poll.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/poll/poll.test 2019-04-17 22:20:46.000000000 +0200 @@ -315,6 +315,11 @@ $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 -Naur drupal-7.23/modules/profile/profile.info drupal-7.66/modules/profile/profile.info --- drupal-7.23/modules/profile/profile.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/profile/profile.info 2019-04-17 22:39:36.000000000 +0200 @@ -11,8 +11,7 @@ ; See user_system_info_alter(). hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/profile/profile.module drupal-7.66/modules/profile/profile.module --- drupal-7.23/modules/profile/profile.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/profile/profile.module 2019-04-17 22:20:46.000000000 +0200 @@ -571,6 +571,7 @@ // Supply filtered version of $fields that have values. foreach ($variables['fields'] as $field) { if ($field->value) { + $variables['profile'][$field->name] = new stdClass(); $variables['profile'][$field->name]->title = $field->title; $variables['profile'][$field->name]->value = $field->value; $variables['profile'][$field->name]->type = $field->type; diff -Naur drupal-7.23/modules/profile/profile.pages.inc drupal-7.66/modules/profile/profile.pages.inc --- drupal-7.23/modules/profile/profile.pages.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/profile/profile.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -17,17 +17,15 @@ if ($name && $field->fid) { // Only allow browsing of fields that have a page title set. if (empty($field->page)) { - drupal_not_found(); - return; + return MENU_NOT_FOUND; } // Do not allow browsing of private and hidden fields by non-admins. if (!user_access('administer users') && ($field->visibility == PROFILE_PRIVATE || $field->visibility == PROFILE_HIDDEN)) { - drupal_access_denied(); - return; + return MENU_ACCESS_DENIED; } // Compile a list of fields to show. - $fields = db_query('SELECT name, title, type, weight, page FROM {profile_field} WHERE fid <> :fid AND visibility = :visibility ORDER BY weight', array( + $fields = db_query('SELECT name, title, type, weight, page, visibility FROM {profile_field} WHERE fid <> :fid AND visibility = :visibility ORDER BY weight', array( ':fid' => $field->fid, ':visibility' => PROFILE_PUBLIC_LISTINGS, ))->fetchAll(); @@ -54,8 +52,7 @@ $query->condition('v.value', '%' . db_like($value) . '%', 'LIKE'); break; default: - drupal_not_found(); - return; + return MENU_NOT_FOUND; } $uids = $query @@ -85,7 +82,7 @@ return $output; } elseif ($name && !$field->fid) { - drupal_not_found(); + return MENU_NOT_FOUND; } else { // Compile a list of fields to show. diff -Naur drupal-7.23/modules/profile/profile.test drupal-7.66/modules/profile/profile.test --- drupal-7.23/modules/profile/profile.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/profile/profile.test 2019-04-17 22:20:46.000000000 +0200 @@ -42,25 +42,25 @@ $this->drupalPost('admin/config/people/profile/add/' . $type, $edit, t('Save field')); $fid = db_query("SELECT fid FROM {profile_field} WHERE title = :title", array(':title' => $title))->fetchField(); - $this->assertTrue($fid, t('New Profile field has been entered in the database')); + $this->assertTrue($fid, 'New Profile field has been entered in the database'); // Check that the new field is appearing on the user edit form. $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); // Checking field. if ($type == 'date') { - $this->assertField($form_name . '[month]', t('Found month selection field')); - $this->assertField($form_name . '[day]', t('Found day selection field')); - $this->assertField($form_name . '[year]', t('Found day selection field')); + $this->assertField($form_name . '[month]', 'Found month selection field'); + $this->assertField($form_name . '[day]', 'Found day selection field'); + $this->assertField($form_name . '[year]', 'Found day selection field'); } else { - $this->assertField($form_name , t('Found form named @name', array('@name' => $form_name))); + $this->assertField($form_name , format_string('Found form named @name', array('@name' => $form_name))); } // Checking name. - $this->assertText($title, t('Checking title for field %title', array('%title' => $title))); + $this->assertText($title, format_string('Checking title for field %title', array('%title' => $title))); // Checking explanation. - $this->assertText($edit['explanation'], t('Checking explanation for field %title', array('%title' => $title))); + $this->assertText($edit['explanation'], format_string('Checking explanation for field %title', array('%title' => $title))); return array( 'fid' => $fid, @@ -96,18 +96,18 @@ // Checking field. if ($type == 'date') { - $this->assertField($form_name . '[month]', t('Found month selection field')); - $this->assertField($form_name . '[day]', t('Found day selection field')); - $this->assertField($form_name . '[year]', t('Found day selection field')); + $this->assertField($form_name . '[month]', 'Found month selection field'); + $this->assertField($form_name . '[day]', 'Found day selection field'); + $this->assertField($form_name . '[year]', 'Found day selection field'); } else { - $this->assertField($form_name , t('Found form named @name', array('@name' => $form_name))); + $this->assertField($form_name , format_string('Found form named @name', array('@name' => $form_name))); } // Checking name. - $this->assertText($title, t('Checking title for field %title', array('%title' => $title))); + $this->assertText($title, format_string('Checking title for field %title', array('%title' => $title))); // Checking explanation. - $this->assertText($edit['explanation'], t('Checking explanation for field %title', array('%title' => $title))); + $this->assertText($edit['explanation'], format_string('Checking explanation for field %title', array('%title' => $title))); return array( 'fid' => $fid, @@ -141,11 +141,11 @@ // Check profile page. $content = $this->drupalGet('user/' . $this->normal_user->uid); - $this->assertText($field['title'], t('Found profile field with title %title', array('%title' => $field['title']))); + $this->assertText($field['title'], format_string('Found profile field with title %title', array('%title' => $field['title']))); if ($field['type'] != 'checkbox') { // $value must be cast to a string in order to be found by assertText. - $this->assertText("$value", t('Found profile field with value %value', array('%value' => $value))); + $this->assertText("$value", format_string('Found profile field with value %value', array('%value' => $value))); } return $value; @@ -160,7 +160,7 @@ function deleteProfileField($field) { $this->drupalPost('admin/config/people/profile/delete/' . $field['fid'], array(), t('Delete')); $this->drupalGet('admin/config/people/profile'); - $this->assertNoText($field['title'], t('Checking deleted field %title', array('%title' => $field['title']))); + $this->assertNoText($field['title'], format_string('Checking deleted field %title', array('%title' => $field['title']))); } } @@ -270,9 +270,9 @@ // Check profile page. $this->drupalGet('user/' . $this->normal_user->uid); - $this->assertText($field['title'], t('Found profile field with title %title', array('%title' => $field['title']))); + $this->assertText($field['title'], format_string('Found profile field with title %title', array('%title' => $field['title']))); - $this->assertText('01/09/1983', t('Found date profile field.')); + $this->assertText('01/09/1983', 'Found date profile field.'); $edit = array( 'name' => $field['form_name'], @@ -305,10 +305,10 @@ $this->setProfileField($field2, $this->randomName(8)); $profile_edit = $this->drupalGet('user/' . $this->normal_user->uid . '/edit/' . $category); - $this->assertTrue(strpos($profile_edit, $field1['title']) > strpos($profile_edit, $field2['title']), t('Profile field weights are respected on the user edit form.')); + $this->assertTrue(strpos($profile_edit, $field1['title']) > strpos($profile_edit, $field2['title']), 'Profile field weights are respected on the user edit form.'); $profile_page = $this->drupalGet('user/' . $this->normal_user->uid); - $this->assertTrue(strpos($profile_page, $field1['title']) > strpos($profile_page, $field2['title']), t('Profile field weights are respected on the user profile page.')); + $this->assertTrue(strpos($profile_page, $field1['title']) > strpos($profile_page, $field2['title']), 'Profile field weights are respected on the user profile page.'); } } @@ -339,20 +339,30 @@ $this->setProfileField($field, $field['value']); // Set some html for what we want to see in the page output later. - $autocomplete_html = ''; - $field_html = ''; + // Autocomplete always uses non-clean URLs. + $current_clean_url = isset($GLOBALS['conf']['clean_url']) ? $GLOBALS['conf']['clean_url'] : NULL; + $GLOBALS['conf']['clean_url'] = 0; + $autocomplete_url = url('profile/autocomplete/' . $field['fid'], array('absolute' => TRUE, 'script' => 'index.php')); + $GLOBALS['conf']['clean_url'] = $current_clean_url; + $autocomplete_id = drupal_html_id('edit-' . $field['form_name'] . '-autocomplete'); + $autocomplete_html = ''; // Check that autocompletion html is found on the user's profile edit page. $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); - $this->assertRaw($autocomplete_html, t('Autocomplete found.')); - $this->assertRaw('misc/autocomplete.js', t('Autocomplete JavaScript found.')); - $this->assertRaw('class="form-text form-autocomplete"', t('Autocomplete form element class found.')); + $this->assertRaw($autocomplete_html, 'Autocomplete found.'); + $this->assertFieldByXPath( + '//input[@type="text" and @name="' . $field['form_name'] . '" and contains(@class, "form-autocomplete")]', + '', + 'Text input field found' + ); + $this->assertRaw('misc/autocomplete.js', 'Autocomplete JavaScript found.'); + $this->assertRaw('class="form-text form-autocomplete"', 'Autocomplete form element class found.'); // Check the autocompletion path using the first letter of our user's profile // field value to make sure access is allowed and a valid result if found. $this->drupalGet('profile/autocomplete/' . $field['fid'] . '/' . $field['value'][0]); - $this->assertResponse(200, t('Autocomplete path allowed to user with permission.')); - $this->assertRaw($field['value'], t('Autocomplete value found.')); + $this->assertResponse(200, 'Autocomplete path allowed to user with permission.'); + $this->assertRaw($field['value'], 'Autocomplete value found.'); // Logout and login with a user without the 'access user profiles' permission. $this->drupalLogout(); @@ -360,11 +370,11 @@ // Check that autocompletion html is not found on the user's profile edit page. $this->drupalGet('user/' . $this->normal_user->uid . '/edit/' . $category); - $this->assertNoRaw($autocomplete_html, t('Autocomplete not found.')); + $this->assertNoRaw($autocomplete_html, 'Autocomplete not found.'); // User should be denied access to the profile autocomplete path. $this->drupalGet('profile/autocomplete/' . $field['fid'] . '/' . $field['value'][0]); - $this->assertResponse(403, t('Autocomplete path denied to user without permission.')); + $this->assertResponse(403, 'Autocomplete path denied to user without permission.'); } } @@ -403,48 +413,48 @@ $edit = array(); $edit['blocks[profile_author-information][region]'] = 'footer'; $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - $this->assertText(t('The block settings have been updated.'), t('Block successfully move to footer region.')); + $this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.'); // Enable field 1. $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => TRUE, ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.'); // Visit the node and confirm that the field is displayed. $this->drupalGet('node/' . $this->node->nid); - $this->assertRaw($this->value1, t('Field 1 is displayed')); - $this->assertNoRaw($this->value2, t('Field 2 is not displayed')); + $this->assertRaw($this->value1, 'Field 1 is displayed'); + $this->assertNoRaw($this->value2, 'Field 2 is not displayed'); // Enable only field 2. $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => FALSE, 'profile_block_author_fields[' . $this->field2['form_name'] . ']' => TRUE, ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.'); // Visit the node and confirm that the field is displayed. $this->drupalGet('node/' . $this->node->nid); - $this->assertNoRaw($this->value1, t('Field 1 is not displayed')); - $this->assertRaw($this->value2, t('Field 2 is displayed')); + $this->assertNoRaw($this->value1, 'Field 1 is not displayed'); + $this->assertRaw($this->value2, 'Field 2 is displayed'); // Enable both fields. $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => TRUE, 'profile_block_author_fields[' . $this->field2['form_name'] . ']' => TRUE, ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.'); // Visit the node and confirm that the field is displayed. $this->drupalGet('node/' . $this->node->nid); - $this->assertRaw($this->value1, t('Field 1 is displayed')); - $this->assertRaw($this->value2, t('Field 2 is displayed')); + $this->assertRaw($this->value1, 'Field 1 is displayed'); + $this->assertRaw($this->value2, 'Field 2 is displayed'); // Enable the link to the user profile. $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( 'profile_block_author_fields[user_profile]' => TRUE, ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.'); // Visit the node and confirm that the user profile link is displayed. $this->drupalGet('node/' . $this->node->nid); diff -Naur drupal-7.23/modules/rdf/rdf.info drupal-7.66/modules/rdf/rdf.info --- drupal-7.23/modules/rdf/rdf.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/rdf/rdf.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x files[] = rdf.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/rdf/rdf.module drupal-7.66/modules/rdf/rdf.module --- drupal-7.23/modules/rdf/rdf.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/rdf/rdf.module 2019-04-17 22:20:46.000000000 +0200 @@ -190,17 +190,33 @@ * 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')) + $mappings = _rdf_mapping_load_multiple($type, array($bundle)); + return $mappings ? reset($mappings) : 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', $bundle) + ->condition('bundle', $bundles) ->execute() - ->fetchField(); + ->fetchAllKeyed(); - if (!$mapping) { - return array(); + foreach ($mappings as $bundle => $mapping) { + $mappings[$bundle] = unserialize($mapping); } - return unserialize($mapping); + return $mappings; } /** @@ -368,10 +384,13 @@ 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 @@ $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

          tag which might contain HTML code, we specify an empty - // datatype to ensure the value of the title read by the RDFa parsers is a - // literal. - $variables['title_attributes_array']['property'] = empty($variables['node']->rdf_mapping['title']['predicates']) ? NULL : $variables['node']->rdf_mapping['title']['predicates']; - $variables['title_attributes_array']['datatype'] = ''; - - // In full node mode, the title is not displayed by node.tpl.php so it is - // added in the tag of the HTML page. - if ($variables['page']) { - $element = array( - '#tag' => 'meta', - '#attributes' => array( - 'content' => $variables['title'], - 'about' => $variables['node_url'], + // Adds RDFa markup about the title of the node to the title_suffix. + if (!empty($variables['node']->rdf_mapping['title']['predicates'])) { + $variables['title_suffix']['rdf_meta_title'] = array( + '#theme' => 'rdf_metadata', + '#metadata' => array( + array( + 'property' => $variables['node']->rdf_mapping['title']['predicates'], + 'content' => $variables['node']->title, + ), ), ); - if (!empty($variables['node']->rdf_mapping['title']['predicates'])) { - $element['#attributes']['property'] = $variables['node']->rdf_mapping['title']['predicates']; - } - drupal_add_html_head($element, 'rdf_node_title'); } // Adds RDFa markup for the date. @@ -511,35 +520,20 @@ } // Adds RDFa markup annotating the number of comments a node has. - if (isset($variables['node']->comment_count) && !empty($variables['node']->rdf_mapping['comment_count']['predicates'])) { - // Annotates the 'x comments' link in teaser view. - if (isset($variables['content']['links']['comment']['#links']['comment-comments'])) { - $comment_count_attributes['property'] = $variables['node']->rdf_mapping['comment_count']['predicates']; - $comment_count_attributes['content'] = $variables['node']->comment_count; - $comment_count_attributes['datatype'] = $variables['node']->rdf_mapping['comment_count']['datatype']; - // According to RDFa parsing rule number 4, a new subject URI is created - // from the href attribute if no rel/rev attribute is present. To get the - // original node URL from the about attribute of the parent container we - // set an empty rel attribute which triggers rule number 5. See - // http://www.w3.org/TR/rdfa-syntax/#sec_5.5. - $comment_count_attributes['rel'] = ''; - $variables['content']['links']['comment']['#links']['comment-comments']['attributes'] += $comment_count_attributes; - } - // In full node view, the number of comments is not displayed by - // node.tpl.php so it is expressed in RDFa in the tag of the HTML - // page. - if ($variables['page'] && user_access('access comments')) { - $element = array( - '#tag' => 'meta', - '#attributes' => array( - 'about' => $variables['node_url'], + if (isset($variables['node']->comment_count) && + !empty($variables['node']->rdf_mapping['comment_count']['predicates']) && + user_access('access comments')) { + // Adds RDFa markup for the comment count near the node title as metadata. + $variables['title_suffix']['rdf_meta_comment_count'] = array( + '#theme' => 'rdf_metadata', + '#metadata' => array( + array( 'property' => $variables['node']->rdf_mapping['comment_count']['predicates'], 'content' => $variables['node']->comment_count, 'datatype' => $variables['node']->rdf_mapping['comment_count']['datatype'], ), - ); - drupal_add_html_head($element, 'rdf_node_comment_count'); - } + ), + ); } } @@ -865,9 +859,9 @@ $output = ''; foreach ($variables['metadata'] as $attributes) { // Add a class so that developers viewing the HTML source can see why there - // are empty tags in the document. The class can also be used to set - // a CSS display:none rule in a theme where empty spans affect display. + // are empty tags in the document. $attributes['class'][] = 'rdf-meta'; + $attributes['class'][] = 'element-hidden'; // The XHTML+RDFa doctype allows either or syntax to // be used, but for maximum browser compatibility, W3C recommends the // former when serving pages using the text/html media type, see diff -Naur drupal-7.23/modules/rdf/rdf.test drupal-7.66/modules/rdf/rdf.test --- drupal-7.23/modules/rdf/rdf.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/rdf/rdf.test 2019-04-17 22:20:46.000000000 +0200 @@ -301,7 +301,7 @@ // Ensure the default bundle mapping for node is used. These attributes come // from the node default bundle definition. - $blog_title = $this->xpath("//meta[@property='dc:title' and @content='$node->title']"); + $blog_title = $this->xpath("//div[@about='$url']/span[@property='dc:title' and @content='$node->title']"); $blog_meta = $this->xpath("//div[(@about='$url') and (@typeof='sioct:Weblog')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$isoDate']"); $this->assertTrue(!empty($blog_title), 'Property dc:title is present in meta tag.'); $this->assertTrue(!empty($blog_meta), 'RDF type is present on post. Properties dc:date and dc:created are present on post date.'); @@ -313,13 +313,18 @@ */ function testAttributesInMarkup2() { $type = $this->drupalCreateContentType(array('type' => 'test_bundle_hook_install')); - $node = $this->drupalCreateNode(array('type' => 'test_bundle_hook_install')); + // Create node with single quotation mark title to ensure it does not get + // escaped more than once. + $node = $this->drupalCreateNode(array( + 'type' => 'test_bundle_hook_install', + 'title' => $this->randomName(8) . "'", + )); $isoDate = date('c', $node->changed); $url = url('node/' . $node->nid); $this->drupalGet('node/' . $node->nid); // Ensure the mapping defined in rdf_module.test is used. - $test_bundle_title = $this->xpath("//meta[@property='dc:title' and @content='$node->title']"); + $test_bundle_title = $this->xpath("//div[@about='$url']/span[@property='dc:title' and @content=\"$node->title\"]"); $test_bundle_meta = $this->xpath("//div[(@about='$url') and contains(@typeof, 'foo:mapping_install1') and contains(@typeof, 'bar:mapping_install2')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$isoDate']"); $this->assertTrue(!empty($test_bundle_title), 'Property dc:title is present in meta tag.'); $this->assertTrue(!empty($test_bundle_meta), 'RDF type is present on post. Properties dc:date and dc:created are present on post date.'); @@ -338,7 +343,7 @@ // Ensure the default bundle mapping for node is used. These attributes come // from the node default bundle definition. - $random_bundle_title = $this->xpath("//meta[@property='dc:title' and @content='$node->title']"); + $random_bundle_title = $this->xpath("//div[@about='$url']/span[@property='dc:title' and @content='$node->title']"); $random_bundle_meta = $this->xpath("//div[(@about='$url') and contains(@typeof, 'sioc:Item') and contains(@typeof, 'foaf:Document')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$isoDate']"); $this->assertTrue(!empty($random_bundle_title), 'Property dc:title is present in meta tag.'); $this->assertTrue(!empty($random_bundle_meta), 'RDF type is present on post. Properties dc:date and dc:created are present on post date.'); @@ -436,7 +441,7 @@ $this->setCommentPreview(DRUPAL_OPTIONAL); $this->setCommentForm(TRUE); $this->setCommentSubject(TRUE); - $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, t('Comment paging changed.')); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.'); // Creates the nodes on which the test comments will be posted. $this->drupalLogin($this->web_user); @@ -456,15 +461,13 @@ // Tests number of comments in teaser view. $this->drupalGet('node'); - $comment_count_teaser = $this->xpath('//div[contains(@typeof, "sioc:Item")]//li[contains(@class, "comment-comments")]/a[contains(@property, "sioc:num_replies") and contains(@content, "2") and @datatype="xsd:integer"]'); + $node_url = url('node/' . $this->node1->nid); + $comment_count_teaser = $this->xpath('//div[@about=:node-url]/span[@property="sioc:num_replies" and @content="2" and @datatype="xsd:integer"]', array(':node-url' => $node_url)); $this->assertTrue(!empty($comment_count_teaser), 'RDFa markup for the number of comments found on teaser view.'); - $comment_count_link = $this->xpath('//div[@about=:url]//a[contains(@property, "sioc:num_replies") and @rel=""]', array(':url' => url("node/{$this->node1->nid}"))); - $this->assertTrue(!empty($comment_count_link), 'Empty rel attribute found in comment count link.'); // Tests number of comments in full node view. $this->drupalGet('node/' . $this->node1->nid); - $node_url = url('node/' . $this->node1->nid); - $comment_count_teaser = $this->xpath('/html/head/meta[@about=:node-url and @property="sioc:num_replies" and @content="2" and @datatype="xsd:integer"]', array(':node-url' => $node_url)); + $comment_count_teaser = $this->xpath('//div[@about=:node-url]/span[@property="sioc:num_replies" and @content="2" and @datatype="xsd:integer"]', array(':node-url' => $node_url)); $this->assertTrue(!empty($comment_count_teaser), 'RDFa markup for the number of comments found on full node view.'); } diff -Naur drupal-7.23/modules/rdf/tests/rdf_test.info drupal-7.66/modules/rdf/tests/rdf_test.info --- drupal-7.23/modules/rdf/tests/rdf_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/rdf/tests/rdf_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,9 +4,9 @@ version = VERSION core = 7.x hidden = TRUE +dependencies[] = blog -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/search/search-result.tpl.php drupal-7.66/modules/search/search-result.tpl.php --- drupal-7.23/modules/search/search-result.tpl.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/search/search-result.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -25,7 +25,7 @@ * the template. * * Default keys within $info_split: - * - $info_split['type']: Node type (or item type string supplied by module). + * - $info_split['module']: The module that implemented the search query. * - $info_split['user']: Author of the node linked to users profile. Depends * on permission. * - $info_split['date']: Last update of the node. Short formatted. diff -Naur drupal-7.23/modules/search/search.admin.inc drupal-7.66/modules/search/search.admin.inc --- drupal-7.23/modules/search/search.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/search/search.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -125,6 +125,16 @@ '#options' => $module_options, '#description' => t('Choose which search module is the default.') ); + $form['logging'] = array( + '#type' => 'fieldset', + '#title' => t('Logging') + ); + $form['logging']['search_logging'] = array( + '#type' => 'checkbox', + '#title' => t('Log searches'), + '#default_value' => variable_get('search_logging', 1), + '#description' => t('If checked, all searches will be logged. Uncheck to skip logging. Logging may affect performance.'), + ); $form['#validate'][] = 'search_admin_settings_validate'; $form['#submit'][] = 'search_admin_settings_submit'; diff -Naur drupal-7.23/modules/search/search.api.php drupal-7.66/modules/search/search.api.php --- drupal-7.23/modules/search/search.api.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/search/search.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -30,8 +30,9 @@ * * @return * Array with optional keys: - * - title: Title for the tab on the search page for this module. Defaults - * to the module name if not given. + * - title: Title for the tab on the search page for this module. Title must + * be untranslated. Outside of this return array, pass the title through the + * t() function to register it as a translatable string. * - path: Path component after 'search/' for searching with this module. * Defaults to the module name if not given. * - conditions_callback: An implementation of callback_search_conditions(). @@ -39,6 +40,9 @@ * @ingroup search */ function hook_search_info() { + // Make the title translatable. + t('Content'); + return array( 'title' => 'Content', 'path' => 'node', diff -Naur drupal-7.23/modules/search/search.extender.inc drupal-7.66/modules/search/search.extender.inc --- drupal-7.23/modules/search/search.extender.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/search/search.extender.inc 2019-04-17 22:20:46.000000000 +0200 @@ -105,6 +105,8 @@ * Stores score expressions. * * @var array + * + * @see addScore() */ protected $scores = array(); @@ -116,7 +118,7 @@ protected $scoresArguments = array(); /** - * Total value of all the multipliers. + * Stores multipliers for score expressions. * * @var array */ @@ -147,6 +149,17 @@ $this->searchExpression = $expression; $this->type = $module; + // Add a search_* tag. This needs to be added before any preExecute methods + // for decorated queries are called, as $this->prepared will be set to TRUE + // and tags added in the execute method will never get used. For example, + // if $query is extended by 'SearchQuery' then 'PagerDefault', the + // search-specific tag will be added too late (when preExecute() has + // already been called from the PagerDefault extender), and as a + // consequence will not be available to hook_query_alter() implementations, + // nor will the correct hook_query_TAG_alter() implementations get invoked. + // See node_search_execute(). + $this->addTag('search_' . $module); + return $this; } @@ -391,21 +404,39 @@ /** * Adds a custom score expression to the search query. * - * Each score expression can optionally use a multiplier, and multiple - * expressions are combined. + * Score expressions are used to order search results. If no calls to + * addScore() have taken place, a default keyword relevance score will be + * used. However, if at least one call to addScore() has taken place, the + * keyword relevance score is not automatically added. + * + * Note that you must use this method to add ordering to your searches, and + * not call orderBy() directly, when using the SearchQuery extender. This is + * because of the two-pass system the SearchQuery class uses to normalize + * scores. * * @param $score - * The score expression. + * The score expression, which should evaluate to a number between 0 and 1. + * The string 'i.relevance' in a score expression will be replaced by a + * measure of keyword relevance between 0 and 1. * @param $arguments - * Custom query arguments for that expression. + * Query arguments needed to provide values to the score expression. * @param $multiply - * If set, the score is multiplied with that value. Search query ensures - * that the search scores are still normalized. + * If set, the score is multiplied with this value. However, all scores + * with multipliers are then divided by the total of all multipliers, so + * that overall, the normalization is maintained. + * + * @return object + * The updated query object. */ public function addScore($score, $arguments = array(), $multiply = FALSE) { if ($multiply) { $i = count($this->multiply); + // Modify the score expression so it is multiplied by the multiplier, + // with a divisor to renormalize. $score = "CAST(:multiply_$i AS DECIMAL) * COALESCE(( " . $score . "), 0) / CAST(:total_$i AS DECIMAL)"; + // Add an argument for the multiplier. The :total_$i argument is taken + // care of in the execute() method, which is when the total divisor is + // calculated. $arguments[':multiply_' . $i] = $multiply; $this->multiply[] = $multiply; } @@ -446,8 +477,9 @@ } if (count($this->multiply)) { - // Add the total multiplicator as many times as requested to maintain - // normalization as far as possible. + // Re-normalize scores with multipliers by dividing by the total of all + // multipliers. The expressions were altered in addScore(), so here just + // add the arguments for the total. $i = 0; $sum = array_sum($this->multiply); foreach ($this->multiply as $total) { @@ -456,19 +488,25 @@ } } - // Replace i.relevance pseudo-field with the actual, normalized value. - $this->scores = str_replace('i.relevance', '(' . (1.0 / $this->normalize) . ' * i.score * t.count)', $this->scores); - // Convert scores to an expression. + // Replace the pseudo-expression 'i.relevance' with a measure of keyword + // relevance in all score expressions, using string replacement. Careful + // though! If you just print out a float, some locales use ',' as the + // decimal separator in PHP, while SQL always uses '.'. So, make sure to + // set the number format correctly. + $relevance = number_format((1.0 / $this->normalize), 10, '.', ''); + $this->scores = str_replace('i.relevance', '(' . $relevance . ' * i.score * t.count)', $this->scores); + + // Add all scores together to form a query field. $this->addExpression('SUM(' . implode(' + ', $this->scores) . ')', 'calculated_score', $this->scoresArguments); + // If an order has not yet been set for this query, add a default order + // that sorts by the calculated sum of scores. if (count($this->getOrderBy()) == 0) { - // Add default order after adding the expression. $this->orderBy('calculated_score', 'DESC'); } - // Add tag and useful metadata. + // Add useful metadata. $this - ->addTag('search_' . $this->type) ->addMetaData('normalize', $this->normalize) ->fields('i', array('type', 'sid')); diff -Naur drupal-7.23/modules/search/search.info drupal-7.66/modules/search/search.info --- drupal-7.23/modules/search/search.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/search/search.info 2019-04-17 22:39:36.000000000 +0200 @@ -8,8 +8,7 @@ configure = admin/config/search/settings stylesheets[all][] = search.css -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/search/search.install drupal-7.66/modules/search/search.install --- drupal-7.23/modules/search/search.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/search/search.install 2019-04-17 22:20:46.000000000 +0200 @@ -12,6 +12,7 @@ variable_del('minimum_word_size'); variable_del('overlap_cjk'); variable_del('search_cron_limit'); + variable_del('search_logging'); } /** diff -Naur drupal-7.23/modules/search/search.pages.inc drupal-7.66/modules/search/search.pages.inc --- drupal-7.23/modules/search/search.pages.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/search/search.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -49,7 +49,7 @@ // which will get us back to this page callback. In other words, the search // form submits with POST but redirects to GET. This way we can keep // the search query URL clean as a whistle. - if (empty($_POST['form_id']) || $_POST['form_id'] != 'search_form') { + if (empty($_POST['form_id']) || ($_POST['form_id'] != 'search_form' && $_POST['form_id'] != 'search_block_form')) { $conditions = NULL; if (isset($info['conditions_callback']) && function_exists($info['conditions_callback'])) { // Build an optional array of more search conditions. @@ -57,9 +57,10 @@ } // Only search if there are keywords or non-empty conditions. if ($keys || !empty($conditions)) { - // Log the search keys. - watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys)); - + if (variable_get('search_logging', TRUE)) { + // Log the search keys. + watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys)); + } // Collect the search results. $results = search_data($keys, $info['module'], $conditions); } diff -Naur drupal-7.23/modules/search/search.test drupal-7.66/modules/search/search.test --- drupal-7.23/modules/search/search.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/search/search.test 2019-04-17 22:20:46.000000000 +0200 @@ -11,6 +11,9 @@ define('SEARCH_TYPE_2', '_test2_'); define('SEARCH_TYPE_JPN', '_test3_'); +/** + * Indexes content and queries it. + */ class SearchMatchTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -307,6 +310,9 @@ } } +/** + * Indexes content and tests the advanced search form. + */ class SearchAdvancedSearchForm extends DrupalWebTestCase { protected $node; @@ -370,6 +376,9 @@ } } +/** + * Indexes content and tests ranking factors. + */ class SearchRankingTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -580,6 +589,9 @@ } } +/** + * Tests the rendering of the search block. + */ class SearchBlockTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -654,6 +666,24 @@ url('search/node/', array('absolute' => TRUE)), 'Redirected to correct url.' ); + + // Test that after entering a too-short keyword in the form, you can then + // search again with a longer keyword. First test using the block form. + $terms = array('search_block_form' => 'a'); + $this->drupalPost('node', $terms, t('Search')); + $this->assertText('You must include at least one positive keyword with 3 characters or more'); + $terms = array('search_block_form' => 'foo'); + $this->drupalPost(NULL, $terms, t('Search')); + $this->assertNoText('You must include at least one positive keyword with 3 characters or more'); + $this->assertText('Your search yielded no results'); + + // Same test again, using the search page form for the second search this time. + $terms = array('search_block_form' => 'a'); + $this->drupalPost('node', $terms, t('Search')); + $terms = array('keys' => 'foo'); + $this->drupalPost(NULL, $terms, t('Search')); + $this->assertNoText('You must include at least one positive keyword with 3 characters or more'); + $this->assertText('Your search yielded no results'); } } @@ -727,7 +757,7 @@ public static function getInfo() { return array( 'name' => 'Comment Search tests', - 'description' => 'Verify text formats and filters used elsewhere.', + 'description' => 'Test integration searching comments.', 'group' => 'Search', ); } @@ -1423,7 +1453,7 @@ parent::setUp('search', 'search_extra_type'); // Login as a user that can create and search content. - $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks')); + $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks', 'access site reports')); $this->drupalLogin($this->search_user); // Add a single piece of content and index it. @@ -1472,6 +1502,19 @@ ); $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); $this->assertNoText(t('The configuration options have been saved.'), 'Form does not save with an invalid word length.'); + + // Test logging setting. It should be on by default. + $text = $this->randomName(5); + $this->drupalPost('search/node', array('keys' => $text), t('Search')); + $this->drupalGet('admin/reports/dblog'); + $this->assertLink('Searched Content for ' . $text . '.', 0, 'Search was logged'); + + // Turn off logging. + variable_set('search_logging', FALSE); + $text = $this->randomName(5); + $this->drupalPost('search/node', array('keys' => $text), t('Search')); + $this->drupalGet('admin/reports/dblog'); + $this->assertNoLink('Searched Content for ' . $text . '.', 'Search was not logged'); } /** @@ -1567,7 +1610,7 @@ /** * Tests the search_excerpt() function. */ -class SearchExcerptTestCase extends DrupalUnitTestCase { +class SearchExcerptTestCase extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'Search excerpt extraction', @@ -1577,8 +1620,7 @@ } function setUp() { - drupal_load('module', 'search'); - parent::setUp(); + parent::setUp('search'); } /** @@ -1603,7 +1645,7 @@ $this->assertEqual($result, 'The quick brown fox & jumps over the lazy dog ...', 'Found keyword is highlighted'); $longtext = str_repeat($text . ' ', 10); - $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text)); + $result = preg_replace('| +|', ' ', search_excerpt('nothing', $longtext)); $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected'); $entities = str_repeat('készítése ', 20); @@ -2018,10 +2060,11 @@ } /** - * Tests that search returns results with punctuation in the search phrase. + * Tests that search works with punctuation and HTML entities. */ function testPhraseSearchPunctuation() { $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => "The bunny's ears were fuzzy."))))); + $node2 = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => 'Dignissim Aliquam & Quieligo meus natu quae quia te. Damnum© erat— neo pneum. Facilisi feugiat ibidem ratis.'))))); // Update the search index. module_invoke_all('update_index'); @@ -2034,5 +2077,99 @@ $edit = array('keys' => '"bunny\'s"'); $this->drupalPost('search/node', $edit, t('Search')); $this->assertText($node->title); + + // Search for "&" and verify entities are not broken up in the output. + $edit = array('keys' => '&'); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertNoRaw('&amp;'); + $this->assertText('You must include at least one positive keyword'); + + $edit = array('keys' => '&'); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertNoRaw('&amp;'); + $this->assertText('You must include at least one positive keyword'); + } +} + +/** + * Tests node search with query tags. + */ +class SearchNodeTagTest extends DrupalWebTestCase { + public $test_user; + + public static function getInfo() { + return array( + 'name' => 'Node search query tags', + 'description' => 'Tests Node search tags functionality.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_node_tags'); + node_access_rebuild(); + + // Create a test user and log in. + $this->test_user = $this->drupalCreateUser(array('search content')); + $this->drupalLogin($this->test_user); + } + + /** + * Tests that the correct tags are available and hooks invoked. + */ + function testNodeSearchQueryTags() { + $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => 'testing testing testing.'))))); + + // Update the search index. + module_invoke_all('update_index'); + search_update_totals(); + + $edit = array('keys' => 'testing'); + $this->drupalPost('search/node', $edit, t('Search')); + + $this->assertTrue(variable_get('search_node_tags_test_query_tag', FALSE), 'hook_query_alter() was invoked and the query contained the "search_node" tag.'); + $this->assertTrue(variable_get('search_node_tags_test_query_tag_hook', FALSE), 'hook_query_search_node_alter() was invoked.'); + } +} + +/** + * Tests searching with locale values set. + */ +class SearchSetLocaleTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Search with numeric locale set', + 'description' => 'Check that search works with numeric locale settings', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + + // Create a simple node so something will be put in the index. + $info = array( + 'body' => array(LANGUAGE_NONE => array(array('value' => 'Tapir'))), + ); + $this->drupalCreateNode($info); + + // Run cron to index. + $this->cronRun(); + } + + /** + * Verify that search works with a numeric locale set. + */ + public function testSearchWithNumericLocale() { + // French decimal point is comma. + setlocale(LC_NUMERIC, 'fr_FR'); + + // An exception will be thrown if a float in the wrong format occurs in the + // query to the database, so an assertion is not necessary here. + db_select('search_index', 'i') + ->extend('searchquery') + ->searchexpression('tapir', 'node') + ->execute(); } } diff -Naur drupal-7.23/modules/search/tests/search_embedded_form.info drupal-7.66/modules/search/tests/search_embedded_form.info --- drupal-7.23/modules/search/tests/search_embedded_form.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/search/tests/search_embedded_form.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/search/tests/search_extra_type.info drupal-7.66/modules/search/tests/search_extra_type.info --- drupal-7.23/modules/search/tests/search_extra_type.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/search/tests/search_extra_type.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/search/tests/search_node_tags.info drupal-7.66/modules/search/tests/search_node_tags.info --- drupal-7.23/modules/search/tests/search_node_tags.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/search/tests/search_node_tags.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = "Test search node tags" +description = "Support module for Node search tags testing." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/search/tests/search_node_tags.module drupal-7.66/modules/search/tests/search_node_tags.module --- drupal-7.23/modules/search/tests/search_node_tags.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/search/tests/search_node_tags.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,23 @@ +hasTag('search_node')) { + variable_set('search_node_tags_test_query_tag', TRUE); + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function search_node_tags_query_search_node_alter(QueryAlterableInterface $query) { + variable_set('search_node_tags_test_query_tag_hook', TRUE); +} diff -Naur drupal-7.23/modules/shortcut/shortcut.admin.inc drupal-7.66/modules/shortcut/shortcut.admin.inc --- drupal-7.23/modules/shortcut/shortcut.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -784,5 +784,5 @@ drupal_goto(); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } diff -Naur drupal-7.23/modules/shortcut/shortcut.info drupal-7.66/modules/shortcut/shortcut.info --- drupal-7.23/modules/shortcut/shortcut.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = shortcut.test configure = admin/config/user-interface/shortcut -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/drupal_web_test_case.php drupal-7.66/modules/simpletest/drupal_web_test_case.php --- drupal-7.23/modules/simpletest/drupal_web_test_case.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/drupal_web_test_case.php 2019-04-17 22:20:46.000000000 +0200 @@ -40,6 +40,13 @@ protected $originalFileDirectory = NULL; /** + * URL to the verbose output file directory. + * + * @var string + */ + protected $verboseDirectoryUrl; + + /** * Time limit for the test. */ protected $timeLimit = 500; @@ -143,15 +150,7 @@ ); // Store assertion for display after the test has completed. - try { - $connection = Database::getConnection('default', 'simpletest_original_default'); - } - catch (DatabaseConnectionNotDefinedException $e) { - // If the test was not set up, the simpletest_original_default - // connection does not exist. - $connection = Database::getConnection('default', 'default'); - } - $connection + self::getDatabaseConnection() ->insert('simpletest') ->fields($assertion) ->execute(); @@ -167,6 +166,25 @@ } /** + * Returns the database connection to the site running Simpletest. + * + * @return DatabaseConnection + * The database connection to use for inserting assertions. + */ + public static function getDatabaseConnection() { + try { + $connection = Database::getConnection('default', 'simpletest_original_default'); + } + catch (DatabaseConnectionNotDefinedException $e) { + // If the test was not set up, the simpletest_original_default + // connection does not exist. + $connection = Database::getConnection('default', 'default'); + } + + return $connection; + } + + /** * Store an assertion from outside the testing context. * * This is useful for inserting assertions that can only be recorded after @@ -205,7 +223,8 @@ 'file' => $caller['file'], ); - return db_insert('simpletest') + return self::getDatabaseConnection() + ->insert('simpletest') ->fields($assertion) ->execute(); } @@ -221,7 +240,8 @@ * @see DrupalTestCase::insertAssert() */ public static function deleteAssert($message_id) { - return (bool) db_delete('simpletest') + return (bool) self::getDatabaseConnection() + ->delete('simpletest') ->condition('message_id', $message_id) ->execute(); } @@ -435,10 +455,10 @@ } /** - * Logs verbose message in a text file. + * Logs a verbose message in a text file. * - * The a link to the vebose message will be placed in the test results via - * as a passing assertion with the text '[verbose message]'. + * The link to the verbose message will be placed in the test results as a + * passing assertion with the text '[verbose message]'. * * @param $message * The verbose message to be stored. @@ -448,8 +468,11 @@ protected function verbose($message) { if ($id = simpletest_verbose($message)) { $class_safe = str_replace('\\', '_', get_class($this)); - $url = file_create_url($this->originalFileDirectory . '/simpletest/verbose/' . $class_safe . '-' . $id . '.html'); - $this->error(l(t('Verbose message'), $url, array('attributes' => array('target' => '_blank'))), 'User notice'); + $url = $this->verboseDirectoryUrl . '/' . $class_safe . '-' . $id . '.html'; + // Not using l() to avoid invoking the theme system, so that unit tests + // can use verbose() as well. + $link = '' . t('Verbose message') . ''; + $this->error($link, 'User notice'); } } @@ -541,6 +564,15 @@ E_RECOVERABLE_ERROR => 'Recoverable error', ); + // PHP 5.3 adds new error logging constants. Add these conditionally for + // backwards compatibility with PHP 5.2. + if (defined('E_DEPRECATED')) { + $error_map += array( + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'User deprecated', + ); + } + $backtrace = debug_backtrace(); $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace)); } @@ -697,10 +729,17 @@ * method. */ protected function setUp() { - global $conf; + global $conf, $language; // Store necessary current values before switching to the test environment. $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); + $this->verboseDirectoryUrl = file_create_url($this->originalFileDirectory . '/simpletest/verbose'); + + // Set up English language. + $this->originalLanguage = $language; + $this->originalLanguageDefault = variable_get('language_default'); + unset($conf['language_default']); + $language = language_default(); // Reset all statics so that test is performed with a clean environment. drupal_static_reset(); @@ -730,6 +769,10 @@ // subsequently will fail as the database is not accessible. $module_list = module_list(); if (isset($module_list['locale'])) { + // Transform the list into the format expected as input to module_list(). + foreach ($module_list as &$module) { + $module = array('filename' => drupal_get_filename('module', $module)); + } $this->originalModuleList = $module_list; unset($module_list['locale']); module_list(TRUE, FALSE, FALSE, $module_list); @@ -738,7 +781,7 @@ } protected function tearDown() { - global $conf; + global $conf, $language; // Get back to the original connection. Database::removeConnection('default'); @@ -749,6 +792,12 @@ if (isset($this->originalModuleList)) { module_list(TRUE, FALSE, FALSE, $this->originalModuleList); } + + // Reset language. + $language = $this->originalLanguage; + if ($this->originalLanguageDefault) { + $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; + } } } @@ -828,6 +877,13 @@ protected $cookieFile = NULL; /** + * The cookies of the page currently loaded in the internal browser. + * + * @var array + */ + protected $cookies = array(); + + /** * Additional cURL options. * * DrupalWebTestCase itself never sets this but always obeys what is set. @@ -916,7 +972,6 @@ protected function drupalCreateNode($settings = array()) { // Populate defaults array. $settings += array( - 'body' => array(LANGUAGE_NONE => array(array())), 'title' => $this->randomName(8), 'comment' => 2, 'changed' => REQUEST_TIME, @@ -931,6 +986,12 @@ 'language' => LANGUAGE_NONE, ); + // Add the body after the language is defined so that it may be set + // properly. + $settings += array( + 'body' => array($settings['language'] => array(array())), + ); + // Use the original node's created time for existing nodes. if (isset($settings['created']) && !isset($settings['date'])) { $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O'); @@ -989,9 +1050,7 @@ 'description' => '', 'help' => '', 'title_label' => 'Title', - 'body_label' => 'Body', 'has_title' => 1, - 'has_body' => 1, ); // Imposed values for a custom type. $forced = array( @@ -1041,7 +1100,7 @@ $lines = array(16, 256, 1024, 2048, 20480); $count = 0; foreach ($lines as $line) { - simpletest_generate_file('text-' . $count++, 64, $line); + simpletest_generate_file('text-' . $count++, 64, $line, 'text'); } // Copy other test files from simpletest. @@ -1132,7 +1191,7 @@ } /** - * Internal helper function; Create a role with specified permissions. + * Creates a role with specified permissions. * * @param $permissions * Array of permission names to assign to role. @@ -1338,12 +1397,14 @@ * @see DrupalWebTestCase::tearDown() */ protected function prepareEnvironment() { - global $user, $language, $conf; + global $user, $language, $language_url, $conf; // Store necessary current values before switching to prefixed database. $this->originalLanguage = $language; + $this->originalLanguageUrl = $language_url; $this->originalLanguageDefault = variable_get('language_default'); $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); + $this->verboseDirectoryUrl = file_create_url($this->originalFileDirectory . '/simpletest/verbose'); $this->originalProfile = drupal_get_profile(); $this->originalCleanUrl = variable_get('clean_url', 0); $this->originalUser = $user; @@ -1351,7 +1412,7 @@ // Set to English to prevent exceptions from utf8_truncate() from t() // during install if the current language is not 'en'. // The following array/object conversion is copied from language_default(). - $language = (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => ''); + $language_url = $language = (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => ''); // Save and clean the shutdown callbacks array because it is static cached // and will be changed by the test run. Otherwise it will contain callbacks @@ -1409,7 +1470,7 @@ * @see DrupalWebTestCase::prepareEnvironment() */ protected function setUp() { - global $user, $language, $conf; + global $user, $language, $language_url, $conf; // Create the database prefix for this test. $this->prepareDatabasePrefix(); @@ -1506,7 +1567,7 @@ // Set up English language. unset($conf['language_default']); - $language = language_default(); + $language_url = $language = language_default(); // Use the test mail class instead of the default mail handler class. variable_set('mail_system', array('default-system' => 'TestingMailSystem')); @@ -1600,7 +1661,7 @@ * and reset the database prefix. */ protected function tearDown() { - global $user, $language; + global $user, $language, $language_url; // In case a fatal error occurred that was not in the test process read the // log to pick up any fatal errors. @@ -1665,12 +1726,15 @@ // Reset language. $language = $this->originalLanguage; + $language_url = $this->originalLanguageUrl; if ($this->originalLanguageDefault) { $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; } - // Close the CURL handler. + // Close the CURL handler and reset the cookies array so test classes + // containing multiple tests are not polluted. $this->curlClose(); + $this->cookies = array(); } /** @@ -1743,14 +1807,24 @@ protected function curlExec($curl_options, $redirect = FALSE) { $this->curlInitialize(); - // cURL incorrectly handles URLs with a fragment by including the - // fragment in the request to the server, causing some web servers - // to reject the request citing "400 - Bad Request". To prevent - // this, we strip the fragment from the request. - // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. - if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { - $original_url = $curl_options[CURLOPT_URL]; - $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + if (!empty($curl_options[CURLOPT_URL])) { + // Forward XDebug activation if present. + if (isset($_COOKIE['XDEBUG_SESSION'])) { + $options = drupal_parse_url($curl_options[CURLOPT_URL]); + $options += array('query' => array()); + $options['query'] += array('XDEBUG_SESSION_START' => $_COOKIE['XDEBUG_SESSION']); + $curl_options[CURLOPT_URL] = url($options['path'], $options); + } + + // cURL incorrectly handles URLs with a fragment by including the + // fragment in the request to the server, causing some web servers + // to reject the request citing "400 - Bad Request". To prevent + // this, we strip the fragment from the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (strpos($curl_options[CURLOPT_URL], '#')) { + $original_url = $curl_options[CURLOPT_URL]; + $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + } } $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; @@ -2042,7 +2116,14 @@ foreach ($upload as $key => $file) { $file = drupal_realpath($file); if ($file && is_file($file)) { - $post[$key] = '@' . $file; + // Use the new CurlFile class for file uploads when using PHP + // 5.5 or higher. + if (class_exists('CurlFile')) { + $post[$key] = curl_file_create($file); + } + else { + $post[$key] = '@' . $file; + } } } } @@ -2178,6 +2259,7 @@ // Submit the POST request. $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post)); + $this->assertIdentical($this->drupalGetHeader('X-Drupal-Ajax-Token'), '1', 'Ajax response header found.'); // Change the page content by applying the returned commands. if (!empty($ajax_settings) && !empty($return)) { @@ -2214,8 +2296,13 @@ if ($wrapperNode) { // ajax.js adds an enclosing DIV to work around a Safari bug. $newDom = new DOMDocument(); + // DOM can load HTML soup. But, HTML soup can throw warnings, + // suppress them. $newDom->loadHTML('
          ' . $command['data'] . '
          '); - $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); + // Suppress warnings thrown when duplicate HTML IDs are + // encountered. This probably means we are replacing an element + // with the same ID. + $newNode = @$dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; // The "method" is a jQuery DOM manipulation function. Emulate // each one using PHP's DOMNode API. @@ -2249,6 +2336,13 @@ } break; + case 'updateBuildId': + $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0); + if ($buildId) { + $buildId->setAttribute('value', $command['new']); + } + break; + // @todo Add suitable implementations for these commands in order to // have full test coverage of what ajax.js can do. case 'remove': @@ -2261,12 +2355,22 @@ break; case 'restripe': break; + case 'add_css': + break; } } $content = $dom->saveHTML(); } $this->drupalSetContent($content); $this->drupalSetSettings($drupal_settings); + + $verbose = 'AJAX POST request to: ' . $path; + $verbose .= '
          AJAX callback path: ' . $ajax_path; + $verbose .= '
          Ending URL: ' . $this->getUrl(); + $verbose .= '
          ' . $this->content; + + $this->verbose($verbose); + return $return; } @@ -2520,6 +2624,11 @@ * * @param $xpath * The xpath string to use in the search. + * @param array $arguments + * An array of arguments with keys in the form ':name' matching the + * placeholders in the query. The values may be either strings or numeric + * values. + * * @return * The return value of the xpath search. For details on the xpath string * format and return values see the SimpleXML documentation, @@ -2589,8 +2698,6 @@ * * @param $label * Text between the anchor tags. - * @param $index - * Link position counting from zero. * @param $message * Message to display. * @param $group @@ -2649,28 +2756,26 @@ * * Will click the first link found with this link text by default, or a later * one if an index is given. Match is case sensitive with normalized space. - * The label is translated label. There is an assert for successful click. + * The label is translated label. + * + * If the link is discovered and clicked, the test passes. Fail otherwise. * * @param $label * Text between the anchor tags. * @param $index * Link position counting from zero. * @return - * Page on success, or FALSE on failure. + * Page contents on success, or FALSE on failure. */ protected function clickLink($label, $index = 0) { $url_before = $this->getUrl(); $urls = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label)); - if (isset($urls[$index])) { $url_target = $this->getAbsoluteUrl($urls[$index]['href']); - } - - $this->assertTrue(isset($urls[$index]), t('Clicked link %label (@url_target) from @url_before', array('%label' => $label, '@url_target' => $url_target, '@url_before' => $url_before)), t('Browser')); - - if (isset($url_target)) { + $this->pass(t('Clicked link %label (@url_target) from @url_before', array('%label' => $label, '@url_target' => $url_target, '@url_before' => $url_before)), 'Browser'); return $this->drupalGet($url_target); } + $this->fail(t('Link %label does not exist on @url_before', array('%label' => $label, '@url_before' => $url_before)), 'Browser'); return FALSE; } @@ -2695,7 +2800,7 @@ $path = substr($path, $length); } // Ensure that we have an absolute path. - if ($path[0] !== '/') { + if (empty($path) || $path[0] !== '/') { $path = '/' . $path; } // Finally, prepend the $base_url. @@ -2907,7 +3012,7 @@ if (!$message) { $message = t('Raw "@raw" found', array('@raw' => $raw)); } - return $this->assert(strpos($this->drupalGetContent(), $raw) !== FALSE, $message, $group); + return $this->assert(strpos($this->drupalGetContent(), (string) $raw) !== FALSE, $message, $group); } /** @@ -2927,7 +3032,7 @@ if (!$message) { $message = t('Raw "@raw" not found', array('@raw' => $raw)); } - return $this->assert(strpos($this->drupalGetContent(), $raw) === FALSE, $message, $group); + return $this->assert(strpos($this->drupalGetContent(), (string) $raw) === FALSE, $message, $group); } /** @@ -3154,7 +3259,7 @@ * @param $callback * The name of the theme function to invoke; e.g. 'links' for theme_links(). * @param $variables - * An array of variables to pass to the theme function. + * (optional) An array of variables to pass to the theme function. * @param $expected * The expected themed output string. * @param $message @@ -3190,7 +3295,9 @@ * @param $xpath * XPath used to find the field. * @param $value - * (optional) Value of the field to assert. + * (optional) Value of the field to assert. You may pass in NULL (default) + * to skip checking the actual value, while still checking that the field + * exists. * @param $message * (optional) Message to display. * @param $group @@ -3258,12 +3365,14 @@ } /** - * Asserts that a field does not exist in the current page by the given XPath. + * Asserts that a field doesn't exist or its value doesn't match, by XPath. * * @param $xpath * XPath used to find the field. * @param $value - * (optional) Value of the field to assert. + * (optional) Value for the field, to assert that the field's value on the + * page doesn't match it. You may pass in NULL to skip checking the + * value, while still checking that the field doesn't exist. * @param $message * (optional) Message to display. * @param $group @@ -3296,7 +3405,9 @@ * @param $name * Name of field to assert. * @param $value - * Value of the field to assert. + * (optional) Value of the field to assert. You may pass in NULL (default) + * to skip checking the actual value, while still checking that the field + * exists. * @param $message * Message to display. * @param $group @@ -3327,9 +3438,12 @@ * @param $name * Name of field to assert. * @param $value - * Value of the field to assert. + * (optional) Value for the field, to assert that the field's value on the + * page doesn't match it. You may pass in NULL to skip checking the + * value, while still checking that the field doesn't exist. However, the + * default value ('') asserts that the field value is not an empty string. * @param $message - * Message to display. + * (optional) Message to display. * @param $group * The group this message belongs to. * @return @@ -3340,14 +3454,17 @@ } /** - * Asserts that a field exists in the current page with the given id and value. + * Asserts that a field exists in the current page with the given ID and value. * * @param $id - * Id of field to assert. + * ID of field to assert. * @param $value - * Value of the field to assert. + * (optional) Value for the field to assert. You may pass in NULL to skip + * checking the value, while still checking that the field exists. + * However, the default value ('') asserts that the field value is an empty + * string. * @param $message - * Message to display. + * (optional) Message to display. * @param $group * The group this message belongs to. * @return @@ -3358,14 +3475,17 @@ } /** - * Asserts that a field does not exist with the given id and value. + * Asserts that a field does not exist with the given ID and value. * * @param $id - * Id of field to assert. + * ID of field to assert. * @param $value - * Value of the field to assert. + * (optional) Value for the field, to assert that the field's value on the + * page doesn't match it. You may pass in NULL to skip checking the value, + * while still checking that the field doesn't exist. However, the default + * value ('') asserts that the field value is not an empty string. * @param $message - * Message to display. + * (optional) Message to display. * @param $group * The group this message belongs to. * @return @@ -3379,9 +3499,9 @@ * Asserts that a checkbox field in the current page is checked. * * @param $id - * Id of field to assert. + * ID of field to assert. * @param $message - * Message to display. + * (optional) Message to display. * @return * TRUE on pass, FALSE on fail. */ @@ -3394,9 +3514,9 @@ * Asserts that a checkbox field in the current page is not checked. * * @param $id - * Id of field to assert. + * ID of field to assert. * @param $message - * Message to display. + * (optional) Message to display. * @return * TRUE on pass, FALSE on fail. */ @@ -3409,11 +3529,11 @@ * Asserts that a select option in the current page is checked. * * @param $id - * Id of select field to assert. + * ID of select field to assert. * @param $option * Option to assert. * @param $message - * Message to display. + * (optional) Message to display. * @return * TRUE on pass, FALSE on fail. * @@ -3428,11 +3548,11 @@ * Asserts that a select option in the current page is not checked. * * @param $id - * Id of select field to assert. + * ID of select field to assert. * @param $option * Option to assert. * @param $message - * Message to display. + * (optional) Message to display. * @return * TRUE on pass, FALSE on fail. */ @@ -3442,12 +3562,12 @@ } /** - * Asserts that a field exists with the given name or id. + * Asserts that a field exists with the given name or ID. * * @param $field - * Name or id of field to assert. + * Name or ID of field to assert. * @param $message - * Message to display. + * (optional) Message to display. * @param $group * The group this message belongs to. * @return @@ -3458,12 +3578,12 @@ } /** - * Asserts that a field does not exist with the given name or id. + * Asserts that a field does not exist with the given name or ID. * * @param $field - * Name or id of field to assert. + * Name or ID of field to assert. * @param $message - * Message to display. + * (optional) Message to display. * @param $group * The group this message belongs to. * @return diff -Naur drupal-7.23/modules/simpletest/files/css_test_files/css_input_with_import.css drupal-7.66/modules/simpletest/files/css_test_files/css_input_with_import.css --- drupal-7.23/modules/simpletest/files/css_test_files/css_input_with_import.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_input_with_import.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,7 @@ +@import url("http://example.com/style.css"); +@import url("//example.com/style.css"); @import "import1.css"; @import "import2.css"; diff -Naur drupal-7.23/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css drupal-7.66/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css --- drupal-7.23/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,4 @@ -ul,select{font:1em/160% Verdana,sans-serif;color:#494949;}.ui-icon{background-image:url(images/icon.png);} +@import url("http://example.com/style.css");@import url("//example.com/style.css");ul,select{font:1em/160% Verdana,sans-serif;color:#494949;}.ui-icon{background-image:url(images/icon.png);}.data .double-quote{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");}.data .single-quote{background-image:url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAH//Z');}.data .no-quote{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAEAQAAAAAo/mtHAAAAIElEQVQIHWMRnWHwcRNLN8NZ7QYWwT8PlBlYsgqVBRsAankIMw5MtnoAAAAASUVORK5CYII=);} p,select{font:1em/160% Verdana,sans-serif;color:#494949;} body{margin:0;padding:0;background:#edf5fa;font:76%/170% Verdana,sans-serif;color:#494949;}.this .is .a .test{font:1em/100% Verdana,sans-serif;color:#494949;}.this .is diff -Naur drupal-7.23/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css drupal-7.66/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css --- drupal-7.23/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,33 @@ +@import url("http://example.com/style.css"); +@import url("//example.com/style.css"); +ul, select { + font: 1em/160% Verdana, sans-serif; + color: #494949; +} +.ui-icon{background-image: url(images/icon.png);} + +/* Test data URI images with different quote styles. */ +.data .double-quote { + /* http://stackoverflow.com/a/13139830/11023 */ + background-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} + +.data .single-quote { + background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAH//Z'); +} + +.data .no-quote { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAEAQAAAAAo/mtHAAAAIElEQVQIHWMRnWHwcRNLN8NZ7QYWwT8PlBlYsgqVBRsAankIMw5MtnoAAAAASUVORK5CYII=); +} + + +p, select { + font: 1em/160% Verdana, sans-serif; + color: #494949; +} body { diff -Naur drupal-7.23/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css drupal-7.66/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css --- drupal-7.23/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,29 @@ + + +@import "../import1.css"; +@import "../import2.css"; + +body { + margin: 0; + padding: 0; + background: #edf5fa; + font: 76%/170% Verdana, sans-serif; + color: #494949; +} + +.this .is .a .test { + font: 1em/100% Verdana, sans-serif; + color: #494949; +} +.this +.is +.a +.test { +font: 1em/100% Verdana, sans-serif; +color: #494949; +} + +textarea, select { + font: 1em/160% Verdana, sans-serif; + color: #494949; +} diff -Naur drupal-7.23/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.optimized.css drupal-7.66/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.optimized.css --- drupal-7.23/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.optimized.css 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.optimized.css 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,6 @@ +ul,select{font:1em/160% Verdana,sans-serif;color:#494949;}.ui-icon{background-image:url(../images/icon.png);}.data .double-quote{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");}.data .single-quote{background-image:url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAH//Z');}.data .no-quote{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAEAQAAAAAo/mtHAAAAIElEQVQIHWMRnWHwcRNLN8NZ7QYWwT8PlBlYsgqVBRsAankIMw5MtnoAAAAASUVORK5CYII=);} +p,select{font:1em/160% Verdana,sans-serif;color:#494949;} +body{margin:0;padding:0;background:#edf5fa;font:76%/170% Verdana,sans-serif;color:#494949;}.this .is .a .test{font:1em/100% Verdana,sans-serif;color:#494949;}.this +.is +.a +.test{font:1em/100% Verdana,sans-serif;color:#494949;}textarea,select{font:1em/160% Verdana,sans-serif;color:#494949;} diff -Naur drupal-7.23/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.unoptimized.css drupal-7.66/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.unoptimized.css --- drupal-7.23/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.unoptimized.css 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_subfolder/css_input_with_import.css.unoptimized.css 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,54 @@ + + + +ul, select { + font: 1em/160% Verdana, sans-serif; + color: #494949; +} +.ui-icon{background-image: url(../images/icon.png);} + +/* Test data URI images with different quote styles. */ +.data .double-quote { + /* http://stackoverflow.com/a/13139830/11023 */ + background-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} + +.data .single-quote { + background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAH//Z'); +} + +.data .no-quote { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAEAQAAAAAo/mtHAAAAIElEQVQIHWMRnWHwcRNLN8NZ7QYWwT8PlBlYsgqVBRsAankIMw5MtnoAAAAASUVORK5CYII=); +} + + +p, select { + font: 1em/160% Verdana, sans-serif; + color: #494949; +} + + +body { + margin: 0; + padding: 0; + background: #edf5fa; + font: 76%/170% Verdana, sans-serif; + color: #494949; +} + +.this .is .a .test { + font: 1em/100% Verdana, sans-serif; + color: #494949; +} +.this +.is +.a +.test { +font: 1em/100% Verdana, sans-serif; +color: #494949; +} + +textarea, select { + font: 1em/160% Verdana, sans-serif; + color: #494949; +} diff -Naur drupal-7.23/modules/simpletest/files/css_test_files/import1.css drupal-7.66/modules/simpletest/files/css_test_files/import1.css --- drupal-7.23/modules/simpletest/files/css_test_files/import1.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/css_test_files/import1.css 2019-04-17 22:20:46.000000000 +0200 @@ -3,4 +3,18 @@ font: 1em/160% Verdana, sans-serif; color: #494949; } -.ui-icon{background-image: url(images/icon.png);} \ No newline at end of file +.ui-icon{background-image: url(images/icon.png);} + +/* Test data URI images with different quote styles. */ +.data .double-quote { + /* http://stackoverflow.com/a/13139830/11023 */ + background-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} + +.data .single-quote { + background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAH//Z'); +} + +.data .no-quote { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAEAQAAAAAo/mtHAAAAIElEQVQIHWMRnWHwcRNLN8NZ7QYWwT8PlBlYsgqVBRsAankIMw5MtnoAAAAASUVORK5CYII=); +} diff -Naur drupal-7.23/modules/simpletest/files/image-test-no-transparency.gif drupal-7.66/modules/simpletest/files/image-test-no-transparency.gif --- drupal-7.23/modules/simpletest/files/image-test-no-transparency.gif 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/files/image-test-no-transparency.gif 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +GIF89a(,(8*L(C.HCV"ČC1b(S\YŌI%B0Oh N2w^icΠk|x̤<:$@*V5Q]fulح_{U&[iϲ-nBsݚ=7l^,+wHx 8bƅ?6 ; \ No newline at end of file diff -Naur drupal-7.23/modules/simpletest/files/image-test-transparent-out-of-range.gif drupal-7.66/modules/simpletest/files/image-test-transparent-out-of-range.gif --- drupal-7.23/modules/simpletest/files/image-test-transparent-out-of-range.gif 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/files/image-test-transparent-out-of-range.gif 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,2 @@ +GIF89a(!,(|80 lݵҍԇd +gp,E Tl {Q1|ĩ^ޮ`L0er ^`7G{vnoyiV40zk*$ar ; \ No newline at end of file diff -Naur drupal-7.23/modules/simpletest/files/phar-1.phar drupal-7.66/modules/simpletest/files/phar-1.phar --- drupal-7.23/modules/simpletest/files/phar-1.phar 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/files/phar-1.phar 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,301 @@ + 2, +'c' => 'text/plain', +'cc' => 'text/plain', +'cpp' => 'text/plain', +'c++' => 'text/plain', +'dtd' => 'text/plain', +'h' => 'text/plain', +'log' => 'text/plain', +'rng' => 'text/plain', +'txt' => 'text/plain', +'xsd' => 'text/plain', +'php' => 1, +'inc' => 1, +'avi' => 'video/avi', +'bmp' => 'image/bmp', +'css' => 'text/css', +'gif' => 'image/gif', +'htm' => 'text/html', +'html' => 'text/html', +'htmls' => 'text/html', +'ico' => 'image/x-ico', +'jpe' => 'image/jpeg', +'jpg' => 'image/jpeg', +'jpeg' => 'image/jpeg', +'js' => 'application/x-javascript', +'midi' => 'audio/midi', +'mid' => 'audio/midi', +'mod' => 'audio/mod', +'mov' => 'movie/quicktime', +'mp3' => 'audio/mp3', +'mpg' => 'video/mpeg', +'mpeg' => 'video/mpeg', +'pdf' => 'application/pdf', +'png' => 'image/png', +'swf' => 'application/shockwave-flash', +'tif' => 'image/tiff', +'tiff' => 'image/tiff', +'wav' => 'audio/wav', +'xbm' => 'image/xbm', +'xml' => 'text/xml', +); + +header("Cache-Control: no-cache, must-revalidate"); +header("Pragma: no-cache"); + +$basename = basename(__FILE__); +if (!strpos($_SERVER['REQUEST_URI'], $basename)) { +chdir(Extract_Phar::$temp); +include $web; +return; +} +$pt = substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], $basename) + strlen($basename)); +if (!$pt || $pt == '/') { +$pt = $web; +header('HTTP/1.1 301 Moved Permanently'); +header('Location: ' . $_SERVER['REQUEST_URI'] . '/' . $pt); +exit; +} +$a = realpath(Extract_Phar::$temp . DIRECTORY_SEPARATOR . $pt); +if (!$a || strlen(dirname($a)) < strlen(Extract_Phar::$temp)) { +header('HTTP/1.0 404 Not Found'); +echo "\n \n File Not Found<title>\n </head>\n <body>\n <h1>404 - File Not Found</h1>\n </body>\n</html>"; +exit; +} +$b = pathinfo($a); +if (!isset($b['extension'])) { +header('Content-Type: text/plain'); +header('Content-Length: ' . filesize($a)); +readfile($a); +exit; +} +if (isset($mimes[$b['extension']])) { +if ($mimes[$b['extension']] === 1) { +include $a; +exit; +} +if ($mimes[$b['extension']] === 2) { +highlight_file($a); +exit; +} +header('Content-Type: ' .$mimes[$b['extension']]); +header('Content-Length: ' . filesize($a)); +readfile($a); +exit; +} +} + +class Extract_Phar +{ +static $temp; +static $origdir; +const GZ = 0x1000; +const BZ2 = 0x2000; +const MASK = 0x3000; +const START = 'index.php'; +const LEN = 6643; + +static function go($return = false) +{ +$fp = fopen(__FILE__, 'rb'); +fseek($fp, self::LEN); +$L = unpack('V', $a = fread($fp, 4)); +$m = ''; + +do { +$read = 8192; +if ($L[1] - strlen($m) < 8192) { +$read = $L[1] - strlen($m); +} +$last = fread($fp, $read); +$m .= $last; +} while (strlen($last) && strlen($m) < $L[1]); + +if (strlen($m) < $L[1]) { +die('ERROR: manifest length read was "' . +strlen($m) .'" should be "' . +$L[1] . '"'); +} + +$info = self::_unpack($m); +$f = $info['c']; + +if ($f & self::GZ) { +if (!function_exists('gzinflate')) { +die('Error: zlib extension is not enabled -' . +' gzinflate() function needed for zlib-compressed .phars'); +} +} + +if ($f & self::BZ2) { +if (!function_exists('bzdecompress')) { +die('Error: bzip2 extension is not enabled -' . +' bzdecompress() function needed for bz2-compressed .phars'); +} +} + +$temp = self::tmpdir(); + +if (!$temp || !is_writable($temp)) { +$sessionpath = session_save_path(); +if (strpos ($sessionpath, ";") !== false) +$sessionpath = substr ($sessionpath, strpos ($sessionpath, ";")+1); +if (!file_exists($sessionpath) || !is_dir($sessionpath)) { +die('Could not locate temporary directory to extract phar'); +} +$temp = $sessionpath; +} + +$temp .= '/pharextract/'.basename(__FILE__, '.phar'); +self::$temp = $temp; +self::$origdir = getcwd(); +@mkdir($temp, 0777, true); +$temp = realpath($temp); + +if (!file_exists($temp . DIRECTORY_SEPARATOR . md5_file(__FILE__))) { +self::_removeTmpFiles($temp, getcwd()); +@mkdir($temp, 0777, true); +@file_put_contents($temp . '/' . md5_file(__FILE__), ''); + +foreach ($info['m'] as $path => $file) { +$a = !file_exists(dirname($temp . '/' . $path)); +@mkdir(dirname($temp . '/' . $path), 0777, true); +clearstatcache(); + +if ($path[strlen($path) - 1] == '/') { +@mkdir($temp . '/' . $path, 0777); +} else { +file_put_contents($temp . '/' . $path, self::extractFile($path, $file, $fp)); +@chmod($temp . '/' . $path, 0666); +} +} +} + +chdir($temp); + +if (!$return) { +include self::START; +} +} + +static function tmpdir() +{ +if (strpos(PHP_OS, 'WIN') !== false) { +if ($var = getenv('TMP') ? getenv('TMP') : getenv('TEMP')) { +return $var; +} +if (is_dir('/temp') || mkdir('/temp')) { +return realpath('/temp'); +} +return false; +} +if ($var = getenv('TMPDIR')) { +return $var; +} +return realpath('/tmp'); +} + +static function _unpack($m) +{ +$info = unpack('V', substr($m, 0, 4)); + $l = unpack('V', substr($m, 10, 4)); +$m = substr($m, 14 + $l[1]); +$s = unpack('V', substr($m, 0, 4)); +$o = 0; +$start = 4 + $s[1]; +$ret['c'] = 0; + +for ($i = 0; $i < $info[1]; $i++) { + $len = unpack('V', substr($m, $start, 4)); +$start += 4; + $savepath = substr($m, $start, $len[1]); +$start += $len[1]; + $ret['m'][$savepath] = array_values(unpack('Va/Vb/Vc/Vd/Ve/Vf', substr($m, $start, 24))); +$ret['m'][$savepath][3] = sprintf('%u', $ret['m'][$savepath][3] +& 0xffffffff); +$ret['m'][$savepath][7] = $o; +$o += $ret['m'][$savepath][2]; +$start += 24 + $ret['m'][$savepath][5]; +$ret['c'] |= $ret['m'][$savepath][4] & self::MASK; +} +return $ret; +} + +static function extractFile($path, $entry, $fp) +{ +$data = ''; +$c = $entry[2]; + +while ($c) { +if ($c < 8192) { +$data .= @fread($fp, $c); +$c = 0; +} else { +$c -= 8192; +$data .= @fread($fp, 8192); +} +} + +if ($entry[4] & self::GZ) { +$data = gzinflate($data); +} elseif ($entry[4] & self::BZ2) { +$data = bzdecompress($data); +} + +if (strlen($data) != $entry[0]) { +die("Invalid internal .phar file (size error " . strlen($data) . " != " . +$stat[7] . ")"); +} + +if ($entry[3] != sprintf("%u", crc32($data) & 0xffffffff)) { +die("Invalid internal .phar file (checksum error)"); +} + +return $data; +} + +static function _removeTmpFiles($temp, $origdir) +{ +chdir($temp); + +foreach (glob('*') as $f) { +if (file_exists($f)) { +is_dir($f) ? @rmdir($f) : @unlink($f); +if (file_exists($f) && is_dir($f)) { +self::_removeTmpFiles($f, getcwd()); +} +} +} + +@rmdir($temp); +clearstatcache(); +chdir($origdir); +} +} + +Extract_Phar::go(); +__HALT_COMPILER(); ?>7������������������ ���index.php���8![���u‰������<?php +/** + * @file + * This test file is used to test Drupal's phar stream wrapper functionality. + * + * @see \Drupal\KernelTests\Core\File\PharWrapperTest + */ + +echo 'Hello, world!'; +1qV5['y RCA���GBMB \ No newline at end of file diff -Naur drupal-7.23/modules/simpletest/simpletest.info drupal-7.66/modules/simpletest/simpletest.info --- drupal-7.23/modules/simpletest/simpletest.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/simpletest.info 2019-04-17 22:39:36.000000000 +0200 @@ -11,10 +11,12 @@ files[] = tests/actions.test files[] = tests/ajax.test files[] = tests/batch.test +files[] = tests/boot.test files[] = tests/bootstrap.test files[] = tests/cache.test files[] = tests/common.test files[] = tests/database_test.test +files[] = tests/entity_crud.test files[] = tests/entity_crud_hook_test.test files[] = tests/entity_query.test files[] = tests/error.test @@ -55,8 +57,7 @@ files[] = tests/upgrade/update.field.test files[] = tests/upgrade/update.user.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/simpletest.module drupal-7.66/modules/simpletest/simpletest.module --- drupal-7.23/modules/simpletest/simpletest.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/simpletest.module 2019-04-17 22:20:46.000000000 +0200 @@ -154,7 +154,7 @@ } /** - * Batch operation callback. + * Implements callback_batch_operation(). */ function _simpletest_batch_operation($test_list_init, $test_id, &$context) { simpletest_classloader_register(); @@ -205,6 +205,9 @@ $context['finished'] = 1 - $size / $max; } +/** + * Implements callback_batch_finished(). + */ function _simpletest_batch_finished($success, $results, $operations, $elapsed) { if ($success) { drupal_set_message(t('The test run finished in @elapsed.', array('@elapsed' => $elapsed))); @@ -328,25 +331,32 @@ // Also discover PSR-0 test classes, if the PHP version allows it. if (version_compare(PHP_VERSION, '5.3') > 0) { - // Select all PSR-0 classes in the Tests namespace of all modules. + // Select all PSR-0 and PSR-4 classes in the Tests namespace of all + // modules. $system_list = db_query("SELECT name, filename FROM {system}")->fetchAllKeyed(); foreach ($system_list as $name => $filename) { - // Build directory in which the test files would reside. - $tests_dir = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/Drupal/' . $name . '/Tests'; - // Scan it for test files if it exists. - if (is_dir($tests_dir)) { - $files = file_scan_directory($tests_dir, '/.*\.php/'); - if (!empty($files)) { - $basedir = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/'; - foreach ($files as $file) { - // Convert the file name into the namespaced class name. - $replacements = array( - '/' => '\\', - $basedir => '', - '.php' => '', - ); - $classes[] = strtr($file->uri, $replacements); + $module_dir = DRUPAL_ROOT . '/' . dirname($filename); + // Search both the 'lib/Drupal/mymodule' directory (for PSR-0 classes) + // and the 'src' directory (for PSR-4 classes). + foreach(array('lib/Drupal/' . $name, 'src') as $subdir) { + // Build directory in which the test files would reside. + $tests_dir = $module_dir . '/' . $subdir . '/Tests'; + // Scan it for test files if it exists. + if (is_dir($tests_dir)) { + $files = file_scan_directory($tests_dir, '/.*\.php/'); + if (!empty($files)) { + foreach ($files as $file) { + // Convert the file name into the namespaced class name. + $replacements = array( + '/' => '\\', + $module_dir . '/' => '', + 'lib/' => '', + 'src/' => 'Drupal\\' . $name . '\\', + '.php' => '', + ); + $classes[] = strtr($file->uri, $replacements); + } } } } @@ -364,7 +374,10 @@ // If this test class requires a non-existing module, skip it. if (!empty($info['dependencies'])) { foreach ($info['dependencies'] as $module) { - if (!drupal_get_filename('module', $module)) { + // Pass FALSE as fourth argument so no error gets created for + // the missing file. + $found_module = drupal_get_filename('module', $module, NULL, FALSE); + if (!$found_module) { continue 2; } } @@ -406,17 +419,20 @@ // Only register PSR-0 class loading if we are on PHP 5.3 or higher. if (version_compare(PHP_VERSION, '5.3') > 0) { - spl_autoload_register('_simpletest_autoload_psr0'); + spl_autoload_register('_simpletest_autoload_psr4_psr0'); } } /** - * Autoload callback to find PSR-0 test classes. + * Autoload callback to find PSR-4 and PSR-0 test classes. + * + * Looks in the 'src/Tests' and in the 'lib/Drupal/mymodule/Tests' directory of + * modules for the class. * * This will only work on classes where the namespace is of the pattern * "Drupal\$extension\Tests\.." */ -function _simpletest_autoload_psr0($class) { +function _simpletest_autoload_psr4_psr0($class) { // Static cache for extension paths. // This cache is lazily filled as soon as it is needed. @@ -446,14 +462,26 @@ $namespace = substr($class, 0, $nspos); $classname = substr($class, $nspos + 1); - // Build the filepath where we expect the class to be defined. - $path = dirname($extensions[$extension]) . '/lib/' . - str_replace('\\', '/', $namespace) . '/' . + // Try the PSR-4 location first, and the PSR-0 location as a fallback. + // Build the PSR-4 filepath where we expect the class to be defined. + $psr4_path = dirname($extensions[$extension]) . '/src/' . + str_replace('\\', '/', substr($namespace, strlen('Drupal\\' . $extension . '\\'))) . '/' . str_replace('_', '/', $classname) . '.php'; // Include the file, if it does exist. - if (file_exists($path)) { - include $path; + if (file_exists($psr4_path)) { + include $psr4_path; + } + else { + // Build the PSR-0 filepath where we expect the class to be defined. + $psr0_path = dirname($extensions[$extension]) . '/lib/' . + str_replace('\\', '/', $namespace) . '/' . + str_replace('_', '/', $classname) . '.php'; + + // Include the file, if it does exist. + if (file_exists($psr0_path)) { + include $psr0_path; + } } } } @@ -487,25 +515,25 @@ * Generate test file. */ function simpletest_generate_file($filename, $width, $lines, $type = 'binary-text') { - $size = $width * $lines - $lines; - - // Generate random text $text = ''; - for ($i = 0; $i < $size; $i++) { - switch ($type) { - case 'text': - $text .= chr(rand(32, 126)); - break; - case 'binary': - $text .= chr(rand(0, 31)); - break; - case 'binary-text': - default: - $text .= rand(0, 1); - break; + for ($i = 0; $i < $lines; $i++) { + // Generate $width - 1 characters to leave space for the "\n" character. + for ($j = 0; $j < $width - 1; $j++) { + switch ($type) { + case 'text': + $text .= chr(rand(32, 126)); + break; + case 'binary': + $text .= chr(rand(0, 31)); + break; + case 'binary-text': + default: + $text .= rand(0, 1); + break; + } } + $text .= "\n"; } - $text = wordwrap($text, $width - 1, "\n", TRUE) . "\n"; // Add \n for symmetrical file. // Create filename. file_put_contents('public://' . $filename . '.txt', $text); diff -Naur drupal-7.23/modules/simpletest/simpletest.test drupal-7.66/modules/simpletest/simpletest.test --- drupal-7.23/modules/simpletest/simpletest.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/simpletest.test 2019-04-17 22:20:46.000000000 +0200 @@ -322,6 +322,14 @@ * Test internal testing framework browser. */ class SimpleTestBrowserTestCase extends DrupalWebTestCase { + + /** + * A flag indicating whether a cookie has been set in a test. + * + * @var bool + */ + protected static $cookieSet = FALSE; + public static function getInfo() { return array( 'name' => 'SimpleTest browser', @@ -380,6 +388,46 @@ $urls = $this->xpath('//a[text()=:text]', array(':text' => 'A second "even more weird" link, in memory of George O\'Malley')); $this->assertEqual($urls[0]['href'], 'link2', 'Match with mixed single and double quotes.'); } + + /** + * Tests that cookies set during a request are available for testing. + */ + public function testCookies() { + // Check that the $this->cookies property is populated when a user logs in. + $user = $this->drupalCreateUser(); + $edit = array('name' => $user->name, 'pass' => $user->pass_raw); + $this->drupalPost('<front>', $edit, t('Log in')); + $this->assertEqual(count($this->cookies), 1, 'A cookie is set when the user logs in.'); + + // Check that the name and value of the cookie match the request data. + $cookie_header = $this->drupalGetHeader('set-cookie', TRUE); + + // The name and value are located at the start of the string, separated by + // an equals sign and ending in a semicolon. + preg_match('/^([^=]+)=([^;]+)/', $cookie_header, $matches); + $name = $matches[1]; + $value = $matches[2]; + + $this->assertTrue(array_key_exists($name, $this->cookies), 'The cookie name is correct.'); + $this->assertEqual($value, $this->cookies[$name]['value'], 'The cookie value is correct.'); + + // Set a flag indicating that a cookie has been set in this test. + // @see SimpleTestBrowserTestCase::testCookieDoesNotBleed(). + self::$cookieSet = TRUE; + } + + /** + * Tests that the cookies from a previous test do not bleed into a new test. + * + * @see SimpleTestBrowserTestCase::testCookies(). + */ + public function testCookieDoesNotBleed() { + // In order for this test to be effective it should always run after the + // testCookies() test. + $this->assertTrue(self::$cookieSet, 'Tests have been executed in the expected order.'); + $this->assertEqual(count($this->cookies), 0, 'No cookies are present at the start of a new test.'); + } + } class SimpleTestMailCaptureTestCase extends DrupalWebTestCase { @@ -703,7 +751,9 @@ $classes_all = simpletest_test_get_all(); foreach (array( 'Drupal\\simpletest\\Tests\\PSR0WebTest', + 'Drupal\\simpletest\\Tests\\PSR4WebTest', 'Drupal\\psr_0_test\\Tests\\ExampleTest', + 'Drupal\\psr_4_test\\Tests\\ExampleTest', ) as $class) { $this->assert(!empty($classes_all['SimpleTest'][$class]), t('Class @class must be discovered by simpletest_test_get_all().', array('@class' => $class))); } @@ -726,15 +776,20 @@ // Don't expect PSR-0 tests to be discovered on older PHP versions. return; } - // This one is provided by simpletest itself via PSR-0. + // These are provided by simpletest itself via PSR-0 and PSR-4. $this->assertText('PSR0 web test'); + $this->assertText('PSR4 web test'); $this->assertText('PSR0 example test: PSR-0 in disabled modules.'); + $this->assertText('PSR4 example test: PSR-4 in disabled modules.'); $this->assertText('PSR0 example test: PSR-0 in nested subfolders.'); + $this->assertText('PSR4 example test: PSR-4 in nested subfolders.'); // Test each test individually. foreach (array( 'Drupal\\psr_0_test\\Tests\\ExampleTest', 'Drupal\\psr_0_test\\Tests\\Nested\\NestedExampleTest', + 'Drupal\\psr_4_test\\Tests\\ExampleTest', + 'Drupal\\psr_4_test\\Tests\\Nested\\NestedExampleTest', ) as $class) { $this->drupalGet('admin/config/development/testing'); $edit = array($class => TRUE); diff -Naur drupal-7.23/modules/simpletest/src/Tests/PSR4WebTest.php drupal-7.66/modules/simpletest/src/Tests/PSR4WebTest.php --- drupal-7.23/modules/simpletest/src/Tests/PSR4WebTest.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/src/Tests/PSR4WebTest.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\simpletest\Tests; + +class PSR4WebTest extends \DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'PSR4 web test', + 'description' => 'We want to assert that this PSR-4 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/actions_loop_test.info drupal-7.66/modules/simpletest/tests/actions_loop_test.info --- drupal-7.23/modules/simpletest/tests/actions_loop_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/actions_loop_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/ajax.test drupal-7.66/modules/simpletest/tests/ajax.test --- drupal-7.23/modules/simpletest/tests/ajax.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/ajax.test 2019-04-17 22:20:46.000000000 +0200 @@ -293,7 +293,7 @@ $this->assertCommand($commands, $expected, "'changed' AJAX command (with asterisk) issued with correct selector"); // Tests the 'css' command. - $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue."))); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the '#box' div to be blue."))); $expected = array( 'command' => 'css', 'selector' => '#css_div', @@ -368,6 +368,14 @@ 'settings' => array('ajax_forms_test' => array('foo' => 42)), ); $this->assertCommand($commands, $expected, "'settings' AJAX command issued with correct data"); + + // Tests the 'add_css' command. + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'add_css' command"))); + $expected = array( + 'command' => 'add_css', + 'data' => 'my/file.css', + ); + $this->assertCommand($commands, $expected, "'add_css' AJAX command issued with correct data"); } } @@ -498,6 +506,85 @@ } /** + * Test Ajax forms when page caching for anonymous users is turned on. + */ +class AJAXFormPageCacheTestCase extends AJAXTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'AJAX forms on cached pages', + 'description' => 'Tests that AJAX forms work properly for anonymous users on cached pages.', + 'group' => 'AJAX', + ); + } + + public function setUp() { + parent::setUp(); + + variable_set('cache', TRUE); + } + + /** + * Return the build id of the current form. + */ + protected function getFormBuildId() { + $build_id_fields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page'); + return (string) $build_id_fields[0]['value']; + } + + /** + * Create a simple form, then POST to system/ajax to change to it. + */ + public function testSimpleAJAXFormValue() { + $this->drupalGet('ajax_forms_test_get_form'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $build_id_initial = $this->getFormBuildId(); + + $edit = array('select' => 'green'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_first_ajax = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission'); + $expected = array( + 'command' => 'updateBuildId', + 'old' => $build_id_initial, + 'new' => $build_id_first_ajax, + ); + $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission'); + + $edit = array('select' => 'red'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_second_ajax = $this->getFormBuildId(); + $this->assertEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id remains the same on subsequent AJAX submissions'); + + // Repeat the test sequence but this time with a page loaded from the cache. + $this->drupalGet('ajax_forms_test_get_form'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $build_id_from_cache_initial = $this->getFormBuildId(); + $this->assertEqual($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request'); + + $edit = array('select' => 'green'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_from_cache_first_ajax = $this->getFormBuildId(); + $this->assertNotEqual($build_id_from_cache_initial, $build_id_from_cache_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission'); + $this->assertNotEqual($build_id_first_ajax, $build_id_from_cache_first_ajax, 'Build id from first user is not reused'); + $expected = array( + 'command' => 'updateBuildId', + 'old' => $build_id_from_cache_initial, + 'new' => $build_id_from_cache_first_ajax, + ); + $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission'); + + $edit = array('select' => 'red'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_from_cache_second_ajax = $this->getFormBuildId(); + $this->assertEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id remains the same on subsequent AJAX submissions'); + } +} + + +/** * Miscellaneous Ajax tests using ajax_test module. */ class AJAXElementValidation extends AJAXTestCase { diff -Naur drupal-7.23/modules/simpletest/tests/ajax_forms_test.info drupal-7.66/modules/simpletest/tests/ajax_forms_test.info --- drupal-7.23/modules/simpletest/tests/ajax_forms_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/ajax_forms_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/ajax_forms_test.module drupal-7.66/modules/simpletest/tests/ajax_forms_test.module --- drupal-7.23/modules/simpletest/tests/ajax_forms_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/ajax_forms_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -157,7 +157,7 @@ // Shows the Ajax 'css' command. $form['css_command_example'] = array( - '#value' => t("Set the the '#box' div to be blue."), + '#value' => t("Set the '#box' div to be blue."), '#type' => 'submit', '#ajax' => array( 'callback' => 'ajax_forms_test_advanced_commands_css_callback', @@ -254,6 +254,15 @@ ), ); + // Shows the Ajax 'add_css' command. + $form['add_css_command_example'] = array( + '#type' => 'submit', + '#value' => t("AJAX 'add_css' command"), + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_add_css_callback', + ), + ); + $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), @@ -407,6 +416,15 @@ } /** + * Ajax callback for 'add_css'. + */ +function ajax_forms_test_advanced_commands_add_css_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_add_css('my/file.css'); + return array('#type' => 'ajax', '#commands' => $commands); +} + +/** * This form and its related submit and callback functions demonstrate * not validating another form element when a single Ajax element is triggered. * diff -Naur drupal-7.23/modules/simpletest/tests/ajax_test.info drupal-7.66/modules/simpletest/tests/ajax_test.info --- drupal-7.23/modules/simpletest/tests/ajax_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/ajax_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/batch_test.callbacks.inc drupal-7.66/modules/simpletest/tests/batch_test.callbacks.inc --- drupal-7.23/modules/simpletest/tests/batch_test.callbacks.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/batch_test.callbacks.inc 2019-04-17 22:20:46.000000000 +0200 @@ -7,6 +7,8 @@ */ /** + * Implements callback_batch_operation(). + * * Simple batch operation. */ function _batch_test_callback_1($id, $sleep, &$context) { @@ -20,6 +22,8 @@ } /** + * Implements callback_batch_operation(). + * * Multistep batch operation. */ function _batch_test_callback_2($start, $total, $sleep, &$context) { @@ -53,6 +57,8 @@ } /** + * Implements callback_batch_operation(). + * * Simple batch operation. */ function _batch_test_callback_5($id, $sleep, &$context) { @@ -68,6 +74,8 @@ } /** + * Implements callback_batch_operation(). + * * Batch operation setting up its own batch. */ function _batch_test_nested_batch_callback() { @@ -76,6 +84,8 @@ } /** + * Implements callback_batch_finished(). + * * Common 'finished' callbacks for batches 1 to 4. */ function _batch_test_finished_helper($batch_id, $success, $results, $operations) { @@ -99,6 +109,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 0. */ function _batch_test_finished_0($success, $results, $operations) { @@ -106,6 +118,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 1. */ function _batch_test_finished_1($success, $results, $operations) { @@ -113,6 +127,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 2. */ function _batch_test_finished_2($success, $results, $operations) { @@ -120,6 +136,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 3. */ function _batch_test_finished_3($success, $results, $operations) { @@ -127,6 +145,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 4. */ function _batch_test_finished_4($success, $results, $operations) { @@ -134,6 +154,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 5. */ function _batch_test_finished_5($success, $results, $operations) { diff -Naur drupal-7.23/modules/simpletest/tests/batch_test.info drupal-7.66/modules/simpletest/tests/batch_test.info --- drupal-7.23/modules/simpletest/tests/batch_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/batch_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/boot.test drupal-7.66/modules/simpletest/tests/boot.test --- drupal-7.23/modules/simpletest/tests/boot.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/boot.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,38 @@ +<?php + +/** + * Perform early bootstrap tests. + */ +class EarlyBootstrapTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Early bootstrap test', + 'description' => 'Confirm that calling module_implements() during early bootstrap does not pollute the module_implements() cache.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp('boot_test_1', 'boot_test_2'); + } + + /** + * Test hook_boot() on both regular and "early exit" pages. + */ + public function testHookBoot() { + $paths = array('', 'early_exit'); + foreach ($paths as $path) { + // Empty the module_implements() caches. + module_implements(NULL, FALSE, TRUE); + // Do a request to the front page, which will call module_implements() + // during hook_boot(). + $this->drupalGet($path); + // Reset the static cache so we get implementation data from the persistent + // cache. + drupal_static_reset(); + // Make sure we get a full list of all modules implementing hook_help(). + $modules = module_implements('help'); + $this->assertTrue(in_array('boot_test_2', $modules)); + } + } +} diff -Naur drupal-7.23/modules/simpletest/tests/boot_test_1.info drupal-7.66/modules/simpletest/tests/boot_test_1.info --- drupal-7.23/modules/simpletest/tests/boot_test_1.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/boot_test_1.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = Early bootstrap tests +description = A support module for hook_boot testing. +core = 7.x +package = Testing +version = VERSION +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/boot_test_1.module drupal-7.66/modules/simpletest/tests/boot_test_1.module --- drupal-7.23/modules/simpletest/tests/boot_test_1.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/boot_test_1.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,21 @@ +<?php + +/** + * @file + * Tests calling module_implements() during hook_boot() invocation. + */ + +/** + * Implements hook_boot(). + */ +function boot_test_1_boot() { + // Calling module_implements during hook_boot() will return "vital" modules + // only, and this list of modules will be statically cached. + module_implements('help'); + // Define a special path to test that the static cache isn't written away + // if we exit before having completed the bootstrap. + if ($_GET['q'] == 'early_exit') { + module_implements_write_cache(); + exit(); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/boot_test_2.info drupal-7.66/modules/simpletest/tests/boot_test_2.info --- drupal-7.23/modules/simpletest/tests/boot_test_2.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/boot_test_2.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = Early bootstrap tests +description = A support module for hook_boot hook testing. +core = 7.x +package = Testing +version = VERSION +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/boot_test_2.module drupal-7.66/modules/simpletest/tests/boot_test_2.module --- drupal-7.23/modules/simpletest/tests/boot_test_2.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/boot_test_2.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Defines a hook_help() implementation in a non-"bootstrap" module. + */ + +/** + * Implements hook_help(). + */ +function boot_test_2_help($path, $arg) { + // Empty hook. +} diff -Naur drupal-7.23/modules/simpletest/tests/bootstrap.test drupal-7.66/modules/simpletest/tests/bootstrap.test --- drupal-7.23/modules/simpletest/tests/bootstrap.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/bootstrap.test 2019-04-17 22:20:46.000000000 +0200 @@ -70,6 +70,15 @@ 'Proxy forwarding with trusted proxy got forwarded IP address.' ); + // Proxy forwarding on and proxy address trusted and visiting from proxy. + $_SERVER['REMOTE_ADDR'] = $this->proxy_ip; + $_SERVER['HTTP_X_FORWARDED_FOR'] = $this->proxy_ip; + drupal_static_reset('ip_address'); + $this->assertTrue( + ip_address() == $this->proxy_ip, + 'Visiting from trusted proxy got proxy IP address.' + ); + // Multi-tier architecture with comma separated values in header. $_SERVER['REMOTE_ADDR'] = $this->proxy_ip; $_SERVER['HTTP_X_FORWARDED_FOR'] = implode(', ', array($this->untrusted_ip, $this->forwarded_ip, $this->proxy2_ip)); @@ -93,6 +102,11 @@ $this->assertFalse(drupal_valid_http_host('security\\.drupal.org:80'), 'HTTP_HOST with \\ is invalid'); $this->assertFalse(drupal_valid_http_host('security<.drupal.org:80'), 'HTTP_HOST with < is invalid'); $this->assertFalse(drupal_valid_http_host('security..drupal.org:80'), 'HTTP_HOST with .. is invalid'); + // Verifies that host names are shorter than 1000 characters. + $this->assertFalse(drupal_valid_http_host(str_repeat('x', 1001)), 'HTTP_HOST with more than 1000 characters is invalid.'); + $this->assertFalse(drupal_valid_http_host(str_repeat('.', 101)), 'HTTP_HOST with more than 100 subdomains is invalid.'); + $this->assertFalse(drupal_valid_http_host(str_repeat(':', 101)), 'HTTP_HOST with more than 100 portseparators is invalid.'); + // IPv6 loopback address $this->assertTrue(drupal_valid_http_host('[::1]:80'), 'HTTP_HOST containing IPv6 loopback is valid'); } @@ -139,7 +153,7 @@ $this->assertResponse(200, 'Conditional request without If-None-Match returned 200 OK.'); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); - $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC1123, strtotime($last_modified) + 1), 'If-None-Match: ' . $etag)); + $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC7231, strtotime($last_modified) + 1), 'If-None-Match: ' . $etag)); $this->assertResponse(200, 'Conditional request with new a If-Modified-Since date newer than Last-Modified returned 200 OK.'); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); @@ -147,7 +161,9 @@ $this->drupalLogin($user); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); $this->assertResponse(200, 'Conditional request returned 200 OK for authenticated user.'); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Absense of Page was not cached.'); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Absence of Page was not cached.'); + $this->assertFalse($this->drupalGetHeader('ETag'), 'ETag HTTP headers are not present for logged in users.'); + $this->assertFalse($this->drupalGetHeader('Last-Modified'), 'Last-Modified HTTP headers are not present for logged in users.'); } /** @@ -184,7 +200,7 @@ $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Caching was bypassed.'); $this->assertTrue(strpos($this->drupalGetHeader('Vary'), 'Cookie') === FALSE, 'Vary: Cookie header was not sent.'); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', 'Cache-Control header was sent.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate', 'Cache-Control header was sent.'); $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.'); $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', 'Custom header was sent.'); @@ -219,6 +235,18 @@ $this->assertFalse($this->drupalGetHeader('Content-Encoding'), 'A Content-Encoding header was not sent.'); $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), 'Site title matches.'); $this->assertRaw('</html>', 'Page was not compressed.'); + + // Disable compression mode. + variable_set('page_compression', FALSE); + + // Verify if cached page is still available for a client with compression support. + $this->drupalGet('', array(), array('Accept-Encoding: gzip,deflate')); + $this->drupalSetContent(gzinflate(substr($this->drupalGetContent(), 10, -8))); + $this->assertRaw('</html>', 'Page was delivered after compression mode is changed (compression support enabled).'); + + // Verify if cached page is still available for a client without compression support. + $this->drupalGet(''); + $this->assertRaw('</html>', 'Page was delivered after compression mode is changed (compression support disabled).'); } } @@ -270,6 +298,39 @@ } /** + * Tests the auto-loading behavior of the code registry. + */ +class BootstrapAutoloadTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Code registry', + 'description' => 'Test that the code registry functions correctly.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('drupal_autoload_test'); + } + + /** + * Tests that autoloader name matching is not case sensitive. + */ + function testAutoloadCase() { + // Test interface autoloader. + $this->assertTrue(drupal_autoload_interface('drupalautoloadtestinterface'), 'drupal_autoload_interface() recognizes <em>DrupalAutoloadTestInterface</em> in lower case.'); + // Test class autoloader. + $this->assertTrue(drupal_autoload_class('drupalautoloadtestclass'), 'drupal_autoload_class() recognizes <em>DrupalAutoloadTestClass</em> in lower case.'); + // Test trait autoloader. + if (version_compare(PHP_VERSION, '5.4') >= 0) { + $this->assertTrue(drupal_autoload_trait('drupalautoloadtesttrait'), 'drupal_autoload_trait() recognizes <em>DrupalAutoloadTestTrait</em> in lower case.'); + } + } + +} + +/** * Test hook_boot() and hook_exit(). */ class HookBootExitTestCase extends DrupalWebTestCase { @@ -327,13 +388,20 @@ public static function getInfo() { return array( - 'name' => 'Get filename test', - 'description' => 'Test that drupal_get_filename() works correctly when the file is not found in the database.', + 'name' => 'Get filename test (without the system table)', + 'description' => 'Test that drupal_get_filename() works correctly when the database is not available.', 'group' => 'Bootstrap', ); } /** + * The last file-related error message triggered by the filename test. + * + * Used by BootstrapGetFilenameTestCase::testDrupalGetFilename(). + */ + protected $getFilenameTestTriggeredError; + + /** * Test that drupal_get_filename() works correctly when the file is not found in the database. */ function testDrupalGetFilename() { @@ -362,6 +430,203 @@ // automatically check there for 'script' files, just as it does for (e.g.) // 'module' files in modules. $this->assertIdentical(drupal_get_filename('script', 'test'), 'scripts/test.script', t('Retrieve test script location.')); + + // When searching for a module that does not exist, drupal_get_filename() + // should return NULL and trigger an appropriate error message. + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $non_existing_module = $this->randomName(); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for an item that does not exist triggers the correct error.'); + restore_error_handler(); + + // Check that the result is stored in the file system scan cache. + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files static variable.'); + + // Performing the search again in the same request still should not find + // the file, but the error message should not be repeated (therefore we do + // not override the error handler here). + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL during the second search.'); + } + + /** + * Skips handling of "file not found" errors. + */ + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + // Skip error handling if this is a "file not found" error. + if (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { + $this->getFilenameTestTriggeredError = $message; + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); + } +} + +/** + * Test drupal_get_filename() in the context of a full Drupal installation. + */ +class BootstrapGetFilenameWebTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Get filename test (full installation)', + 'description' => 'Test that drupal_get_filename() works correctly in the context of a full Drupal installation.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * The last file-related error message triggered by the filename test. + * + * Used by BootstrapGetFilenameWebTestCase::testDrupalGetFilename(). + */ + protected $getFilenameTestTriggeredError; + + /** + * Test that drupal_get_filename() works correctly with a full Drupal site. + */ + function testDrupalGetFilename() { + // Search for a module that exists in the file system and the {system} + // table and make sure that it is found. + $this->assertIdentical(drupal_get_filename('module', 'node'), 'modules/node/node.module', 'Module found at expected location.'); + + // Search for a module that does not exist in either the file system or the + // {system} table. Make sure that an appropriate error is triggered and + // that the module winds up in the static and persistent cache. + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $non_existing_module = $this->randomName(); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for a module that does not exist triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files persistent cache.'); + + // Simulate moving a module to a location that does not match the location + // in the {system} table and perform similar tests as above. + db_update('system') + ->fields(array('filename' => 'modules/simpletest/tests/fake_location/module_test.module')) + ->condition('name', 'module_test') + ->condition('type', 'module') + ->execute(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertIdentical(drupal_get_filename('module', 'module_test'), 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved finds the module at its new location.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module has moved within the file system: %name', array('%name' => 'module_test'))) === 0, 'Searching for a module that has moved triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module']['module_test'], 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module']['module_test'], 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved creates a record in the missing and moved files persistent cache.'); + + // Simulate a module that exists in the {system} table but does not exist + // in the file system and perform similar tests as above. + $non_existing_module = $this->randomName(); + db_update('system') + ->fields(array('name' => $non_existing_module)) + ->condition('name', 'module_test') + ->condition('type', 'module') + ->execute(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that exists in the system table but not in the file system returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for a module that exists in the system table but not in the file system triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that exists in the system table but not in the file system creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module'][$non_existing_module], FALSE, 'Searching for a module that exists in the system table but not in the file system creates a record in the missing and moved files persistent cache.'); + + // Simulate a module that exists in the file system but not in the {system} + // table and perform similar tests as above. + db_delete('system') + ->condition('name', 'common_test') + ->condition('type', 'module') + ->execute(); + system_list_reset(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertIdentical(drupal_get_filename('module', 'common_test'), 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table finds the module at its actual location.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module has moved within the file system: %name', array('%name' => 'common_test'))) === 0, 'Searching for a module that does not exist in the system table triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module']['common_test'], 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module']['common_test'], 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table creates a record in the missing and moved files persistent cache.'); + } + + /** + * Skips handling of "file not found" errors. + */ + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + // Skip error handling if this is a "file not found" error. + if (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { + $this->getFilenameTestTriggeredError = $message; + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); + } + + /** + * Test that watchdog messages about missing files are correctly recorded. + */ + public function testWatchdog() { + // Search for a module that does not exist in either the file system or the + // {system} table. Make sure that an appropriate warning is recorded in the + // logs. + $non_existing_module = $this->randomName(); + $query_parameters = array( + ':type' => 'php', + ':severity' => WATCHDOG_WARNING, + ); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND severity = :severity', $query_parameters)->fetchField(), 0, 'No warning message appears in the logs before searching for a module that does not exist.'); + // Trigger the drupal_get_filename() call. This must be done via a request + // to a separate URL since the watchdog() will happen in a shutdown + // function, and so that SimpleTest can be told to ignore (and not fail as + // a result of) the expected PHP warnings generated during this process. + variable_set('system_test_drupal_get_filename_test_module_name', $non_existing_module); + $this->drupalGet('system-test/drupal-get-filename'); + $message_variables = db_query('SELECT variables FROM {watchdog} WHERE type = :type AND severity = :severity', $query_parameters)->fetchCol(); + $this->assertEqual(count($message_variables), 1, 'A single warning message appears in the logs after searching for a module that does not exist.'); + $variables = reset($message_variables); + $variables = unserialize($variables); + $this->assertTrue(isset($variables['!message']) && strpos($variables['!message'], format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) !== FALSE, 'The warning message that appears in the logs after searching for a module that does not exist contains the expected text.'); + } + + /** + * Test that drupal_get_filename() does not break recursive rebuilds. + */ + public function testRecursiveRebuilds() { + // Ensure that the drupal_get_filename() call due to a missing module does + // not break the data returned by an attempted recursive rebuild. The code + // path which is tested is as follows: + // - Call drupal_get_schema(). + // - Within a hook_schema() implementation, trigger a drupal_get_filename() + // search for a nonexistent module. + // - In the watchdog() call that results from that, trigger + // drupal_get_schema() again. + // Without some kind of recursion protection, this could cause the second + // drupal_get_schema() call to return incomplete results. This test ensures + // that does not happen. + $non_existing_module = $this->randomName(); + variable_set('system_test_drupal_get_filename_test_module_name', $non_existing_module); + $this->drupalGet('system-test/drupal-get-filename-with-schema-rebuild'); + $original_drupal_get_schema_tables = variable_get('system_test_drupal_get_filename_with_schema_rebuild_original_tables'); + $final_drupal_get_schema_tables = variable_get('system_test_drupal_get_filename_with_schema_rebuild_final_tables'); + $this->assertTrue(!empty($original_drupal_get_schema_tables)); + $this->assertTrue(!empty($final_drupal_get_schema_tables)); + $this->assertEqual($original_drupal_get_schema_tables, $final_drupal_get_schema_tables); } } @@ -464,16 +729,12 @@ * Tests that the drupal_check_memory_limit() function works as expected. */ function testCheckMemoryLimit() { - $memory_limit = ini_get('memory_limit'); // Test that a very reasonable amount of memory is available. $this->assertTrue(drupal_check_memory_limit('30MB'), '30MB of memory tested available.'); - // Get the available memory and multiply it by two to make it unreasonably - // high. - $twice_avail_memory = ($memory_limit * 2) . 'MB'; - + // Test an unlimited memory limit. // The function should always return true if the memory limit is set to -1. - $this->assertTrue(drupal_check_memory_limit($twice_avail_memory, -1), 'drupal_check_memory_limit() returns TRUE when a limit of -1 (none) is supplied'); + $this->assertTrue(drupal_check_memory_limit('9999999999YB', -1), 'drupal_check_memory_limit() returns TRUE when a limit of -1 (none) is supplied'); // Test that even though we have 30MB of memory available - the function // returns FALSE when given an upper limit for how much memory can be used. @@ -529,3 +790,85 @@ } } } + +/** + * Tests for $_GET['destination'] and $_REQUEST['destination'] validation. + */ +class BootstrapDestinationTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'URL destination validation', + 'description' => 'Test that $_GET[\'destination\'] and $_REQUEST[\'destination\'] cannot contain external URLs.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * Tests that $_GET/$_REQUEST['destination'] only contain internal URLs. + * + * @see _drupal_bootstrap_variables() + * @see system_test_get_destination() + * @see system_test_request_destination() + */ + public function testDestination() { + $test_cases = array( + array( + 'input' => 'node', + 'output' => 'node', + 'message' => "Standard internal example node path is present in the 'destination' parameter.", + ), + array( + 'input' => '/example.com', + 'output' => '/example.com', + 'message' => 'Internal path with one leading slash is allowed.', + ), + array( + 'input' => '//example.com/test', + 'output' => '', + 'message' => 'External URL without scheme is not allowed.', + ), + array( + 'input' => 'example:test', + 'output' => 'example:test', + 'message' => 'Internal URL using a colon is allowed.', + ), + array( + 'input' => 'http://example.com', + 'output' => '', + 'message' => 'External URL is not allowed.', + ), + array( + 'input' => 'javascript:alert(0)', + 'output' => 'javascript:alert(0)', + 'message' => 'Javascript URL is allowed because it is treated as an internal URL.', + ), + ); + foreach ($test_cases as $test_case) { + // Test $_GET['destination']. + $this->drupalGet('system-test/get-destination', array('query' => array('destination' => $test_case['input']))); + $this->assertIdentical($test_case['output'], $this->drupalGetContent(), $test_case['message']); + // Test $_REQUEST['destination']. There's no form to submit to, so + // drupalPost() won't work here; this just tests a direct $_POST request + // instead. + $curl_parameters = array( + CURLOPT_URL => $this->getAbsoluteUrl('system-test/request-destination'), + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => 'destination=' . urlencode($test_case['input']), + CURLOPT_HTTPHEADER => array(), + ); + $post_output = $this->curlExec($curl_parameters); + $this->assertIdentical($test_case['output'], $post_output, $test_case['message']); + } + + // Make sure that 404 pages do not populate $_GET['destination'] with + // external URLs. + variable_set('site_404', 'system-test/get-destination'); + $this->drupalGet('http://example.com', array('external' => FALSE)); + $this->assertIdentical('', $this->drupalGetContent(), 'External URL is not allowed on 404 pages.'); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/common.test drupal-7.66/modules/simpletest/tests/common.test --- drupal-7.23/modules/simpletest/tests/common.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/common.test 2019-04-17 22:20:46.000000000 +0200 @@ -76,7 +76,7 @@ class CommonURLUnitTest extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => 'URL generation tests', + 'name' => 'URL generation unit tests', 'description' => 'Confirm that url(), drupal_get_query_parameters(), drupal_http_build_query(), and l() work correctly with various input.', 'group' => 'System', ); @@ -169,7 +169,7 @@ $this->assertEqual(drupal_http_build_query(array('a' => ' &#//+%20@۞')), 'a=%20%26%23//%2B%2520%40%DB%9E', 'Value was properly encoded.'); $this->assertEqual(drupal_http_build_query(array(' &#//+%20@۞' => 'a')), '%20%26%23%2F%2F%2B%2520%40%DB%9E=a', 'Key was properly encoded.'); $this->assertEqual(drupal_http_build_query(array('a' => '1', 'b' => '2', 'c' => '3')), 'a=1&b=2&c=3', 'Multiple values were properly concatenated.'); - $this->assertEqual(drupal_http_build_query(array('a' => array('b' => '2', 'c' => '3'), 'd' => 'foo')), 'a[b]=2&a[c]=3&d=foo', 'Nested array was properly encoded.'); + $this->assertEqual(drupal_http_build_query(array('a' => array('b' => '2', 'c' => '3'), 'd' => 'foo')), 'a%5Bb%5D=2&a%5Bc%5D=3&d=foo', 'Nested array was properly encoded.'); } /** @@ -209,7 +209,16 @@ // Test that drupal can recognize an absolute URL. Used to prevent attack vectors. $this->assertTrue(url_is_external($url), 'Correctly identified an external URL.'); + // External URL without an explicit protocol. + $url = '//drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; + $this->assertTrue(url_is_external($url), 'Correctly identified an external URL without a protocol part.'); + + // Internal URL starting with a slash. + $url = '/drupal.org'; + $this->assertFalse(url_is_external($url), 'Correctly identified an internal URL with a leading slash.'); + // Test the parsing of absolute URLs. + $url = 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; $result = array( 'path' => 'http://drupal.org/foo/bar', 'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''), @@ -349,6 +358,108 @@ $query = array($this->randomName(5) => $this->randomName(5)); $result = url($url, array('query' => $query)); $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result, 'External URL query string can be extended with a custom query string in $options.'); + + // Verify that an internal URL does not result in an external URL without + // protocol part. + $url = '/drupal.org'; + $result = url($url); + $this->assertTrue(strpos($result, '//') === FALSE, 'Internal URL does not turn into an external URL.'); + + // Verify that an external URL without protocol part is recognized as such. + $url = '//drupal.org'; + $result = url($url); + $this->assertEqual($url, $result, 'External URL without protocol is not altered.'); + } +} + +/** + * Web tests for URL generation functions. + */ +class CommonURLWebTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'URL generation web tests', + 'description' => 'Confirm that URL-generating functions work correctly on specific site paths.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp('common_test'); + } + + /** + * Tests the url() function on internal paths which mimic external URLs. + */ + function testInternalPathMimicsExternal() { + // Ensure that calling url(current_path()) on "/http://example.com" (an + // internal path which mimics an external URL) always links to the internal + // path, not the external URL. This helps protect against external URL link + // injection vulnerabilities. + variable_set('common_test_link_to_current_path', TRUE); + $this->drupalGet('/http://example.com'); + $this->clickLink('link which should point to the current path'); + $this->assertUrl('/http://example.com'); + $this->assertText('link which should point to the current path'); + } +} + +/** + * Tests url_is_external(). + */ +class UrlIsExternalUnitTest extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'External URL checking', + 'description' => 'Performs tests on url_is_external().', + 'group' => 'System', + ); + } + + /** + * Tests if each URL is external or not. + */ + function testUrlIsExternal() { + foreach ($this->examples() as $path => $expected) { + $this->assertIdentical(url_is_external($path), $expected, $path); + } + } + + /** + * Provides data for testUrlIsExternal(). + * + * @return array + * An array of test data, keyed by a path, with the expected value where + * TRUE is external, and FALSE is not external. + */ + protected function examples() { + return array( + // Simple external URLs. + 'http://example.com' => TRUE, + 'https://example.com' => TRUE, + 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo' => TRUE, + '//drupal.org' => TRUE, + // Some browsers ignore or strip leading control characters. + "\x00//www.example.com" => TRUE, + "\x08//www.example.com" => TRUE, + "\x1F//www.example.com" => TRUE, + "\n//www.example.com" => TRUE, + // JSON supports decoding directly from UTF-8 code points. + json_decode('"\u00AD"') . "//www.example.com" => TRUE, + json_decode('"\u200E"') . "//www.example.com" => TRUE, + json_decode('"\uE0020"') . "//www.example.com" => TRUE, + json_decode('"\uE000"') . "//www.example.com" => TRUE, + // Backslashes should be normalized to forward. + '\\\\example.com' => TRUE, + // Local URLs. + 'node' => FALSE, + '/system/ajax' => FALSE, + '?q=foo:bar' => FALSE, + 'node/edit:me' => FALSE, + '/drupal.org' => FALSE, + '<front>' => FALSE, + ); } } @@ -661,6 +772,10 @@ drupal_add_css($css); $styles = drupal_get_css(); $this->assertTrue(strpos($styles, $css) > 0, 'Rendered CSS includes the added stylesheet.'); + // Verify that newlines are properly added inside style tags. + $query_string = variable_get('css_js_query_string', '0'); + $css_processed = "<style type=\"text/css\" media=\"all\">\n@import url(\"" . check_plain(file_create_url($css)) . "?" . $query_string ."\");\n</style>"; + $this->assertEqual(trim($styles), $css_processed, 'Rendered CSS includes newlines inside style tags for JavaScript use.'); } /** @@ -687,6 +802,31 @@ } /** + * Tests removing charset when rendering stylesheets with preprocessing on. + */ + function testRenderRemoveCharsetPreprocess() { + $cases = array( + array( + 'asset' => '@charset "UTF-8";html{font-family:"sans-serif";}', + 'expected' => 'html{font-family:"sans-serif";}', + ), + // This asset contains extra \n character. + array( + 'asset' => "@charset 'UTF-8';\nhtml{font-family:'sans-serif';}", + 'expected' => "\nhtml{font-family:'sans-serif';}", + ), + ); + + foreach ($cases as $case) { + $this->assertEqual( + $case['expected'], + drupal_load_stylesheet_content($case['asset']), + 'CSS optimizing correctly removes the charset declaration.' + ); + } + } + + /** * Tests rendering inline stylesheets with preprocessing off. */ function testRenderInlineNoPreprocess() { @@ -839,6 +979,31 @@ // Verify that invalid characters (including non-breaking space) are stripped from the identifier. $this->assertIdentical(drupal_clean_css_identifier('invalid !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', array()), 'invalididentifier', 'Strip invalid characters.'); + + // Verify that double underscores are replaced in the identifier by default. + $identifier = 'css__identifier__with__double__underscores'; + $expected = 'css--identifier--with--double--underscores'; + $this->assertIdentical(drupal_clean_css_identifier($identifier), $expected, 'Verify double underscores are replaced with double hyphens by default.'); + + // Verify that double underscores are preserved in the identifier if the + // variable allow_css_double_underscores is set to TRUE. + $this->setAllowCSSDoubleUnderscores(TRUE); + $this->assertIdentical(drupal_clean_css_identifier($identifier), $identifier, 'Verify double underscores are preserved if the allow_css_double_underscores set to TRUE.'); + + // To avoid affecting other test cases, set the variable + // allow_css_double_underscores to FALSE which is the default value. + $this->setAllowCSSDoubleUnderscores(FALSE); + } + + /** + * Set the variable allow_css_double_underscores and reset the cache. + * + * @param $value bool + * A new value to be set to allow_css_double_underscores. + */ + function setAllowCSSDoubleUnderscores($value) { + $GLOBALS['conf']['allow_css_double_underscores'] = $value; + drupal_static_reset('drupal_clean_css_identifier:allow_css_double_underscores'); } /** @@ -889,9 +1054,11 @@ * Tests basic CSS loading with and without optimization via drupal_load_stylesheet(). * * Known tests: - * - Retain white-space in selectors. (http://drupal.org/node/472820) - * - Proper URLs in imported files. (http://drupal.org/node/265719) - * - Retain pseudo-selectors. (http://drupal.org/node/460448) + * - Retain white-space in selectors. (https://drupal.org/node/472820) + * - Proper URLs in imported files. (https://drupal.org/node/265719) + * - Retain pseudo-selectors. (https://drupal.org/node/460448) + * - Don't adjust data URIs. (https://drupal.org/node/2142441) + * - Files imported from external URLs. (https://drupal.org/node/2014851) */ function testLoadCssBasic() { // Array of files to test living in 'simpletest/files/css_test_files/'. @@ -901,26 +1068,30 @@ $testfiles = array( 'css_input_without_import.css', 'css_input_with_import.css', + 'css_subfolder/css_input_with_import.css', 'comment_hacks.css' ); $path = drupal_get_path('module', 'simpletest') . '/files/css_test_files'; foreach ($testfiles as $file) { - $expected = file_get_contents("$path/$file.unoptimized.css"); - $unoptimized_output = drupal_load_stylesheet("$path/$file.unoptimized.css", FALSE); + $file_path = $path . '/' . $file; + $file_url = $GLOBALS['base_url'] . '/' . $file_path; + + $expected = file_get_contents($file_path . '.unoptimized.css'); + $unoptimized_output = drupal_load_stylesheet($file_path, FALSE); $this->assertEqual($unoptimized_output, $expected, format_string('Unoptimized CSS file has expected contents (@file)', array('@file' => $file))); - $expected = file_get_contents("$path/$file.optimized.css"); - $optimized_output = drupal_load_stylesheet("$path/$file", TRUE); + $expected = file_get_contents($file_path . '.optimized.css'); + $optimized_output = drupal_load_stylesheet($file_path, TRUE); $this->assertEqual($optimized_output, $expected, format_string('Optimized CSS file has expected contents (@file)', array('@file' => $file))); // Repeat the tests by accessing the stylesheets by URL. - $expected = file_get_contents("$path/$file.unoptimized.css"); - $unoptimized_output_url = drupal_load_stylesheet($GLOBALS['base_url'] . "/$path/$file.unoptimized.css", FALSE); - $this->assertEqual($unoptimized_output, $expected, format_string('Unoptimized CSS file (loaded from an URL) has expected contents (@file)', array('@file' => $file))); - - $expected = file_get_contents("$path/$file.optimized.css"); - $optimized_output = drupal_load_stylesheet($GLOBALS['base_url'] . "/$path/$file", TRUE); - $this->assertEqual($optimized_output, $expected, format_string('Optimized CSS file (loaded from an URL) has expected contents (@file)', array('@file' => $file))); + $expected = file_get_contents($file_path . '.unoptimized.css'); + $unoptimized_output_url = drupal_load_stylesheet($file_url, FALSE); + $this->assertEqual($unoptimized_output_url, $expected, format_string('Unoptimized CSS file (loaded from an URL) has expected contents (@file)', array('@file' => $file))); + + $expected = file_get_contents($file_path . '.optimized.css'); + $optimized_output_url = drupal_load_stylesheet($file_url, TRUE); + $this->assertEqual($optimized_output_url, $expected, format_string('Optimized CSS file (loaded from an URL) has expected contents (@file)', array('@file' => $file))); } } } @@ -993,8 +1164,8 @@ $result = drupal_http_request($auth); $this->drupalSetContent($result->data); - $this->assertRaw($username, '$_SERVER["PHP_AUTH_USER"] is passed correctly.'); - $this->assertRaw($password, '$_SERVER["PHP_AUTH_PW"] is passed correctly.'); + $this->assertRaw($username, 'Username is passed correctly.'); + $this->assertRaw($password, 'Password is passed correctly.'); } function testDrupalHTTPRequestRedirect() { @@ -1054,6 +1225,74 @@ } /** + * Tests parsing of the HTTP response status line. + */ +class DrupalHTTPResponseStatusLineTest extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Drupal HTTP request response status parsing', + 'description' => 'Perform unit tests on _drupal_parse_response_status().', + 'group' => 'System', + ); + } + + /** + * Tests parsing HTTP response status line. + */ + public function testStatusLine() { + // Grab the big array of test data from statusLineData(). + $data = $this->statusLineData(); + foreach($data as $test_case) { + $test_data = array_shift($test_case); + $expected = array_shift($test_case); + + $outcome = _drupal_parse_response_status($test_data); + + foreach(array_keys($expected) as $key) { + $this->assertIdentical($outcome[$key], $expected[$key]); + } + } + } + + /** + * Data provider for testStatusLine(). + * + * @return array + * Test data. + */ + protected function statusLineData() { + return array( + array( + 'HTTP/1.1 200 OK', + array( + 'http_version' => 'HTTP/1.1', + 'response_code' => '200', + 'reason_phrase' => 'OK', + ), + ), + // Data set with no reason phrase. + array( + 'HTTP/1.1 200', + array( + 'http_version' => 'HTTP/1.1', + 'response_code' => '200', + 'reason_phrase' => '', + ), + ), + // Arbitrary strings. + array( + 'version code multi word explanation', + array( + 'http_version' => 'version', + 'response_code' => 'code', + 'reason_phrase' => 'multi word explanation', + ), + ), + ); + } +} + +/** * Testing drupal_add_region_content and drupal_get_region_content. */ class DrupalSetContentTestCase extends DrupalWebTestCase { @@ -1072,7 +1311,7 @@ function testRegions() { global $theme_key; - $block_regions = array_keys(system_region_list($theme_key)); + $block_regions = system_region_list($theme_key, REGIONS_ALL, FALSE); $delimiter = $this->randomName(32); $values = array(); // Set some random content for each region available. @@ -1133,6 +1372,15 @@ $this->assertText('drupal_goto', 'Drupal goto redirect succeeded.'); $this->assertEqual($this->getUrl(), url('common-test/drupal_goto', array('query' => array('foo' => '123'), 'absolute' => TRUE)), 'Drupal goto redirected to expected URL.'); + // Test that calling drupal_goto() on the current path is not dangerous. + variable_set('common_test_redirect_current_path', TRUE); + $this->drupalGet('', array('query' => array('q' => 'http://www.example.com/'))); + $headers = $this->drupalGetHeaders(TRUE); + list(, $status) = explode(' ', $headers[0][':status'], 3); + $this->assertEqual($status, 302, 'Expected response code was sent.'); + $this->assertNotEqual($this->getUrl(), 'http://www.example.com/', 'Drupal goto did not redirect to external URL.'); + $this->assertTrue(strpos($this->getUrl(), url('<front>', array('absolute' => TRUE))) === 0, 'Drupal redirected to itself.'); + variable_del('common_test_redirect_current_path'); // Test that drupal_goto() respects ?destination=xxx. Use an complicated URL // to test that the path is encoded and decoded properly. $destination = 'common-test/drupal_goto/destination?foo=%2525&bar=123'; @@ -1319,6 +1567,127 @@ } /** + * Test the 'javascript_always_use_jquery' variable. + */ + function testJavaScriptAlwaysUseJQuery() { + // The default front page of the site should use jQuery and other standard + // scripts and settings. + $this->drupalGet(''); + $this->assertRaw('misc/jquery.js', 'Default behavior: The front page of the site includes jquery.js.'); + $this->assertRaw('misc/drupal.js', 'Default behavior: The front page of the site includes drupal.js.'); + $this->assertRaw('Drupal.settings', 'Default behavior: The front page of the site includes Drupal settings.'); + $this->assertRaw('basePath', 'Default behavior: The front page of the site includes the basePath Drupal setting.'); + + // The default front page should not use jQuery and other standard scripts + // and settings when the 'javascript_always_use_jquery' variable is set to + // FALSE. + variable_set('javascript_always_use_jquery', FALSE); + $this->drupalGet(''); + $this->assertNoRaw('misc/jquery.js', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include jquery.js.'); + $this->assertNoRaw('misc/drupal.js', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include drupal.js.'); + $this->assertNoRaw('Drupal.settings', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include Drupal settings.'); + $this->assertNoRaw('basePath', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include the basePath Drupal setting.'); + variable_del('javascript_always_use_jquery'); + + // When only settings have been added via drupal_add_js(), drupal_get_js() + // should still return jQuery and other standard scripts and settings. + $this->resetStaticVariables(); + drupal_add_js(array('testJavaScriptSetting' => 'test'), 'setting'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'testJavaScriptSetting') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes the added Drupal settings.'); + + // When only settings have been added via drupal_add_js() and the + // 'javascript_always_use_jquery' variable is set to FALSE, drupal_get_js() + // should not return jQuery and other standard scripts and settings, nor + // should it return the requested settings (since they cannot actually be + // addded to the page without jQuery). + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js(array('testJavaScriptSetting' => 'test'), 'setting'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'testJavaScriptSetting') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include the added Drupal settings.'); + variable_del('javascript_always_use_jquery'); + + // When a regular file has been added via drupal_add_js(), drupal_get_js() + // should return jQuery and other standard scripts and settings. + $this->resetStaticVariables(); + drupal_add_js('misc/collapse.js'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the custom file.'); + + // When a regular file has been added via drupal_add_js() and the + // 'javascript_always_use_jquery' variable is set to FALSE, drupal_get_js() + // should still return jQuery and other standard scripts and settings + // (since the file is assumed to require jQuery by default). + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js('misc/collapse.js'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the custom file.'); + variable_del('javascript_always_use_jquery'); + + // When a file that does not require jQuery has been added via + // drupal_add_js(), drupal_get_js() should still return jQuery and other + // standard scripts and settings by default. + $this->resetStaticVariables(); + drupal_add_js('misc/collapse.js', array('requires_jquery' => FALSE)); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes the custom file.'); + + // When a file that does not require jQuery has been added via + // drupal_add_js() and the 'javascript_always_use_jquery' variable is set + // to FALSE, drupal_get_js() should not return jQuery and other standard + // scripts and setting, but it should still return the requested file. + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js('misc/collapse.js', array('requires_jquery' => FALSE)); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes the custom file.'); + variable_del('javascript_always_use_jquery'); + + // When 'javascript_always_use_jquery' is set to FALSE and a file that does + // not require jQuery is added, followed by one that does, drupal_get_js() + // should return jQuery and other standard scripts and settings, in + // addition to both of the requested files. + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js('misc/collapse.js', array('requires_jquery' => FALSE)); + drupal_add_js('misc/ajax.js'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes the first custom file.'); + $this->assertTrue(strpos($javascript, 'misc/ajax.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes the second custom file.'); + variable_del('javascript_always_use_jquery'); + } + + /** * Test drupal_add_js() sets preproccess to false when cache is set to false. */ function testNoCache() { @@ -1546,6 +1915,15 @@ $query_string = variable_get('css_js_query_string', '0'); $this->assertRaw(drupal_get_path('module', 'node') . '/node.js?' . $query_string, 'Query string was appended correctly to js.'); } + + /** + * Resets static variables related to adding JavaScript to a page. + */ + function resetStaticVariables() { + drupal_static_reset('drupal_add_js'); + drupal_static_reset('drupal_add_library'); + drupal_static_reset('drupal_get_library'); + } } /** @@ -1864,7 +2242,7 @@ } /** - * Tests caching of an empty render item. + * Tests caching of render items. */ function testDrupalRenderCache() { // Force a request via GET. @@ -1890,6 +2268,59 @@ drupal_render($element); $this->assertFalse(isset($element['#printed']), 'Cache hit'); + // Test that user 1 does not share the cache with other users who have the + // same roles, even when DRUPAL_CACHE_PER_ROLE is used. + $user1 = user_load(1); + $first_authenticated_user = $this->drupalCreateUser(); + $second_authenticated_user = $this->drupalCreateUser(); + $user1->roles = array_intersect_key($user1->roles, array(DRUPAL_AUTHENTICATED_RID => TRUE)); + user_save($user1); + // Load all the accounts again, to make sure we have complete account + // objects. + $user1 = user_load(1); + $first_authenticated_user = user_load($first_authenticated_user->uid); + $second_authenticated_user = user_load($second_authenticated_user->uid); + $this->assertEqual($user1->roles, $first_authenticated_user->roles, 'User 1 has the same roles as an authenticated user.'); + // Impersonate user 1 and render content that only user 1 should have + // permission to see. + $original_user = $GLOBALS['user']; + $original_session_state = drupal_save_session(); + drupal_save_session(FALSE); + $GLOBALS['user'] = $user1; + $test_element = array( + '#cache' => array( + 'keys' => array('test'), + 'granularity' => DRUPAL_CACHE_PER_ROLE, + ), + ); + $element = $test_element; + $element['#markup'] = 'content for user 1'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for user 1'); + // Verify the cache is working by rendering the same element but with + // different markup passed in; the result should be the same. + $element = $test_element; + $element['#markup'] = 'should not be used'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for user 1'); + // Verify that the first authenticated user does not see the same content + // as user 1. + $GLOBALS['user'] = $first_authenticated_user; + $element = $test_element; + $element['#markup'] = 'content for authenticated users'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for authenticated users'); + // Verify that the second authenticated user shares the cache with the + // first authenticated user. + $GLOBALS['user'] = $second_authenticated_user; + $element = $test_element; + $element['#markup'] = 'should not be used'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for authenticated users'); + // Restore the original logged-in user. + $GLOBALS['user'] = $original_user; + drupal_save_session($original_session_state); + // Restore the previous request method. $_SERVER['REQUEST_METHOD'] = $request_method; } @@ -2756,3 +3187,28 @@ $this->assertIdentical(drupal_array_diff_assoc_recursive($this->array1, $this->array2), $expected); } } + +/** + * Tests the functionality of drupal_get_query_array(). + */ +class DrupalGetQueryArrayTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Query parsing using drupal_get_query_array()', + 'description' => 'Tests that drupal_get_query_array() correctly parses query parameters.', + 'group' => 'System', + ); + } + + /** + * Tests that drupal_get_query_array() correctly explodes query parameters. + */ + public function testDrupalGetQueryArray() { + $url = "http://my.site.com/somepath?foo=/content/folder[@name='foo']/folder[@name='bar']"; + $parsed = parse_url($url); + $result = drupal_get_query_array($parsed['query']); + $this->assertEqual($result['foo'], "/content/folder[@name='foo']/folder[@name='bar']", 'drupal_get_query_array() should only explode parameters on the first equals sign.'); + } + +} diff -Naur drupal-7.23/modules/simpletest/tests/common_test.info drupal-7.66/modules/simpletest/tests/common_test.info --- drupal-7.23/modules/simpletest/tests/common_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/common_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ stylesheets[print][] = common_test.print.css hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/common_test.module drupal-7.66/modules/simpletest/tests/common_test.module --- drupal-7.23/modules/simpletest/tests/common_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/common_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -93,6 +93,18 @@ } /** + * Implements hook_init(). + */ +function common_test_init() { + if (variable_get('common_test_redirect_current_path', FALSE)) { + drupal_goto(current_path()); + } + if (variable_get('common_test_link_to_current_path', FALSE)) { + drupal_set_message(l('link which should point to the current path', current_path())); + } +} + +/** * Print destination query parameter. */ function common_test_destination() { diff -Naur drupal-7.23/modules/simpletest/tests/common_test_cron_helper.info drupal-7.66/modules/simpletest/tests/common_test_cron_helper.info --- drupal-7.23/modules/simpletest/tests/common_test_cron_helper.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/common_test_cron_helper.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/database_test.info drupal-7.66/modules/simpletest/tests/database_test.info --- drupal-7.23/modules/simpletest/tests/database_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/database_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/database_test.install drupal-7.66/modules/simpletest/tests/database_test.install --- drupal-7.23/modules/simpletest/tests/database_test.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/database_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -87,6 +87,9 @@ ), ); + $schema['test_people_copy'] = $schema['test_people']; + $schema['test_people_copy']['description'] = 'A duplicate version of the test_people table, used for additional tests.'; + $schema['test_one_blob'] = array( 'description' => 'A simple table including a BLOB field for testing BLOB behavior.', 'fields' => array( diff -Naur drupal-7.23/modules/simpletest/tests/database_test.test drupal-7.66/modules/simpletest/tests/database_test.test --- drupal-7.23/modules/simpletest/tests/database_test.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/database_test.test 2019-04-17 22:20:46.000000000 +0200 @@ -23,6 +23,7 @@ $schema['test'] = drupal_get_schema('test'); $schema['test_people'] = drupal_get_schema('test_people'); + $schema['test_people_copy'] = drupal_get_schema('test_people_copy'); $schema['test_one_blob'] = drupal_get_schema('test_one_blob'); $schema['test_two_blobs'] = drupal_get_schema('test_two_blobs'); $schema['test_task'] = drupal_get_schema('test_task'); @@ -237,7 +238,7 @@ // Open the default target so we have an object to compare. $db1 = Database::getConnection('default', 'default'); - // Try to close the the default connection, then open a new one. + // Try to close the default connection, then open a new one. Database::closeConnection('default', 'default'); $db2 = Database::getConnection('default', 'default'); @@ -603,9 +604,9 @@ } /** - * Test that the INSERT INTO ... SELECT ... syntax works. + * Test that the INSERT INTO ... SELECT (fields) ... syntax works. */ - function testInsertSelect() { + function testInsertSelectFields() { $query = db_select('test_people', 'tp'); // The query builder will always append expressions after fields. // Add the expression first to test that the insert fields are correctly @@ -627,6 +628,27 @@ $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Meredith'))->fetchField(); $this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.'); } + + /** + * Tests that the INSERT INTO ... SELECT * ... syntax works. + */ + function testInsertSelectAll() { + $query = db_select('test_people', 'tp') + ->fields('tp') + ->condition('tp.name', 'Meredith'); + + // The resulting query should be equivalent to: + // INSERT INTO test_people_copy + // SELECT * + // FROM test_people tp + // WHERE tp.name = 'Meredith' + db_insert('test_people_copy') + ->from($query) + ->execute(); + + $saved_age = db_query('SELECT age FROM {test_people_copy} WHERE name = :name', array(':name' => 'Meredith'))->fetchField(); + $this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.'); + } } /** @@ -1392,10 +1414,47 @@ } $query = (string)$query; - $expected = "/* Testing query comments SELECT nid FROM {node}; -- */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; + $expected = "/* Testing query comments * / SELECT nid FROM {node}; -- */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); $this->assertEqual($query, $expected, 'The flattened query contains the sanitised comment string.'); + + $connection = Database::getConnection(); + foreach ($this->makeCommentsProvider() as $test_set) { + list($expected, $comments) = $test_set; + $this->assertEqual($expected, $connection->makeComment($comments)); + } + } + + /** + * Provides expected and input values for testVulnerableComment(). + */ + function makeCommentsProvider() { + return array( + array( + '/* */ ', + array(''), + ), + // Try and close the comment early. + array( + '/* Exploit * / DROP TABLE node; -- */ ', + array('Exploit */ DROP TABLE node; --'), + ), + // Variations on comment closing. + array( + '/* Exploit * / * / DROP TABLE node; -- */ ', + array('Exploit */*/ DROP TABLE node; --'), + ), + array( + '/* Exploit * * // DROP TABLE node; -- */ ', + array('Exploit **// DROP TABLE node; --'), + ), + // Try closing the comment in the second string which is appended. + array( + '/* Exploit * / DROP TABLE node; --; Another try * / DROP TABLE node; -- */ ', + array('Exploit */ DROP TABLE node; --', 'Another try */ DROP TABLE node; --'), + ), + ); } /** @@ -1925,6 +1984,15 @@ $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); } + + /** + * Tests that the sort direction is sanitized properly. + */ + function testOrderByEscaping() { + $query = db_select('test')->orderBy('name', 'invalid direction'); + $order_bys = $query->getOrderBy(); + $this->assertEqual($order_bys['name'], 'ASC', 'Invalid order by direction is converted to ASC.'); + } } /** @@ -2600,6 +2668,52 @@ } /** + * Confirm that an extended query has a "tag" added to it. + */ + function testExtenderHasTag() { + $query = db_select('test') + ->extend('SelectQueryExtender'); + $query->addField('test', 'name'); + $query->addField('test', 'age', 'age'); + + $query->addTag('test'); + + $this->assertTrue($query->hasTag('test'), 'hasTag() returned true.'); + $this->assertFalse($query->hasTag('other'), 'hasTag() returned false.'); + } + + /** + * Test extended query tagging "has all of these tags" functionality. + */ + function testExtenderHasAllTags() { + $query = db_select('test') + ->extend('SelectQueryExtender'); + $query->addField('test', 'name'); + $query->addField('test', 'age', 'age'); + + $query->addTag('test'); + $query->addTag('other'); + + $this->assertTrue($query->hasAllTags('test', 'other'), 'hasAllTags() returned true.'); + $this->assertFalse($query->hasAllTags('test', 'stuff'), 'hasAllTags() returned false.'); + } + + /** + * Test extended query tagging "has at least one of these tags" functionality. + */ + function testExtenderHasAnyTag() { + $query = db_select('test') + ->extend('SelectQueryExtender'); + $query->addField('test', 'name'); + $query->addField('test', 'age', 'age'); + + $query->addTag('test'); + + $this->assertTrue($query->hasAnyTag('test', 'other'), 'hasAnyTag() returned true.'); + $this->assertFalse($query->hasAnyTag('other', 'stuff'), 'hasAnyTag() returned false.'); + } + + /** * Test that we can attach meta data to a query object. * * This is how we pass additional context to alter hooks. @@ -3069,6 +3183,15 @@ $this->assertEqual($this->countTableRows($table_name_system), $this->countTableRows("system"), 'A temporary table was created successfully in this request.'); $this->assertEqual($this->countTableRows($table_name_users), $this->countTableRows("users"), 'A second temporary table was created successfully in this request.'); + + // Check that leading whitespace and comments do not cause problems + // in the modified query. + $sql = " + -- Let's select some rows into a temporary table + SELECT name FROM {test} + "; + $table_name_test = db_query_temporary($sql, array()); + $this->assertEqual($this->countTableRows($table_name_test), $this->countTableRows('test'), 'Leading white space and comments do not interfere with temporary table creation.'); } } @@ -3307,6 +3430,34 @@ $this->assertEqual(count($names), 3, 'Correct number of names returned'); } + + /** + * Test SQL injection via database query array arguments. + */ + public function testArrayArgumentsSQLInjection() { + // Attempt SQL injection and verify that it does not work. + $condition = array( + "1 ;INSERT INTO {test} (name) VALUES ('test12345678'); -- " => '', + '1' => '', + ); + try { + db_query("SELECT * FROM {test} WHERE name = :name", array(':name' => $condition))->fetchObject(); + $this->fail('SQL injection attempt via array arguments should result in a PDOException.'); + } + catch (PDOException $e) { + $this->pass('SQL injection attempt via array arguments should result in a PDOException.'); + } + + // Test that the insert query that was used in the SQL injection attempt did + // not result in a row being inserted in the database. + $result = db_select('test') + ->condition('name', 'test12345678') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertFalse($result, 'SQL injection attempt did not result in a row being inserted in the database table.'); + } + } /** @@ -3340,12 +3491,14 @@ } /** - * Helper method for transaction unit test. This "outer layer" transaction - * starts and then encapsulates the "inner layer" transaction. This nesting - * is used to evaluate whether the the database transaction API properly - * supports nesting. By "properly supports," we mean the outer transaction - * continues to exist regardless of what functions are called and whether - * those functions start their own transactions. + * Helper method for transaction unit test. + * + * This "outer layer" transaction starts and then encapsulates the + * "inner layer" transaction. This nesting is used to evaluate whether the + * database transaction API properly supports nesting. By "properly supports," + * we mean the outer transaction continues to exist regardless of what + * functions are called and whether those functions start their own + * transactions. * * In contrast, a typical database would commit the outer transaction, start * a new transaction for the inner layer, commit the inner layer transaction, diff -Naur drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info --- drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,13 @@ +name = "Drupal code registry test" +description = "Support module for testing the code registry." +files[] = drupal_autoload_test_interface.inc +files[] = drupal_autoload_test_class.inc +package = Testing +version = VERSION +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module --- drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * Test module to check code registry. + */ + +/** + * Implements hook_registry_files_alter(). + */ +function drupal_autoload_test_registry_files_alter(&$files, $modules) { + foreach ($modules as $module) { + // Add the drupal_autoload_test_trait.sh file to the registry when PHP 5.4+ + // is being used. + if ($module->name == 'drupal_autoload_test' && version_compare(PHP_VERSION, '5.4') >= 0) { + $files["$module->dir/drupal_autoload_test_trait.sh"] = array( + 'module' => $module->name, + 'weight' => $module->weight, + ); + } + } +} diff -Naur drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc --- drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,11 @@ +<?php + +/** + * @file + * Test classes for code registry testing. + */ + +/** + * This class is empty because we only care if Drupal can find it. + */ +class DrupalAutoloadTestClass implements DrupalAutoloadTestInterface {} diff -Naur drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_interface.inc drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_interface.inc --- drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_interface.inc 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_interface.inc 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,11 @@ +<?php + +/** + * @file + * Test interfaces for code registry testing. + */ + +/** + * This interface is empty because we only care if Drupal can find it. + */ +interface DrupalAutoloadTestInterface {} diff -Naur drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_trait.sh drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_trait.sh --- drupal-7.23/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_trait.sh 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_trait.sh 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,16 @@ +<?php + +/** + * @file + * Test traits for code registry testing. + * + * This file has a non-standard extension to prevent PHP < 5.4 testbots from + * trying to run a syntax check on it. + * @todo Use a standard extension once the testbots allow it. See + * https://www.drupal.org/node/2589649. + */ + +/** + * This trait is empty because we only care if Drupal can find it. + */ +trait DrupalAutoloadTestTrait {} diff -Naur drupal-7.23/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info drupal-7.66/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info --- drupal-7.23/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info drupal-7.66/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info --- drupal-7.23/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/entity_cache_test.info drupal-7.66/modules/simpletest/tests/entity_cache_test.info --- drupal-7.23/modules/simpletest/tests/entity_cache_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/entity_cache_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ dependencies[] = entity_cache_test_dependency hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/entity_cache_test_dependency.info drupal-7.66/modules/simpletest/tests/entity_cache_test_dependency.info --- drupal-7.23/modules/simpletest/tests/entity_cache_test_dependency.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/entity_cache_test_dependency.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/entity_crud.test drupal-7.66/modules/simpletest/tests/entity_crud.test --- drupal-7.23/modules/simpletest/tests/entity_crud.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_crud.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,49 @@ +<?php + +/** + * @file + * Tests for the Entity CRUD API. + */ + +/** + * Tests the entity_load() function. + */ +class EntityLoadTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Entity loading', + 'description' => 'Tests the entity_load() function.', + 'group' => 'Entity API', + ); + } + + /** + * Tests the functionality for loading entities matching certain conditions. + */ + public function testEntityLoadConditions() { + // Create a few nodes. One of them is given an edge-case title of "Array", + // because loading entities by an array of conditions is subject to PHP + // array-to-string conversion issues and we want to test those. + $node_1 = $this->drupalCreateNode(array('title' => 'Array')); + $node_2 = $this->drupalCreateNode(array('title' => 'Node 2')); + $node_3 = $this->drupalCreateNode(array('title' => 'Node 3')); + + // Load all entities so that they are statically cached. + $all_nodes = entity_load('node', FALSE); + + // Check that the first node can be loaded by title. + $nodes_loaded = entity_load('node', FALSE, array('title' => 'Array')); + $this->assertEqual(array_keys($nodes_loaded), array($node_1->nid)); + + // Check that the second and third nodes can be loaded by title using an + // array of conditions, and that the first node is not loaded from the + // cache along with them. + $nodes_loaded = entity_load('node', FALSE, array('title' => array('Node 2', 'Node 3'))); + ksort($nodes_loaded); + $this->assertEqual(array_keys($nodes_loaded), array($node_2->nid, $node_3->nid)); + $this->assertIdentical($nodes_loaded[$node_2->nid], $all_nodes[$node_2->nid], 'Loaded node 2 is identical to cached node.'); + $this->assertIdentical($nodes_loaded[$node_3->nid], $all_nodes[$node_3->nid], 'Loaded node 3 is identical to cached node.'); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/entity_crud_hook_test.info drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.info --- drupal-7.23/modules/simpletest/tests/entity_crud_hook_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/entity_query_access_test.info drupal-7.66/modules/simpletest/tests/entity_query_access_test.info --- drupal-7.23/modules/simpletest/tests/entity_query_access_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/entity_query_access_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/error_test.info drupal-7.66/modules/simpletest/tests/error_test.info --- drupal-7.23/modules/simpletest/tests/error_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/error_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/file.test drupal-7.66/modules/simpletest/tests/file.test --- drupal-7.23/modules/simpletest/tests/file.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/file.test 2019-04-17 22:20:46.000000000 +0200 @@ -480,21 +480,6 @@ * Test file_validate_size(). */ function testFileValidateSize() { - global $user; - $original_user = $user; - drupal_save_session(FALSE); - - // Run these test as uid = 1. - $user = user_load(1); - - $file = new stdClass(); - $file->filesize = 999999; - $errors = file_validate_size($file, 1, 1); - $this->assertEqual(count($errors), 0, 'No size limits enforced on uid=1.', 'File'); - - // Run these tests as a regular user. - $user = $this->drupalCreateUser(); - // Create a file with a size of 1000 bytes, and quotas of only 1 byte. $file = new stdClass(); $file->filesize = 1000; @@ -506,9 +491,6 @@ $this->assertEqual(count($errors), 1, 'Error for the user being over their limit.', 'File'); $errors = file_validate_size($file, 1, 1); $this->assertEqual(count($errors), 2, 'Errors for both the file and their limit.', 'File'); - - $user = $original_user; - drupal_save_session(TRUE); } } @@ -952,7 +934,7 @@ $this->assertTrue(is_file(file_default_scheme() . '://.htaccess'), 'Successfully re-created the .htaccess file in the files directory.', 'File'); // Verify contents of .htaccess file. $file = file_get_contents(file_default_scheme() . '://.htaccess'); - $this->assertEqual($file, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks", 'The .htaccess file contains the proper content.', 'File'); + $this->assertEqual($file, file_htaccess_lines(FALSE), 'The .htaccess file contains the proper content.', 'File'); } /** @@ -975,6 +957,15 @@ $path = file_create_filename($basename, $directory); $this->assertEqual($path, $expected, format_string('Creating a new filepath from %original equals %new.', array('%new' => $path, '%original' => $original)), 'File'); + try { + $filename = "a\xFFtest\x80€.txt"; + file_create_filename($filename, $directory); + $this->fail('Expected exception not thrown'); + } + catch (RuntimeException $e) { + $this->assertEqual("Invalid filename '$filename'", $e->getMessage()); + } + // @TODO: Finally we copy a file into a directory several times, to ensure a properly iterating filename suffix. } @@ -1007,6 +998,14 @@ $this->assertNotEqual($path, $destination, 'A new filepath destination is created when filepath destination already exists with FILE_EXISTS_RENAME.', 'File'); $path = file_destination($destination, FILE_EXISTS_ERROR); $this->assertEqual($path, FALSE, 'An error is returned when filepath destination already exists with FILE_EXISTS_ERROR.', 'File'); + + try { + file_destination("core/misc/a\xFFtest\x80€.txt", FILE_EXISTS_REPLACE); + $this->fail('Expected exception not thrown'); + } + catch (RuntimeException $e) { + $this->assertEqual("Invalid filename 'a\xFFtest\x80€.txt'", $e->getMessage()); + } } /** @@ -2388,7 +2387,7 @@ $this->assertEqual($headers['x-foo'], 'Bar', 'Found header set by file_test module on private download.'); $this->assertResponse(200, 'Correctly allowed access to a file when file_test provides headers.'); - // Test that the file transfered correctly. + // Test that the file transferred correctly. $this->assertEqual($contents, $this->content, 'Contents of the file are correct.'); // Deny access to all downloads via a -1 header. @@ -2564,6 +2563,7 @@ parent::setUp(); $this->bad_extension = 'php'; $this->name = $this->randomName() . '.' . $this->bad_extension . '.txt'; + $this->name_with_uc_ext = $this->randomName() . '.' . strtoupper($this->bad_extension) . '.txt'; } /** @@ -2601,9 +2601,13 @@ * White listed extensions are ignored by file_munge_filename(). */ function testMungeIgnoreWhitelisted() { - // Declare our extension as whitelisted. - $munged_name = file_munge_filename($this->name, $this->bad_extension); - $this->assertIdentical($munged_name, $this->name, format_string('The new filename (%munged) matches the original (%original) once the extension has been whitelisted.', array('%munged' => $munged_name, '%original' => $this->name))); + // Declare our extension as whitelisted. The declared extensions should + // be case insensitive so test using one with a different case. + $munged_name = file_munge_filename($this->name_with_uc_ext, $this->bad_extension); + $this->assertIdentical($munged_name, $this->name_with_uc_ext, format_string('The new filename (%munged) matches the original (%original) once the extension has been whitelisted.', array('%munged' => $munged_name, '%original' => $this->name_with_uc_ext))); + // The allowed extensions should also be normalized. + $munged_name = file_munge_filename($this->name, strtoupper($this->bad_extension)); + $this->assertIdentical($munged_name, $this->name, format_string('The new filename (%munged) matches the original (%original) also when the whitelisted extension is in uppercase.', array('%munged' => $munged_name, '%original' => $this->name))); } /** @@ -2779,4 +2783,64 @@ $this->assertTrue(file_stream_wrapper_valid_scheme(file_uri_scheme('public://asdf')), 'Got a valid stream scheme from public://asdf'); $this->assertFalse(file_stream_wrapper_valid_scheme(file_uri_scheme('foo://asdf')), 'Did not get a valid stream scheme from foo://asdf'); } + + /** + * Tests that phar stream wrapper is registered as expected. + * + * @see file_get_stream_wrappers() + */ + public function testPharStreamWrapperRegistration() { + if (!class_exists('Phar', FALSE)) { + $this->assertFalse(in_array('phar', stream_get_wrappers(), TRUE), 'PHP is compiled without phar support. Therefore, no phar stream wrapper is registered.'); + } + elseif (version_compare(PHP_VERSION, '5.3.3', '<')) { + $this->assertFalse(in_array('phar', stream_get_wrappers(), TRUE), 'The PHP version is <5.3.3. The built-in phar stream wrapper has been unregistered and not replaced.'); + } + else { + $this->assertTrue(in_array('phar', stream_get_wrappers(), TRUE), 'A phar stream wrapper is registered.'); + $this->assertFalse(file_stream_wrapper_valid_scheme('phar'), 'The phar scheme is not a valid scheme for Drupal File API usage.'); + } + + // Ensure that calling file_get_stream_wrappers() multiple times, both + // without and with a drupal_static_reset() in between, does not create + // errors due to the PharStreamWrapperManager singleton. + file_get_stream_wrappers(); + file_get_stream_wrappers(); + drupal_static_reset('file_get_stream_wrappers'); + file_get_stream_wrappers(); + } + + /** + * Tests that only valid phar files can be used. + */ + public function testPharFile() { + if (!in_array('phar', stream_get_wrappers(), TRUE)) { + $this->pass('There is no phar stream wrapper registered.'); + // Nothing else in this test is relevant when there's no phar stream + // wrapper. testPharStreamWrapperRegistration() is sufficient for testing + // the conditions of when the stream wrapper should or should not be + // registered. + return; + } + + $base = dirname(dirname(__FILE__)) . '/files'; + + // Ensure that file operations via the phar:// stream wrapper work for phar + // files with the .phar extension. + $this->assertFalse(file_exists("phar://$base/phar-1.phar/no-such-file.php")); + $this->assertTrue(file_exists("phar://$base/phar-1.phar/index.php")); + $file_contents = file_get_contents("phar://$base/phar-1.phar/index.php"); + $expected_hash = 'c7e7904ea573c5ebea3ef00bb08c1f86af1a45961fbfbeb1892ff4a98fd73ad5'; + $this->assertIdentical($expected_hash, hash('sha256', $file_contents)); + + // Ensure that file operations via the phar:// stream wrapper throw an + // exception for files without the .phar extension. + try { + file_exists("phar://$base/image-2.jpg/index.php"); + $this->fail('Expected exception failed to be thrown when accessing an invalid phar file.'); + } + catch (Exception $e) { + $this->assertEqual(get_class($e), 'TYPO3\PharStreamWrapper\Exception', 'Expected exception thrown when accessing an invalid phar file.'); + } + } } diff -Naur drupal-7.23/modules/simpletest/tests/file_test.info drupal-7.66/modules/simpletest/tests/file_test.info --- drupal-7.23/modules/simpletest/tests/file_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/file_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = file_test.module hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/filter_test.info drupal-7.66/modules/simpletest/tests/filter_test.info --- drupal-7.23/modules/simpletest/tests/filter_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/filter_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/form.test drupal-7.66/modules/simpletest/tests/form.test --- drupal-7.23/modules/simpletest/tests/form.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/form.test 2019-04-17 22:20:46.000000000 +0200 @@ -82,6 +82,10 @@ $form_state['input'][$element] = $empty; $form_state['input']['form_id'] = $form_id; $form_state['method'] = 'post'; + + // The form token CSRF protection should not interfere with this test, + // so we bypass it by marking this test form as programmed. + $form_state['programmed'] = TRUE; drupal_prepare_form($form_id, $form, $form_state); drupal_process_form($form_id, $form, $form_state); $errors = form_get_errors(); @@ -466,6 +470,64 @@ $this->drupalPost(NULL, array('checkboxes[one]' => TRUE, 'checkboxes[two]' => TRUE), t('Submit')); $this->assertText('An illegal choice has been detected.', 'Input forgery was detected.'); } + + /** + * Tests that submitted values are converted to scalar strings for textfields. + */ + public function testTextfieldStringValue() { + // Check multivalued submissions. + $multivalue = array('evil' => 'multivalue', 'not so' => 'good'); + $this->checkFormValue('textfield', $multivalue, ''); + $this->checkFormValue('password', $multivalue, ''); + $this->checkFormValue('textarea', $multivalue, ''); + $this->checkFormValue('machine_name', $multivalue, ''); + $this->checkFormValue('password_confirm', $multivalue, array('pass1' => '', 'pass2' => '')); + // Check integer submissions. + $integer = 5; + $string = '5'; + $this->checkFormValue('textfield', $integer, $string); + $this->checkFormValue('password', $integer, $string); + $this->checkFormValue('textarea', $integer, $string); + $this->checkFormValue('machine_name', $integer, $string); + $this->checkFormValue('password_confirm', array('pass1' => $integer, 'pass2' => $integer), array('pass1' => $string, 'pass2' => $string)); + // Check that invalid array keys are ignored for password confirm elements. + $this->checkFormValue('password_confirm', array('pass1' => 'test', 'pass2' => 'test', 'extra' => 'invalid'), array('pass1' => 'test', 'pass2' => 'test')); + } + + /** + * Checks that a given form input value is sanitized to the expected result. + * + * @param string $element_type + * The form element type. Example: textfield. + * @param mixed $input_value + * The submitted user input value for the form element. + * @param mixed $expected_value + * The sanitized result value in the form state after calling + * form_builder(). + */ + protected function checkFormValue($element_type, $input_value, $expected_value) { + $form_id = $this->randomName(); + $form = array(); + $form_state = form_state_defaults(); + $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); + $form[$element_type] = array( + '#type' => $element_type, + '#title' => 'test', + ); + + $form_state['input'][$element_type] = $input_value; + $form_state['input']['form_id'] = $form_id; + $form_state['method'] = 'post'; + $form_state['values'] = array(); + drupal_prepare_form($form_id, $form, $form_state); + + // This is the main function we want to test: it is responsible for + // populating user supplied $form_state['input'] to sanitized + // $form_state['values']. + form_builder($form_id, $form, $form_state); + + $this->assertIdentical($form_state['values'][$element_type], $expected_value, format_string('Form submission for the "@element_type" element type has been correctly sanitized.', array('@element_type' => $element_type))); + } } /** @@ -614,6 +676,26 @@ $this->drupalPost(NULL, array(), 'Save'); $this->assertNoFieldByName('name', 'Form element was hidden.'); $this->assertText('Name value: element_validate_access', 'Value for inaccessible form element exists.'); + + // Verify that #validate handlers don't run if the CSRF token is invalid. + $this->drupalLogin($this->drupalCreateUser()); + $this->drupalGet('form-test/validate'); + $edit = array( + 'name' => 'validate', + 'form_token' => 'invalid token' + ); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertNoFieldByName('name', '#value changed by #validate', 'Form element #value was not altered.'); + $this->assertNoText('Name value: value changed by form_set_value() in #validate', 'Form element value in $form_state was not altered.'); + $this->assertText('The form has become outdated. Copy any unsaved work in the form below'); + } + + /** + * Tests that a form with a disabled CSRF token can be validated. + */ + function testDisabledToken() { + $this->drupalPost('form-test/validate-no-token', array(), 'Save'); + $this->assertText('The form_test_validate_no_token form has been submitted successfully.'); } /** @@ -920,6 +1002,26 @@ $this->assertTrue(isset($errors['tableselect']), 'Option checker disallows invalid values for radio buttons.'); } + /** + * Test presence of ajax functionality + */ + function testAjax() { + $rows = array('row1', 'row2', 'row3'); + // Test checkboxes (#multiple == TRUE). + foreach ($rows as $row) { + $element = 'tableselect[' . $row . ']'; + $edit = array($element => TRUE); + $result = $this->drupalPostAJAX('form_test/tableselect/multiple-true', $edit, $element); + $this->assertFalse(empty($result), t('Ajax triggers on checkbox for @row.', array('@row' => $row))); + } + // Test radios (#multiple == FALSE). + $element = 'tableselect'; + foreach ($rows as $row) { + $edit = array($element => $row); + $result = $this->drupalPostAjax('form_test/tableselect/multiple-false', $edit, $element); + $this->assertFalse(empty($result), t('Ajax triggers on radio for @row.', array('@row' => $row))); + } + } /** * Helper function for the option check test to submit a form while collecting errors. @@ -941,6 +1043,10 @@ $form_state['input'] = $edit; $form_state['input']['form_id'] = $form_id; + // The form token CSRF protection should not interfere with this test, + // so we bypass it by marking this test form as programmed. + $form_state['programmed'] = TRUE; + drupal_prepare_form($form_id, $form, $form_state); drupal_process_form($form_id, $form, $form_state); @@ -1136,6 +1242,235 @@ $this->assertText('State persisted.'); } } + + /** + * Verify that the form build-id remains the same when validation errors + * occur on a mutable form. + */ + function testMutableForm() { + // Request the form with 'cache' query parameter to enable form caching. + $this->drupalGet('form_test/form-storage', array('query' => array('cache' => 1))); + $buildIdFields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page'); + $buildId = (string) $buildIdFields[0]['value']; + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Continue submit'); + + // Verify that the build-id did not change. + $this->assertFieldByName('form_build_id', $buildId, 'Build id remains the same when form validation fails'); + } + + /** + * Verifies that form build-id is regenerated when loading an immutable form + * from the cache. + */ + function testImmutableForm() { + // Request the form with 'cache' query parameter to enable form caching. + $this->drupalGet('form_test/form-storage', array('query' => array('cache' => 1, 'immutable' => 1))); + $buildIdFields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page'); + $buildId = (string) $buildIdFields[0]['value']; + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Continue submit'); + + // Verify that the build-id did change. + $this->assertNoFieldByName('form_build_id', $buildId, 'Build id changes when form validation fails'); + + // Retrieve the new build-id. + $buildIdFields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page'); + $buildId = (string) $buildIdFields[0]['value']; + + // Trigger validation error by again submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Continue submit'); + + // Verify that the build-id does not change the second time. + $this->assertFieldByName('form_build_id', $buildId, 'Build id remains the same when form validation fails subsequently'); + } + + /** + * Verify that existing contrib code cannot overwrite immutable form state. + */ + public function testImmutableFormLegacyProtection() { + $this->drupalGet('form_test/form-storage', array('query' => array('cache' => 1, 'immutable' => 1))); + $build_id_fields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page'); + $build_id = (string) $build_id_fields[0]['value']; + + // Try to poison the form cache. + $original = $this->drupalGetAJAX('form_test/form-storage-legacy/' . $build_id); + $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded'); + $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated'); + + // Assert that a watchdog message was logged by form_set_cache. + $status = (bool) db_query_range('SELECT 1 FROM {watchdog} WHERE message = :message', 0, 1, array(':message' => 'Form build-id mismatch detected while attempting to store a form in the cache.')); + $this->assert($status, 'A watchdog message was logged by form_set_cache'); + + // Ensure that the form state was not poisoned by the preceeding call. + $original = $this->drupalGetAJAX('form_test/form-storage-legacy/' . $build_id); + $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded'); + $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated'); + $this->assert(empty($original['form']['#poisoned']), 'Original form structure was preserved'); + $this->assert(empty($original['form_state']['poisoned']), 'Original form state was preserved'); + } +} + +/** + * Test the form storage when page caching for anonymous users is turned on. + */ +class FormsFormStoragePageCacheTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Forms using form storage on cached pages', + 'description' => 'Tests a form using form storage and makes sure validation and caching works when page caching for anonymous users is turned on.', + 'group' => 'Form API', + ); + } + + public function setUp() { + parent::setUp('form_test'); + + variable_set('cache', TRUE); + } + + /** + * Return the build id of the current form. + */ + protected function getFormBuildId() { + $build_id_fields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page'); + return (string) $build_id_fields[0]['value']; + } + + /** + * Build-id is regenerated when validating cached form. + */ + public function testValidateFormStorageOnCachedPage() { + $this->drupalGet('form_test/form-storage-page-cache'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_initial = $this->getFormBuildId(); + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText($build_id_initial, 'Old build id on the page'); + $build_id_first_validation = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_first_validation, 'Build id changes when form validation fails'); + + // Trigger validation error by again submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_second_validation = $this->getFormBuildId(); + $this->assertEqual($build_id_first_validation, $build_id_second_validation, 'Build id remains the same when form validation fails subsequently'); + + // Repeat the test sequence but this time with a page loaded from the cache. + $this->drupalGet('form_test/form-storage-page-cache'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_from_cache_initial = $this->getFormBuildId(); + $this->assertEqual($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request'); + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText($build_id_initial, 'Old build id is initial build id'); + $build_id_from_cache_first_validation = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_from_cache_first_validation, 'Build id changes when form validation fails'); + $this->assertNotEqual($build_id_first_validation, $build_id_from_cache_first_validation, 'Build id from first user is not reused'); + + // Trigger validation error by again submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_from_cache_second_validation = $this->getFormBuildId(); + $this->assertEqual($build_id_from_cache_first_validation, $build_id_from_cache_second_validation, 'Build id remains the same when form validation fails subsequently'); + } + + /** + * Build-id is regenerated when rebuilding cached form. + */ + public function testRebuildFormStorageOnCachedPage() { + $this->drupalGet('form_test/form-storage-page-cache'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_initial = $this->getFormBuildId(); + + // Trigger rebuild, should regenerate build id. + $edit = array('title' => 'something'); + $this->drupalPost(NULL, $edit, 'Rebuild'); + $this->assertText($build_id_initial, 'Initial build id as old build id on the page'); + $build_id_first_rebuild = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_first_rebuild, 'Build id changes on first rebuild.'); + + // Trigger subsequent rebuild, should regenerate the build id again. + $edit = array('title' => 'something'); + $this->drupalPost(NULL, $edit, 'Rebuild'); + $this->assertText($build_id_first_rebuild, 'First build id as old build id on the page'); + $build_id_second_rebuild = $this->getFormBuildId(); + $this->assertNotEqual($build_id_first_rebuild, $build_id_second_rebuild, 'Build id changes on second rebuild.'); + } +} + +/** + * Test cache_form. + */ +class FormsFormCacheTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Form caching', + 'description' => 'Tests storage and retrieval of forms from cache.', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Tests storing and retrieving the form from cache. + */ + function testCacheForm() { + $form = drupal_get_form('form_test_cache_form'); + $form_state = array('foo' => 'bar', 'build_info' => array('baz')); + form_set_cache($form['#build_id'], $form, $form_state); + + $cached_form_state = array(); + $cached_form = form_get_cache($form['#build_id'], $cached_form_state); + + $this->assertEqual($cached_form['#build_id'], $form['#build_id'], 'Form retrieved from cache_form successfully.'); + $this->assertEqual($cached_form_state['foo'], 'bar', 'Data retrieved from cache_form successfully.'); + } + + /** + * Tests changing form_cache_expiration. + */ + function testCacheFormCustomExpiration() { + variable_set('form_cache_expiration', -1 * (24 * 60 * 60)); + + $form = drupal_get_form('form_test_cache_form'); + $form_state = array('foo' => 'bar', 'build_info' => array('baz')); + form_set_cache($form['#build_id'], $form, $form_state); + + // Clear expired entries from cache_form, which should include the entry we + // just stored. Without this, the form will still be retrieved from cache. + cache_clear_all(NULL, 'cache_form'); + + $cached_form_state = array(); + $cached_form = form_get_cache($form['#build_id'], $cached_form_state); + + $this->assertNull($cached_form, 'Expired form was not returned from cache.'); + $this->assertTrue(empty($cached_form_state), 'No data retrieved from cache for expired form.'); + } } /** @@ -1466,6 +1801,16 @@ $this->submitForm(array('textfield' => 'dummy value', 'checkboxes' => array(1 => NULL, 2 => 2)), TRUE); $this->submitForm(array('textfield' => 'dummy value', 'checkboxes' => array(1 => NULL, 2 => NULL)), TRUE); + // Test that a programmatic form submission can successfully submit values + // even for fields where the #access property is FALSE. + $this->submitForm(array('textfield' => 'dummy value', 'textfield_no_access' => 'test value'), TRUE); + // Test that #access is respected for programmatic form submissions when + // requested to do so. + $submitted_values = array('textfield' => 'dummy value', 'textfield_no_access' => 'test value'); + $expected_values = array('textfield' => 'dummy value', 'textfield_no_access' => 'default value'); + $form_state = array('programmed_bypass_access_check' => FALSE); + $this->submitForm($submitted_values, TRUE, $expected_values, $form_state); + // Test that a programmatic form submission can correctly click a button // that limits validation errors based on user input. Since we do not // submit any values for "textfield" here and the textfield is required, we @@ -1488,10 +1833,18 @@ * @param $valid_input * A boolean indicating whether or not the form submission is expected to * be valid. + * @param $expected_values + * (Optional) An array of field values that are expected to be stored by + * the form submit handler. If not set, the submitted $values are assumed + * to also be the expected stored values. + * @param $form_state + * (Optional) A keyed array containing the state of the form, to be sent in + * the call to drupal_form_submit(). The $values parameter is added to + * $form_state['values'] by default, if it is not already set. */ - private function submitForm($values, $valid_input) { + private function submitForm($values, $valid_input, $expected_values = NULL, $form_state = array()) { // Programmatically submit the given values. - $form_state = array('values' => $values); + $form_state += array('values' => $values); drupal_form_submit('form_test_programmatic_form', $form_state); // Check that the form returns an error when expected, and vice versa. @@ -1508,7 +1861,10 @@ // By fetching the values from $form_state['storage'] we ensure that the // submission handler was properly executed. $stored_values = $form_state['storage']['programmatic_form_submit']; - foreach ($values as $key => $value) { + if (!isset($expected_values)) { + $expected_values = $values; + } + foreach ($expected_values as $key => $value) { $this->assertTrue(isset($stored_values[$key]) && $stored_values[$key] == $value, format_string('Submission handler correctly executed: %stored_key is %stored_value', array('%stored_key' => $key, '%stored_value' => print_r($value, TRUE)))); } } @@ -1824,3 +2180,36 @@ $this->assertNoDuplicateIds('There are no duplicate IDs'); } } + +/** + * Tests for form textarea. + */ +class FormTextareaTestCase extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Form textarea', + 'description' => 'Tests form textarea related functions.', + 'group' => 'Form API', + ); + } + + /** + * Tests that textarea value is properly set. + */ + public function testValueCallback() { + $element = array(); + $form_state = array(); + $test_cases = array( + array(NULL, FALSE), + array(NULL, NULL), + array('', array('test')), + array('test', 'test'), + array('123', 123), + ); + foreach ($test_cases as $test_case) { + list($expected, $input) = $test_case; + $this->assertIdentical($expected, form_type_textarea_value($element, $input, $form_state)); + } + } +} diff -Naur drupal-7.23/modules/simpletest/tests/form_test.info drupal-7.66/modules/simpletest/tests/form_test.info --- drupal-7.23/modules/simpletest/tests/form_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/form_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/form_test.module drupal-7.66/modules/simpletest/tests/form_test.module --- drupal-7.23/modules/simpletest/tests/form_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/form_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -37,6 +37,13 @@ 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['form-test/validate-no-token'] = array( + 'title' => 'Form validation without a CSRF token', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_validate_no_token'), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); $items['form-test/limit-validation-errors'] = array( 'title' => 'Form validation with some error suppression', 'page callback' => 'drupal_get_form', @@ -90,6 +97,21 @@ 'type' => MENU_CALLBACK, ); + $items['form_test/form-storage-legacy'] = array( + 'title' => 'Emulate legacy AHAH-style ajax callback', + 'page callback' => 'form_test_storage_legacy_handler', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + + $items['form_test/form-storage-page-cache'] = array( + 'title' => 'Form storage with page cache test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_storage_page_cache_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + $items['form_test/wrapper-callback'] = array( 'title' => 'Form wrapper callback test', 'page callback' => 'form_test_wrapper_callback', @@ -440,6 +462,27 @@ } /** + * Form builder for testing submission of a form without a CSRF token. + */ +function form_test_validate_no_token($form, &$form_state) { + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + + $form['#token'] = FALSE; + + return $form; +} + +/** + * Form submission handler for form_test_validate_no_token(). + */ +function form_test_validate_no_token_submit($form, &$form_state) { + drupal_set_message('The form_test_validate_no_token form has been submitted successfully.'); +} + +/** * Builds a simple form with a button triggering partial validation. */ function form_test_limit_validation_errors_form($form, &$form_state) { @@ -574,11 +617,17 @@ $form['tableselect'] = $element_properties; $form['tableselect'] += array( + '#prefix' => '<div id="tableselect-wrapper">', + '#suffix' => '</div>', '#type' => 'tableselect', '#header' => $header, '#options' => $options, '#multiple' => FALSE, '#empty' => t('Empty text.'), + '#ajax' => array( + 'callback' => '_form_test_tableselect_ajax_callback', + 'wrapper' => 'tableselect-wrapper', + ), ); $form['submit'] = array( @@ -683,6 +732,13 @@ } /** +* Ajax callback that returns the form element. +*/ +function _form_test_tableselect_ajax_callback($form, &$form_state) { + return $form['tableselect']; +} + +/** * A multistep form for testing the form storage. * * It uses two steps for editing a virtual "thing". Any changes to it are saved @@ -746,10 +802,37 @@ $form_state['cache'] = TRUE; } + if (isset($_REQUEST['immutable'])) { + $form_state['build_info']['immutable'] = TRUE; + } + return $form; } /** + * Emulate legacy AHAH-style ajax callback. + * + * Drupal 6 AHAH callbacks used to operate directly on forms retrieved using + * form_get_cache and stored using form_set_cache after manipulation. This + * callback helps testing whether form_set_cache prevents resaving of immutable + * forms. + */ +function form_test_storage_legacy_handler($form_build_id) { + $form_state = array(); + $form = form_get_cache($form_build_id, $form_state); + + drupal_json_output(array( + 'form' => $form, + 'form_state' => $form_state, + )); + + $form['#poisoned'] = TRUE; + $form_state['poisoned'] = TRUE; + + form_set_cache($form_build_id, $form, $form_state); +} + +/** * Form element validation handler for 'value' element in form_test_storage_form(). * * Tests updating of cached form storage during validation. @@ -786,6 +869,74 @@ } /** + * A simple form for testing form storage when page caching is enabled. + */ +function form_test_storage_page_cache_form($form, &$form_state) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => 'Title', + '#required' => TRUE, + ); + + $form['test_build_id_old'] = array( + '#type' => 'item', + '#title' => 'Old build id', + '#markup' => 'No old build id', + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + + $form['rebuild'] = array( + '#type' => 'submit', + '#value' => 'Rebuild', + '#submit' => array('form_test_storage_page_cache_rebuild'), + ); + + $form['#after_build'] = array('form_test_storage_page_cache_old_build_id'); + $form_state['cache'] = TRUE; + + return $form; +} + +/** + * Form element #after_build callback: output the old form build-id. + */ +function form_test_storage_page_cache_old_build_id($form) { + if (isset($form['#build_id_old'])) { + $form['test_build_id_old']['#markup'] = check_plain($form['#build_id_old']); + } + return $form; +} + +/** + * Form submit callback: Rebuild the form and continue. + */ +function form_test_storage_page_cache_rebuild($form, &$form_state) { + $form_state['rebuild'] = TRUE; +} + +/** + * A simple form for testing form caching. + */ +function form_test_cache_form($form, &$form_state) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => 'Title', + '#required' => TRUE, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + + return $form; +} + +/** * A form for testing form labels and required marks. */ function form_label_test_form() { @@ -1548,6 +1699,15 @@ '#default_value' => array(1, 2), ); + // This is used to test that programmatic form submissions can bypass #access + // restrictions. + $form['textfield_no_access'] = array( + '#type' => 'textfield', + '#title' => 'Textfield no access', + '#default_value' => 'default value', + '#access' => FALSE, + ); + $form['field_to_validate'] = array( '#type' => 'radios', '#title' => 'Field to validate (in the case of limited validation)', diff -Naur drupal-7.23/modules/simpletest/tests/image.test drupal-7.66/modules/simpletest/tests/image.test --- drupal-7.23/modules/simpletest/tests/image.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/image.test 2019-04-17 22:20:46.000000000 +0200 @@ -207,9 +207,11 @@ protected $green = array(0, 255, 0, 0); protected $blue = array(0, 0, 255, 0); protected $yellow = array(255, 255, 0, 0); - protected $fuchsia = array(255, 0, 255, 0); // Used as background colors. - protected $transparent = array(0, 0, 0, 127); protected $white = array(255, 255, 255, 0); + protected $transparent = array(0, 0, 0, 127); + // Used as rotate background colors. + protected $fuchsia = array(255, 0, 255, 0); + protected $rotate_transparent = array(255, 255, 255, 127); protected $width = 40; protected $height = 20; @@ -261,6 +263,7 @@ */ function testManipulations() { // If GD isn't available don't bother testing this. + module_load_include('inc', 'system', 'image.gd'); if (!function_exists('image_gd_check_settings') || !image_gd_check_settings()) { $this->pass(t('Image manipulations for the GD toolkit were skipped because the GD toolkit is not available.')); return; @@ -274,6 +277,7 @@ $files = array( 'image-test.png', 'image-test.gif', + 'image-test-no-transparency.gif', 'image-test.jpg', ); @@ -331,15 +335,10 @@ ); // Systems using non-bundled GD2 don't have imagerotate. Test if available. - if (function_exists('imagerotate')) { + // @todo Remove the version check once https://www.drupal.org/node/2918570 + // is resolved. + if (function_exists('imagerotate') && (version_compare(PHP_VERSION, '7.0.26', '<') || (version_compare(PHP_VERSION, '7.1', '>=') && version_compare(PHP_VERSION, '7.1.12', '<')))) { $operations += array( - 'rotate_5' => array( - 'function' => 'rotate', - 'arguments' => array(5, 0xFF00FF), // Fuchsia background. - 'width' => 42, - 'height' => 24, - 'corners' => array_fill(0, 4, $this->fuchsia), - ), 'rotate_90' => array( 'function' => 'rotate', 'arguments' => array(90, 0xFF00FF), // Fuchsia background. @@ -347,13 +346,6 @@ 'height' => 40, 'corners' => array($this->fuchsia, $this->red, $this->green, $this->blue), ), - 'rotate_transparent_5' => array( - 'function' => 'rotate', - 'arguments' => array(5), - 'width' => 42, - 'height' => 24, - 'corners' => array_fill(0, 4, $this->transparent), - ), 'rotate_transparent_90' => array( 'function' => 'rotate', 'arguments' => array(90), @@ -362,6 +354,49 @@ 'corners' => array($this->transparent, $this->red, $this->green, $this->blue), ), ); + // As of PHP version 5.5, GD uses a different algorithm to rotate images + // than version 5.4 and below, resulting in different dimensions. + // See https://bugs.php.net/bug.php?id=65148. + // For the 40x20 test images, the dimensions resulting from rotation will + // be 1 pixel smaller in both width and height in PHP 5.5 and above. + // @todo: The PHP bug was fixed in PHP 7.0.26 and 7.1.12. Change the code + // below to reflect that in https://www.drupal.org/node/2918570. + if (version_compare(PHP_VERSION, '5.5', '>=')) { + $operations += array( + 'rotate_5' => array( + 'function' => 'rotate', + 'arguments' => array(5, 0xFF00FF), // Fuchsia background. + 'width' => 41, + 'height' => 23, + 'corners' => array_fill(0, 4, $this->fuchsia), + ), + 'rotate_transparent_5' => array( + 'function' => 'rotate', + 'arguments' => array(5), + 'width' => 41, + 'height' => 23, + 'corners' => array_fill(0, 4, $this->rotate_transparent), + ), + ); + } + else { + $operations += array( + 'rotate_5' => array( + 'function' => 'rotate', + 'arguments' => array(5, 0xFF00FF), // Fuchsia background. + 'width' => 42, + 'height' => 24, + 'corners' => array_fill(0, 4, $this->fuchsia), + ), + 'rotate_transparent_5' => array( + 'function' => 'rotate', + 'arguments' => array(5), + 'width' => 42, + 'height' => 24, + 'corners' => array_fill(0, 4, $this->rotate_transparent), + ), + ); + } } // Systems using non-bundled GD2 don't have imagefilter. Test if available. @@ -379,7 +414,7 @@ array_fill(0, 3, 76) + array(3 => 0), array_fill(0, 3, 149) + array(3 => 0), array_fill(0, 3, 29) + array(3 => 0), - array_fill(0, 3, 0) + array(3 => 127) + array_fill(0, 3, 225) + array(3 => 127) ), ), ); @@ -394,11 +429,14 @@ continue 2; } - // Transparent GIFs and the imagefilter function don't work together. - // There is a todo in image.gd.inc to correct this. + // All images should be converted to truecolor when loaded. + $image_truecolor = imageistruecolor($image->resource); + $this->assertTrue($image_truecolor, format_string('Image %file after load is a truecolor image.', array('%file' => $file))); + if ($image->info['extension'] == 'gif') { if ($op == 'desaturate') { - $values['corners'][3] = $this->white; + // Transparent GIFs and the imagefilter function don't work together. + $values['corners'][3][3] = 0; } } @@ -426,6 +464,11 @@ } // Now check each of the corners to ensure color correctness. foreach ($values['corners'] as $key => $corner) { + // The test gif that does not have transparency has yellow where the + // others have transparent. + if ($file === 'image-test-no-transparency.gif' && $corner === $this->transparent) { + $corner = $this->yellow; + } // Get the location of the corner. switch ($key) { case 0: @@ -451,7 +494,8 @@ $directory = file_default_scheme() . '://imagetests'; file_prepare_directory($directory, FILE_CREATE_DIRECTORY); - image_save($image, $directory . '/' . $op . '.' . $image->info['extension']); + $file_path = $directory . '/' . $op . '.' . $image->info['extension']; + image_save($image, $file_path); $this->assertTrue($correct_dimensions_real, format_string('Image %file after %action action has proper dimensions.', array('%file' => $file, '%action' => $op))); $this->assertTrue($correct_dimensions_object, format_string('Image %file object after %action action is reporting the proper height and width values.', array('%file' => $file, '%action' => $op))); @@ -460,8 +504,37 @@ $this->assertTrue($correct_colors, format_string('Image %file object after %action action has the correct color placement.', array('%file' => $file, '%action' => $op))); } } + + // Check that saved image reloads without raising PHP errors. + $image_reloaded = image_load($file_path); } + } + /** + * Tests loading an image whose transparent color index is out of range. + */ + function testTransparentColorOutOfRange() { + // This image was generated by taking an initial image with a palette size + // of 6 colors, and setting the transparent color index to 6 (one higher + // than the largest allowed index), as follows: + // @code + // $image = imagecreatefromgif('modules/simpletest/files/image-test.gif'); + // imagecolortransparent($image, 6); + // imagegif($image, 'modules/simpletest/files/image-test-transparent-out-of-range.gif'); + // @endcode + // This allows us to test that an image with an out-of-range color index + // can be loaded correctly. + $file = 'image-test-transparent-out-of-range.gif'; + $image = image_load(drupal_get_path('module', 'simpletest') . '/files/' . $file); + + if (!$image) { + $this->fail(format_string('Could not load image %file.', array('%file' => $file))); + } + else { + // All images should be converted to truecolor when loaded. + $image_truecolor = imageistruecolor($image->resource); + $this->assertTrue($image_truecolor, format_string('Image %file after load is a truecolor image.', array('%file' => $file))); + } } } diff -Naur drupal-7.23/modules/simpletest/tests/image_test.info drupal-7.66/modules/simpletest/tests/image_test.info --- drupal-7.23/modules/simpletest/tests/image_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/image_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/mail.test drupal-7.66/modules/simpletest/tests/mail.test --- drupal-7.23/modules/simpletest/tests/mail.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/mail.test 2019-04-17 22:20:46.000000000 +0200 @@ -268,6 +268,31 @@ } /** + * Tests that drupal_wrap_mail() removes trailing whitespace before newlines. + */ + function testDrupalHtmltoTextRemoveTrailingWhitespace() { + $text = "Hi there! \nHerp Derp"; + $mail_lines = explode("\n", drupal_wrap_mail($text)); + $this->assertNotEqual(" ", substr($mail_lines[0], -1), 'Trailing whitespace removed.'); + } + + /** + * Tests drupal_wrap_mail() retains whitespace from Usenet style signatures. + * + * RFC 3676 says, "This is a special case; an (optionally quoted or quoted and + * stuffed) line consisting of DASH DASH SP is neither fixed nor flowed." + */ + function testDrupalHtmltoTextUsenetSignature() { + $text = "Hi there!\n-- \nHerp Derp"; + $mail_lines = explode("\n", drupal_wrap_mail($text)); + $this->assertEqual("-- ", $mail_lines[1], 'Trailing whitespace not removed for dash-dash-space signatures.'); + + $text = "Hi there!\n-- \nHerp Derp"; + $mail_lines = explode("\n", drupal_wrap_mail($text)); + $this->assertEqual("--", $mail_lines[1], 'Trailing whitespace removed for incorrect dash-dash-space signatures.'); + } + + /** * Test that whitespace is collapsed. */ function testDrupalHtmltoTextCollapsesWhitespace() { @@ -416,7 +441,7 @@ * <CRLF> is 1000 characters." */ function testVeryLongLineWrap() { - $input = 'Drupal<br /><p>' . str_repeat('x', 2100) . '</><br />Drupal'; + $input = 'Drupal<br /><p>' . str_repeat('x', 2100) . '</p><br />Drupal'; $output = drupal_html_to_text($input); // This awkward construct comes from includes/mail.inc lines 8-13. $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); @@ -430,7 +455,6 @@ $maximum_line_length = max($maximum_line_length, strlen($line . $eol)); } $verbose = 'Maximum line length found was ' . $maximum_line_length . ' octets.'; - // @todo This should assert that $maximum_line_length <= 1000. - $this->pass($verbose); + $this->assertTrue($maximum_line_length <= 1000, $verbose); } } diff -Naur drupal-7.23/modules/simpletest/tests/menu.test drupal-7.66/modules/simpletest/tests/menu.test --- drupal-7.23/modules/simpletest/tests/menu.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/menu.test 2019-04-17 22:20:46.000000000 +0200 @@ -1025,8 +1025,8 @@ $output = menu_tree_output($this->tree_data); // Validate that the - in main-menu is changed into an underscore - $this->assertEqual( $output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to a dash on menu_link'); - $this->assertEqual( $output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to a dash on menu_tree wrapper'); + $this->assertEqual($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link'); + $this->assertEqual($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper'); // Looking for child items in the data $this->assertEqual( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item'); $this->assertTrue( in_array('active-trail',$output['1']['#below']['2']['#attributes']['class']) , 'Checking the active trail class'); diff -Naur drupal-7.23/modules/simpletest/tests/menu_test.info drupal-7.66/modules/simpletest/tests/menu_test.info --- drupal-7.23/modules/simpletest/tests/menu_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/menu_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/module.test drupal-7.66/modules/simpletest/tests/module.test --- drupal-7.23/modules/simpletest/tests/module.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/module.test 2019-04-17 22:20:46.000000000 +0200 @@ -302,3 +302,45 @@ $this->assertEqual(0, $count, 'Permissions were all removed.'); } } + +class ModuleImplementsAlterTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Module implements alter', + 'description' => 'Tests hook_module_implements_alter().', + 'group' => 'Module', + ); + } + + /** + * Tests hook_module_implements_alter() adding an implementation. + */ + function testModuleImplementsAlter() { + module_enable(array('module_test'), FALSE); + $this->assertTrue(module_exists('module_test'), 'Test module is enabled.'); + + // Assert that module_test.module is now included. + $this->assertTrue(function_exists('module_test_permission'), + 'The file module_test.module was successfully included.'); + + $modules = module_implements('permission'); + $this->assertTrue(in_array('module_test', $modules), 'module_test implements hook_permission.'); + + $modules = module_implements('module_implements_alter'); + $this->assertTrue(in_array('module_test', $modules), 'module_test implements hook_module_implements_alter().'); + + // Assert that module_test.implementations.inc is not included yet. + $this->assertFalse(function_exists('module_test_altered_test_hook'), + 'The file module_test.implementations.inc is not included yet.'); + + // Assert that module_test_module_implements_alter(*, 'altered_test_hook') + // has added an implementation + $this->assertTrue(in_array('module_test', module_implements('altered_test_hook')), + 'module_test implements hook_altered_test_hook().'); + + // Assert that module_test.implementations.inc was included as part of the process. + $this->assertTrue(function_exists('module_test_altered_test_hook'), + 'The file module_test.implementations.inc was included.'); + } + +} diff -Naur drupal-7.23/modules/simpletest/tests/module_test.implementations.inc drupal-7.66/modules/simpletest/tests/module_test.implementations.inc --- drupal-7.23/modules/simpletest/tests/module_test.implementations.inc 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/module_test.implementations.inc 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,10 @@ +<?php + +/** + * Implements hook_altered_test_hook() + * + * @see module_test_module_implements_alter() + */ +function module_test_altered_test_hook() { + return __FUNCTION__; +} diff -Naur drupal-7.23/modules/simpletest/tests/module_test.info drupal-7.66/modules/simpletest/tests/module_test.info --- drupal-7.23/modules/simpletest/tests/module_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/module_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/module_test.module drupal-7.66/modules/simpletest/tests/module_test.module --- drupal-7.23/modules/simpletest/tests/module_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/module_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -129,3 +129,14 @@ // can check that the modules were uninstalled in the correct sequence. variable_set('test_module_uninstall_order', $modules); } + +/** + * Implements hook_module_implements_alter() + */ +function module_test_module_implements_alter(&$implementations, $hook) { + if ($hook === 'altered_test_hook') { + // Add a hook implementation, that will be found in + // module_test.implementations.inc. + $implementations['module_test'] = 'implementations'; + } +} diff -Naur drupal-7.23/modules/simpletest/tests/password.test drupal-7.66/modules/simpletest/tests/password.test --- drupal-7.23/modules/simpletest/tests/password.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/password.test 2019-04-17 22:20:46.000000000 +0200 @@ -57,4 +57,25 @@ $this->assertFalse(user_needs_new_hash($account), 'Re-hashed password does not need a new hash.'); $this->assertTrue(user_check_password($password, $account), 'Password check succeeds with re-hashed password.'); } + + /** + * Verifies that passwords longer than 512 bytes are not hashed. + */ + public function testLongPassword() { + $password = str_repeat('x', 512); + $result = user_hash_password($password); + $this->assertFalse(empty($result), '512 byte long password is allowed.'); + $password = str_repeat('x', 513); + $result = user_hash_password($password); + $this->assertFalse($result, '513 byte long password is not allowed.'); + // Check a string of 3-byte UTF-8 characters. + $password = str_repeat('€', 170); + $result = user_hash_password($password); + $this->assertFalse(empty($result), '510 byte long password is allowed.'); + $password .= 'xx'; + $this->assertFalse(empty($result), '512 byte long password is allowed.'); + $password = str_repeat('€', 171); + $result = user_hash_password($password); + $this->assertFalse($result, '513 byte long password is not allowed.'); + } } diff -Naur drupal-7.23/modules/simpletest/tests/path_test.info drupal-7.66/modules/simpletest/tests/path_test.info --- drupal-7.23/modules/simpletest/tests/path_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/path_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/psr_0_test/psr_0_test.info drupal-7.66/modules/simpletest/tests/psr_0_test/psr_0_test.info --- drupal-7.23/modules/simpletest/tests/psr_0_test/psr_0_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/psr_0_test/psr_0_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ hidden = TRUE package = Testing -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/psr_4_test/psr_4_test.info drupal-7.66/modules/simpletest/tests/psr_4_test/psr_4_test.info --- drupal-7.23/modules/simpletest/tests/psr_4_test/psr_4_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_4_test/psr_4_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = PSR-4 Test cases +description = Test classes to be discovered by simpletest. +core = 7.x + +hidden = TRUE +package = Testing + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/psr_4_test/psr_4_test.module drupal-7.66/modules/simpletest/tests/psr_4_test/psr_4_test.module --- drupal-7.23/modules/simpletest/tests/psr_4_test/psr_4_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_4_test/psr_4_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +<?php diff -Naur drupal-7.23/modules/simpletest/tests/psr_4_test/src/Tests/ExampleTest.php drupal-7.66/modules/simpletest/tests/psr_4_test/src/Tests/ExampleTest.php --- drupal-7.23/modules/simpletest/tests/psr_4_test/src/Tests/ExampleTest.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_4_test/src/Tests/ExampleTest.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\psr_4_test\Tests; + +class ExampleTest extends \DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'PSR4 example test: PSR-4 in disabled modules.', + 'description' => 'We want to assert that this test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php drupal-7.66/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php --- drupal-7.23/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\psr_4_test\Tests\Nested; + +class NestedExampleTest extends \DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'PSR4 example test: PSR-4 in nested subfolders.', + 'description' => 'We want to assert that this PSR-4 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/requirements1_test.info drupal-7.66/modules/simpletest/tests/requirements1_test.info --- drupal-7.23/modules/simpletest/tests/requirements1_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/requirements1_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/requirements2_test.info drupal-7.66/modules/simpletest/tests/requirements2_test.info --- drupal-7.23/modules/simpletest/tests/requirements2_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/requirements2_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/session.test drupal-7.66/modules/simpletest/tests/session.test --- drupal-7.23/modules/simpletest/tests/session.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/session.test 2019-04-17 22:20:46.000000000 +0200 @@ -68,8 +68,7 @@ } /** - * Test data persistence via the session_test module callbacks. Also tests - * drupal_session_count() since session data is already generated here. + * Test data persistence via the session_test module callbacks. */ function testDataPersistence() { $user = $this->drupalCreateUser(array('access content')); @@ -479,6 +478,56 @@ } /** + * Tests that empty session IDs do not cause unrelated sessions to load. + */ + public function testEmptySessionId() { + global $is_https; + + if ($is_https) { + $secure_session_name = session_name(); + } + else { + $secure_session_name = 'S' . session_name(); + } + + // Enable mixed mode for HTTP and HTTPS. + variable_set('https', TRUE); + + $admin_user = $this->drupalCreateUser(array('access administration pages')); + $standard_user = $this->drupalCreateUser(array('access content')); + + // First log in as the admin user on HTTP. + // We cannot use $this->drupalLogin() here because we need to use the + // special http.php URLs. + $edit = array( + 'name' => $admin_user->name, + 'pass' => $admin_user->pass_raw + ); + $this->drupalGet('user'); + $form = $this->xpath('//form[@id="user-login"]'); + $form[0]['action'] = $this->httpUrl('user'); + $this->drupalPost(NULL, $edit, t('Log in')); + + $this->curlClose(); + + // Now start a session for the standard user on HTTPS. + $edit = array( + 'name' => $standard_user->name, + 'pass' => $standard_user->pass_raw + ); + $this->drupalGet('user'); + $form = $this->xpath('//form[@id="user-login"]'); + $form[0]['action'] = $this->httpsUrl('user'); + $this->drupalPost(NULL, $edit, t('Log in')); + + // Make the secure session cookie blank. + curl_setopt($this->curlHandle, CURLOPT_COOKIE, "$secure_session_name="); + $this->drupalGet($this->httpsUrl('user')); + $this->assertNoText($admin_user->name, 'User is not logged in as admin'); + $this->assertNoText($standard_user->name, "The user's own name is not displayed because the invalid session cookie has logged them out."); + } + + /** * Test that there exists a session with two specific session IDs. * * @param $sid diff -Naur drupal-7.23/modules/simpletest/tests/session_test.info drupal-7.66/modules/simpletest/tests/session_test.info --- drupal-7.23/modules/simpletest/tests/session_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/session_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_dependencies_test.info drupal-7.66/modules/simpletest/tests/system_dependencies_test.info --- drupal-7.23/modules/simpletest/tests/system_dependencies_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system_dependencies_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ hidden = TRUE dependencies[] = _missing_dependency -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info --- drupal-7.23/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ hidden = TRUE dependencies[] = system_incompatible_core_version_test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_incompatible_core_version_test.info drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_test.info --- drupal-7.23/modules/simpletest/tests/system_incompatible_core_version_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 5.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info --- drupal-7.23/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ ; system_incompatible_module_version_test declares version 1.0 dependencies[] = system_incompatible_module_version_test (>2.0) -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_incompatible_module_version_test.info drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_test.info --- drupal-7.23/modules/simpletest/tests/system_incompatible_module_version_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_project_namespace_test.info drupal-7.66/modules/simpletest/tests/system_project_namespace_test.info --- drupal-7.23/modules/simpletest/tests/system_project_namespace_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_project_namespace_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,12 @@ +name = "System project namespace test" +description = "Support module for testing project namespace dependencies." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE +dependencies[] = drupal:filter + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_project_namespace_test.module drupal-7.66/modules/simpletest/tests/system_project_namespace_test.module --- drupal-7.23/modules/simpletest/tests/system_project_namespace_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_project_namespace_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +<?php diff -Naur drupal-7.23/modules/simpletest/tests/system_test.info drupal-7.66/modules/simpletest/tests/system_test.info --- drupal-7.23/modules/simpletest/tests/system_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = system_test.module hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/system_test.install drupal-7.66/modules/simpletest/tests/system_test.install --- drupal-7.23/modules/simpletest/tests/system_test.install 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,20 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the system_test module. + */ + +/** + * Implements hook_schema(). + */ +function system_test_schema() { + // Trigger a search for a module in the filesystem when requested by + // system_test_drupal_get_filename_with_schema_rebuild(). + if (variable_get('system_test_drupal_get_filename_attempt_recursive_rebuild')) { + $module_name = variable_get('system_test_drupal_get_filename_test_module_name'); + drupal_get_filename('module', $module_name); + } + + return array(); +} diff -Naur drupal-7.23/modules/simpletest/tests/system_test.module drupal-7.66/modules/simpletest/tests/system_test.module --- drupal-7.23/modules/simpletest/tests/system_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -78,6 +78,13 @@ 'type' => MENU_CALLBACK, ); + $items['system-test/drupal-set-message'] = array( + 'title' => 'Set messages with drupal_set_message()', + 'page callback' => 'system_test_drupal_set_message', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['system-test/main-content-handling'] = array( 'title' => 'Test main content handling', 'page callback' => 'system_test_main_content_fallback', @@ -106,6 +113,34 @@ 'type' => MENU_CALLBACK, ); + $items['system-test/get-destination'] = array( + 'title' => 'Test $_GET[\'destination\']', + 'page callback' => 'system_test_get_destination', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/request-destination'] = array( + 'title' => 'Test $_REQUEST[\'destination\']', + 'page callback' => 'system_test_request_destination', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/drupal-get-filename'] = array( + 'title' => 'Test drupal_get_filename()', + 'page callback' => 'system_test_drupal_get_filename', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/drupal-get-filename-with-schema-rebuild'] = array( + 'title' => 'Test drupal_get_filename() with a schema rebuild', + 'page callback' => 'system_test_drupal_get_filename_with_schema_rebuild', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -114,8 +149,23 @@ } function system_test_basic_auth_page() { - $output = t('$_SERVER[\'PHP_AUTH_USER\'] is @username.', array('@username' => $_SERVER['PHP_AUTH_USER'])); - $output .= t('$_SERVER[\'PHP_AUTH_PW\'] is @password.', array('@password' => $_SERVER['PHP_AUTH_PW'])); + // The Authorization HTTP header is forwarded via Drupal's .htaccess file even + // for PHP CGI SAPIs. + if (isset($_SERVER['HTTP_AUTHORIZATION'])) { + $authorization_header = $_SERVER['HTTP_AUTHORIZATION']; + } + // If using CGI on Apache with mod_rewrite, the forwarded HTTP header appears + // in the redirected HTTP headers. See + // https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/ServerBag.php#L61 + elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { + $authorization_header = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } + // Resemble PHP_AUTH_USER and PHP_AUTH_PW for a Basic authentication from + // the HTTP_AUTHORIZATION header. See + // http://www.php.net/manual/features.http-auth.php + list($user, $pw) = explode(':', base64_decode(substr($authorization_header, 6))); + $output = t('Username is @username.', array('@username' => $user)); + $output .= t('Password is @password.', array('@password' => $pw)); return $output; } @@ -260,6 +310,9 @@ } } + if ($file->name == 'system_project_namespace_test') { + $info['hidden'] = FALSE; + } // Make the system_dependencies_test visible by default. if ($file->name == 'system_dependencies_test') { $info['hidden'] = FALSE; @@ -405,3 +458,114 @@ system_authorized_init('system_test_authorize_run', drupal_get_path('module', 'system_test') . '/system_test.module', array(), $page_title); drupal_goto($authorize_url); } + +/** + * Sets two messages and removes the first one before the messages are displayed. + */ +function system_test_drupal_set_message() { + // Set two messages. + drupal_set_message('First message (removed).'); + drupal_set_message('Second message (not removed).'); + + // Remove the first. + unset($_SESSION['messages']['status'][0]); + + return ''; +} + +/** + * Page callback to print out $_GET['destination'] for testing. + */ +function system_test_get_destination() { + if (isset($_GET['destination'])) { + print $_GET['destination']; + } + // No need to render the whole page, we are just interested in this bit of + // information. + exit; +} + +/** + * Page callback to print out $_REQUEST['destination'] for testing. + */ +function system_test_request_destination() { + if (isset($_REQUEST['destination'])) { + print $_REQUEST['destination']; + } + // No need to render the whole page, we are just interested in this bit of + // information. + exit; +} + +/** + * Page callback to run drupal_get_filename() on a particular module. + */ +function system_test_drupal_get_filename() { + // Prevent SimpleTest from failing as a result of the expected PHP warnings + // this function causes. Any warnings will be recorded in the database logs + // for examination by the tests. + define('SIMPLETEST_COLLECT_ERRORS', FALSE); + + $module_name = variable_get('system_test_drupal_get_filename_test_module_name'); + drupal_get_filename('module', $module_name); + + return ''; +} + +/** + * Page callback to run drupal_get_filename() and do a schema rebuild. + */ +function system_test_drupal_get_filename_with_schema_rebuild() { + // Prevent SimpleTest from failing as a result of the expected PHP warnings + // this function causes. + define('SIMPLETEST_COLLECT_ERRORS', FALSE); + + // Record the original database tables from drupal_get_schema(). + variable_set('system_test_drupal_get_filename_with_schema_rebuild_original_tables', array_keys(drupal_get_schema(NULL, TRUE))); + + // Trigger system_test_schema() and system_test_watchdog() to perform an + // attempted recursive rebuild when drupal_get_schema() is called. See + // BootstrapGetFilenameWebTestCase::testRecursiveRebuilds(). + variable_set('system_test_drupal_get_filename_attempt_recursive_rebuild', TRUE); + drupal_get_schema(NULL, TRUE); + + return ''; +} + +/** + * Implements hook_watchdog(). + */ +function system_test_watchdog($log_entry) { + // If an attempted recursive schema rebuild has been triggered by + // system_test_drupal_get_filename_with_schema_rebuild(), perform the rebuild + // in response to the missing file message triggered by system_test_schema(). + if (!variable_get('system_test_drupal_get_filename_attempt_recursive_rebuild')) { + return; + } + if ($log_entry['type'] != 'php' || $log_entry['severity'] != WATCHDOG_WARNING) { + return; + } + $module_name = variable_get('system_test_drupal_get_filename_test_module_name'); + if (!isset($log_entry['variables']['!message']) || strpos($log_entry['variables']['!message'], format_string('The following module is missing from the file system: %name', array('%name' => $module_name))) === FALSE) { + return; + } + variable_set('system_test_drupal_get_filename_with_schema_rebuild_final_tables', array_keys(drupal_get_schema())); +} + +/** + * Implements hook_module_implements_alter(). + */ +function system_test_module_implements_alter(&$implementations, $hook) { + // For BootstrapGetFilenameWebTestCase::testRecursiveRebuilds() to work + // correctly, this module's hook_schema() implementation cannot be either the + // first implementation (since that would trigger a potential recursive + // rebuild before anything is in the drupal_get_schema() cache) or the last + // implementation (since that would trigger a potential recursive rebuild + // after the cache is already complete). So put it somewhere in the middle. + if ($hook == 'schema') { + $group = $implementations['system_test']; + unset($implementations['system_test']); + $count = count($implementations); + $implementations = array_merge(array_slice($implementations, 0, $count / 2, TRUE), array('system_test' => $group), array_slice($implementations, $count / 2, NULL, TRUE)); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/taxonomy_test.info drupal-7.66/modules/simpletest/tests/taxonomy_test.info --- drupal-7.23/modules/simpletest/tests/taxonomy_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/taxonomy_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ hidden = TRUE dependencies[] = taxonomy -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/taxonomy_test.module drupal-7.66/modules/simpletest/tests/taxonomy_test.module --- drupal-7.23/modules/simpletest/tests/taxonomy_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/taxonomy_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -109,3 +109,33 @@ ->execute() ->fetchField(); } + +/** + * Implements hook_query_alter(). + */ +function taxonomy_test_query_alter(QueryAlterableInterface $query) { + $value = variable_get(__FUNCTION__); + if (isset($value)) { + variable_set(__FUNCTION__, ++$value); + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function taxonomy_test_query_term_access_alter(QueryAlterableInterface $query) { + $value = variable_get(__FUNCTION__); + if (isset($value)) { + variable_set(__FUNCTION__, ++$value); + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function taxonomy_test_query_taxonomy_term_access_alter(QueryAlterableInterface $query) { + $value = variable_get(__FUNCTION__); + if (isset($value)) { + variable_set(__FUNCTION__, ++$value); + } +} diff -Naur drupal-7.23/modules/simpletest/tests/theme.test drupal-7.66/modules/simpletest/tests/theme.test --- drupal-7.23/modules/simpletest/tests/theme.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/theme.test 2019-04-17 22:20:46.000000000 +0200 @@ -155,6 +155,15 @@ $this->assertNotEqual(theme_get_setting('subtheme_override', 'test_basetheme'), theme_get_setting('subtheme_override', 'test_subtheme'), 'Base theme\'s default settings values can be overridden by subtheme.'); $this->assertIdentical(theme_get_setting('basetheme_only', 'test_subtheme'), 'base theme value', 'Base theme\'s default settings values are inherited by subtheme.'); } + + /** + * Test the drupal_add_region_content() function. + */ + function testDrupalAddRegionContent() { + $this->drupalGet('theme-test/drupal-add-region-content'); + $this->assertText('Hello'); + $this->assertText('World'); + } } /** @@ -425,28 +434,100 @@ } /** - * Unit tests for theme_html_tag(). + * Tests the markup of core render element types passed to drupal_render(). */ -class ThemeHtmlTag extends DrupalUnitTestCase { +class RenderElementTypesTestCase extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => 'Theme HTML Tag', - 'description' => 'Tests theme_html_tag() built-in theme functions.', + 'name' => 'Render element types', + 'description' => 'Tests the markup of core render element types passed to drupal_render().', 'group' => 'Theme', ); } /** - * Test function theme_html_tag() + * Asserts that an array of elements is rendered properly. + * + * @param array $elements + * An array of associative arrays describing render elements and their + * expected markup. Each item in $elements must contain the following: + * - 'name': This human readable description will be displayed on the test + * results page. + * - 'value': This is the render element to test. + * - 'expected': This is the expected markup for the element in 'value'. + */ + function assertElements($elements) { + foreach($elements as $element) { + $this->assertIdentical(drupal_render($element['value']), $element['expected'], '"' . $element['name'] . '" input rendered correctly by drupal_render().'); + } + } + + /** + * Tests system #type 'container'. + */ + function testContainer() { + $elements = array( + // Basic container with no attributes. + array( + 'name' => "#type 'container' with no HTML attributes", + 'value' => array( + '#type' => 'container', + 'child' => array( + '#markup' => 'foo', + ), + ), + 'expected' => '<div>foo</div>', + ), + // Container with a class. + array( + 'name' => "#type 'container' with a class HTML attribute", + 'value' => array( + '#type' => 'container', + 'child' => array( + '#markup' => 'foo', + ), + '#attributes' => array( + 'class' => 'bar', + ), + ), + 'expected' => '<div class="bar">foo</div>', + ), + ); + + $this->assertElements($elements); + } + + /** + * Tests system #type 'html_tag'. */ - function testThemeHtmlTag() { - // Test auto-closure meta tag generation - $tag['element'] = array('#tag' => 'meta', '#attributes' => array('name' => 'description', 'content' => 'Drupal test')); - $this->assertEqual('<meta name="description" content="Drupal test" />'."\n", theme_html_tag($tag), 'Test auto-closure meta tag generation.'); - - // Test title tag generation - $tag['element'] = array('#tag' => 'title', '#value' => 'title test'); - $this->assertEqual('<title>title test'."\n", theme_html_tag($tag), 'Test title tag generation.'); + function testHtmlTag() { + $elements = array( + // Test auto-closure meta tag generation. + array( + 'name' => "#type 'html_tag' auto-closure meta tag generation", + 'value' => array( + '#type' => 'html_tag', + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'description', + 'content' => 'Drupal test', + ), + ), + 'expected' => '' . "\n", + ), + // Test title tag generation. + array( + 'name' => "#type 'html_tag' title tag generation", + 'value' => array( + '#type' => 'html_tag', + '#tag' => 'title', + '#value' => 'title test', + ), + 'expected' => 'title test' . "\n", + ), + ); + + $this->assertElements($elements); } } @@ -500,3 +581,99 @@ $this->assertTrue($registry['theme_test_template_test_2'], 'Offset was returned correctly from the theme registry'); } } + +/** + * Tests for theme debug markup. + */ +class ThemeDebugMarkupTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Theme debug markup', + 'description' => 'Tests theme debug markup output.', + 'group' => 'Theme', + ); + } + + function setUp() { + parent::setUp('theme_test', 'node'); + theme_enable(array('test_theme')); + } + + /** + * Tests debug markup added to template output. + */ + function testDebugOutput() { + variable_set('theme_default', 'test_theme'); + // Enable the debug output. + variable_set('theme_debug', TRUE); + + $registry = theme_get_registry(); + $extension = '.tpl.php'; + // Populate array of templates. + $templates = drupal_find_theme_templates($registry, $extension, drupal_get_path('theme', 'test_theme')); + $templates += drupal_find_theme_templates($registry, $extension, drupal_get_path('module', 'node')); + + // Create a node and test different features of the debug markup. + $node = $this->drupalCreateNode(); + $this->drupalGet('node/' . $node->nid); + $this->assertRaw('', 'Theme debug markup found in theme output when debug is enabled.'); + $this->assertRaw("CALL: theme('node')", 'Theme call information found.'); + $this->assertRaw('x node--1' . $extension . PHP_EOL . ' * node--page' . $extension . PHP_EOL . ' * node' . $extension, 'Suggested template files found in order and node ID specific template shown as current template.'); + $template_filename = $templates['node__1']['path'] . '/' . $templates['node__1']['template'] . $extension; + $this->assertRaw("BEGIN OUTPUT from '$template_filename'", 'Full path to current template file found.'); + + // Create another node and make sure the template suggestions shown in the + // debug markup are correct. + $node2 = $this->drupalCreateNode(); + $this->drupalGet('node/' . $node2->nid); + $this->assertRaw('* node--2' . $extension . PHP_EOL . ' * node--page' . $extension . PHP_EOL . ' x node' . $extension, 'Suggested template files found in order and base template shown as current template.'); + + // Create another node and make sure the template suggestions shown in the + // debug markup are correct. + $node3 = $this->drupalCreateNode(); + $build = array('#theme' => 'node__foo__bar'); + $build += node_view($node3); + $output = drupal_render($build); + $this->assertTrue(strpos($output, "CALL: theme('node__foo__bar')") !== FALSE, 'Theme call information found.'); + $this->assertTrue(strpos($output, '* node--foo--bar' . $extension . PHP_EOL . ' * node--foo' . $extension . PHP_EOL . ' * node--3' . $extension . PHP_EOL . ' * node--page' . $extension . PHP_EOL . ' x node' . $extension) !== FALSE, 'Suggested template files found in order and base template shown as current template.'); + + // Disable theme debug. + variable_set('theme_debug', FALSE); + + $this->drupalGet('node/' . $node->nid); + $this->assertNoRaw('', 'Theme debug markup not found in theme output when debug is disabled.'); + } + +} + +/** + * Tests module-provided theme engines. + */ +class ModuleProvidedThemeEngineTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Theme engine test', + 'description' => 'Tests module-provided theme engines.', + 'group' => 'Theme', + ); + } + + function setUp() { + parent::setUp('theme_test'); + theme_enable(array('test_theme', 'test_theme_nyan_cat')); + } + + /** + * Ensures that the module provided theme engine is found and used by core. + */ + function testEngineIsFoundAndWorking() { + variable_set('theme_default', 'test_theme_nyan_cat'); + variable_set('admin_theme', 'test_theme_nyan_cat'); + + $this->drupalGet('theme-test/engine-info-test'); + $this->assertText('Miaou'); + } + +} diff -Naur drupal-7.23/modules/simpletest/tests/theme_test.info drupal-7.66/modules/simpletest/tests/theme_test.info --- drupal-7.23/modules/simpletest/tests/theme_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/theme_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/theme_test.module drupal-7.66/modules/simpletest/tests/theme_test.module --- drupal-7.23/modules/simpletest/tests/theme_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/theme_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -27,10 +27,19 @@ $themes['test_theme'] = drupal_get_path('module', 'theme_test') . '/themes/test_theme/test_theme.info'; $themes['test_basetheme'] = drupal_get_path('module', 'theme_test') . '/themes/test_basetheme/test_basetheme.info'; $themes['test_subtheme'] = drupal_get_path('module', 'theme_test') . '/themes/test_subtheme/test_subtheme.info'; + $themes['test_theme_nyan_cat'] = drupal_get_path('module', 'theme_test') . '/themes/test_theme_nyan_cat/test_theme_nyan_cat.info'; return $themes; } /** + * Implements hook_system_theme_engine_info(). + */ +function theme_test_system_theme_engine_info() { + $theme_engines['nyan_cat'] = drupal_get_path('module', 'theme_test') . '/themes/engines/nyan_cat/nyan_cat.engine'; + return $theme_engines; +} + +/** * Implements hook_menu(). */ function theme_test_menu() { @@ -53,6 +62,17 @@ 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['theme-test/drupal-add-region-content'] = array( + 'page callback' => '_theme_test_drupal_add_region_content', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['theme-test/engine-info-test'] = array( + 'description' => "Serves a simple page rendered using a Nyan Cat theme engine template.", + 'page callback' => '_theme_test_engine_info_test', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -127,6 +147,23 @@ } /** + * Page callback, calls drupal_add_region_content. + */ +function _theme_test_drupal_add_region_content() { + drupal_add_region_content('content', 'World'); + return 'Hello'; +} + +/** + * Serves a simple page renderered using a Nyan Cat theme engine template. + */ +function _theme_test_engine_info_test() { + return array( + '#markup' => theme('theme_test_template_test'), + ); +} + +/** * Theme function for testing theme('theme_test_foo'). */ function theme_theme_test_foo($variables) { diff -Naur drupal-7.23/modules/simpletest/tests/themes/engines/nyan_cat/nyan_cat.engine drupal-7.66/modules/simpletest/tests/themes/engines/nyan_cat/nyan_cat.engine --- drupal-7.23/modules/simpletest/tests/themes/engines/nyan_cat/nyan_cat.engine 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/engines/nyan_cat/nyan_cat.engine 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,53 @@ +filename) . '/template.theme'; + if (file_exists($file)) { + include_once DRUPAL_ROOT . '/' . $file; + } +} + +/** + * Implements hook_theme(). + */ +function nyan_cat_theme($existing, $type, $theme, $path) { + $templates = drupal_find_theme_functions($existing, array($theme)); + $templates += drupal_find_theme_templates($existing, '.nyan-cat.html', $path); + return $templates; +} + +/** + * Implements hook_extension(). + */ +function nyan_cat_extension() { + return '.nyan-cat.html'; +} + +/** + * Implements hook_render_template(). + * + * @param string $template_file + * The filename of the template to render. + * @param mixed[] $variables + * A keyed array of variables that will appear in the output. + * + * @return string + * The output generated by the template. + */ +function nyan_cat_render_template($template_file, $variables) { + $output = str_replace('div', 'nyancat', file_get_contents(DRUPAL_ROOT . '/' . $template_file)); + foreach ($variables as $key => $variable) { + if (strpos($output, '9' . $key) !== FALSE) { + $output = str_replace('9' . $key, $variable, $output); + } + } + return $output; +} diff -Naur drupal-7.23/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info drupal-7.66/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info --- drupal-7.23/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ settings[basetheme_only] = base theme value settings[subtheme_override] = base theme value -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info drupal-7.66/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info --- drupal-7.23/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ settings[subtheme_override] = subtheme value -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/themes/test_theme/templates/node--1.tpl.php drupal-7.66/modules/simpletest/tests/themes/test_theme/templates/node--1.tpl.php --- drupal-7.23/modules/simpletest/tests/themes/test_theme/templates/node--1.tpl.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_theme/templates/node--1.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,2 @@ + +Node Content Dummy diff -Naur drupal-7.23/modules/simpletest/tests/themes/test_theme/test_theme.info drupal-7.66/modules/simpletest/tests/themes/test_theme/test_theme.info --- drupal-7.23/modules/simpletest/tests/themes/test_theme/test_theme.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/themes/test_theme/test_theme.info 2019-04-17 22:39:36.000000000 +0200 @@ -17,8 +17,7 @@ settings[theme_test_setting] = default value -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/themes/test_theme/theme-settings.php drupal-7.66/modules/simpletest/tests/themes/test_theme/theme-settings.php --- drupal-7.23/modules/simpletest/tests/themes/test_theme/theme-settings.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_theme/theme-settings.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,32 @@ + 'checkbox', + '#title' => 'Test theme checkbox', + '#default_value' => theme_get_setting('test_theme_checkbox'), + ); + + // Force the form to be cached so we can test that this file is properly + // loaded and the custom submit handler is properly called even on a cached + // form build. + $form_state['cache'] = TRUE; + $form['#submit'][] = 'test_theme_form_system_theme_settings_submit'; +} + +/** + * Form submission handler for the test theme settings form. + * + * @see test_theme_form_system_theme_settings_alter() + */ +function test_theme_form_system_theme_settings_submit($form, &$form_state) { + drupal_set_message('The test theme setting was saved.'); +} diff -Naur drupal-7.23/modules/simpletest/tests/themes/test_theme_nyan_cat/templates/theme_test_template_test.nyan-cat.html drupal-7.66/modules/simpletest/tests/themes/test_theme_nyan_cat/templates/theme_test_template_test.nyan-cat.html --- drupal-7.23/modules/simpletest/tests/themes/test_theme_nyan_cat/templates/theme_test_template_test.nyan-cat.html 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_theme_nyan_cat/templates/theme_test_template_test.nyan-cat.html 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +Miaou \ No newline at end of file diff -Naur drupal-7.23/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info drupal-7.66/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info --- drupal-7.23/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,10 @@ +name = Nyan cat engine based test theme +description = Theme for testing the module-provided theme engines. +core = 7.x +hidden = TRUE +engine = nyan_cat + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/update_script_test.info drupal-7.66/modules/simpletest/tests/update_script_test.info --- drupal-7.23/modules/simpletest/tests/update_script_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/update_script_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/update_script_test.install drupal-7.66/modules/simpletest/tests/update_script_test.install --- drupal-7.23/modules/simpletest/tests/update_script_test.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/update_script_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -31,6 +31,19 @@ 'severity' => REQUIREMENT_ERROR, ); break; + case REQUIREMENT_INFO: + $requirements['update_script_test_stop'] = array( + 'title' => 'Update script test stop', + 'value' => 'Error', + 'description' => 'This is a requirements error provided by the update_script_test module to stop the page redirect for the info.', + 'severity' => REQUIREMENT_ERROR, + ); + $requirements['update_script_test'] = array( + 'title' => 'Update script test', + 'description' => 'This is a requirements info provided by the update_script_test module.', + 'severity' => REQUIREMENT_INFO, + ); + break; } } diff -Naur drupal-7.23/modules/simpletest/tests/update_test_1.info drupal-7.66/modules/simpletest/tests/update_test_1.info --- drupal-7.23/modules/simpletest/tests/update_test_1.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/update_test_1.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/update_test_2.info drupal-7.66/modules/simpletest/tests/update_test_2.info --- drupal-7.23/modules/simpletest/tests/update_test_2.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/update_test_2.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/update_test_3.info drupal-7.66/modules/simpletest/tests/update_test_3.info --- drupal-7.23/modules/simpletest/tests/update_test_3.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/update_test_3.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/upgrade/drupal-6.filled.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.filled.database.php --- drupal-7.23/modules/simpletest/tests/upgrade/drupal-6.filled.database.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.filled.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -19919,7 +19919,7 @@ 'vid' => '1', 'name' => 'vocabulary 1 (i=0)', 'description' => 'description of vocabulary 1 (i=0)', - 'help' => '', + 'help' => 'help for vocabulary 1 (i=0)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -19932,7 +19932,7 @@ 'vid' => '2', 'name' => 'vocabulary 2 (i=1)', 'description' => 'description of vocabulary 2 (i=1)', - 'help' => '', + 'help' => 'help for vocabulary 2 (i=1)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -19945,7 +19945,7 @@ 'vid' => '3', 'name' => 'vocabulary 3 (i=2)', 'description' => 'description of vocabulary 3 (i=2)', - 'help' => '', + 'help' => 'help for vocabulary 3 (i=2)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -19958,7 +19958,7 @@ 'vid' => '4', 'name' => 'vocabulary 4 (i=3)', 'description' => 'description of vocabulary 4 (i=3)', - 'help' => '', + 'help' => 'help for vocabulary 4 (i=3)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -19971,7 +19971,7 @@ 'vid' => '5', 'name' => 'vocabulary 5 (i=4)', 'description' => 'description of vocabulary 5 (i=4)', - 'help' => '', + 'help' => 'help for vocabulary 5 (i=4)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -19984,7 +19984,7 @@ 'vid' => '6', 'name' => 'vocabulary 6 (i=5)', 'description' => 'description of vocabulary 6 (i=5)', - 'help' => '', + 'help' => 'help for vocabulary 6 (i=5)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', @@ -19997,7 +19997,7 @@ 'vid' => '7', 'name' => 'vocabulary 7 (i=6)', 'description' => 'description of vocabulary 7 (i=6)', - 'help' => '', + 'help' => 'help for vocabulary 7 (i=6)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -20010,7 +20010,7 @@ 'vid' => '8', 'name' => 'vocabulary 8 (i=7)', 'description' => 'description of vocabulary 8 (i=7)', - 'help' => '', + 'help' => 'help for vocabulary 8 (i=7)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -20023,7 +20023,7 @@ 'vid' => '9', 'name' => 'vocabulary 9 (i=8)', 'description' => 'description of vocabulary 9 (i=8)', - 'help' => '', + 'help' => 'help for vocabulary 9 (i=8)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -20036,7 +20036,7 @@ 'vid' => '10', 'name' => 'vocabulary 10 (i=9)', 'description' => 'description of vocabulary 10 (i=9)', - 'help' => '', + 'help' => 'help for vocabulary 10 (i=9)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -20049,7 +20049,7 @@ 'vid' => '11', 'name' => 'vocabulary 11 (i=10)', 'description' => 'description of vocabulary 11 (i=10)', - 'help' => '', + 'help' => 'help for vocabulary 11 (i=10)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -20062,7 +20062,7 @@ 'vid' => '12', 'name' => 'vocabulary 12 (i=11)', 'description' => 'description of vocabulary 12 (i=11)', - 'help' => '', + 'help' => 'help for vocabulary 12 (i=11)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', @@ -20075,7 +20075,7 @@ 'vid' => '13', 'name' => 'vocabulary 13 (i=12)', 'description' => 'description of vocabulary 13 (i=12)', - 'help' => '', + 'help' => 'help for vocabulary 13 (i=12)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -20088,7 +20088,7 @@ 'vid' => '14', 'name' => 'vocabulary 14 (i=13)', 'description' => 'description of vocabulary 14 (i=13)', - 'help' => '', + 'help' => 'help for vocabulary 14 (i=13)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -20101,7 +20101,7 @@ 'vid' => '15', 'name' => 'vocabulary 15 (i=14)', 'description' => 'description of vocabulary 15 (i=14)', - 'help' => '', + 'help' => 'help for vocabulary 15 (i=14)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -20114,7 +20114,7 @@ 'vid' => '16', 'name' => 'vocabulary 16 (i=15)', 'description' => 'description of vocabulary 16 (i=15)', - 'help' => '', + 'help' => 'help for vocabulary 16 (i=15)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -20127,7 +20127,7 @@ 'vid' => '17', 'name' => 'vocabulary 17 (i=16)', 'description' => 'description of vocabulary 17 (i=16)', - 'help' => '', + 'help' => 'help for vocabulary 17 (i=16)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -20140,7 +20140,7 @@ 'vid' => '18', 'name' => 'vocabulary 18 (i=17)', 'description' => 'description of vocabulary 18 (i=17)', - 'help' => '', + 'help' => 'help for vocabulary 18 (i=17)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', @@ -20153,7 +20153,7 @@ 'vid' => '19', 'name' => 'vocabulary 19 (i=18)', 'description' => 'description of vocabulary 19 (i=18)', - 'help' => '', + 'help' => 'help for vocabulary 19 (i=18)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -20166,7 +20166,7 @@ 'vid' => '20', 'name' => 'vocabulary 20 (i=19)', 'description' => 'description of vocabulary 20 (i=19)', - 'help' => '', + 'help' => 'help for vocabulary 20 (i=19)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -20179,7 +20179,7 @@ 'vid' => '21', 'name' => 'vocabulary 21 (i=20)', 'description' => 'description of vocabulary 21 (i=20)', - 'help' => '', + 'help' => 'help for vocabulary 21 (i=20)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -20192,7 +20192,7 @@ 'vid' => '22', 'name' => 'vocabulary 22 (i=21)', 'description' => 'description of vocabulary 22 (i=21)', - 'help' => '', + 'help' => 'help for vocabulary 22 (i=21)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -20205,7 +20205,7 @@ 'vid' => '23', 'name' => 'vocabulary 23 (i=22)', 'description' => 'description of vocabulary 23 (i=22)', - 'help' => '', + 'help' => 'help for vocabulary 23 (i=22)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -20218,7 +20218,7 @@ 'vid' => '24', 'name' => 'vocabulary 24 (i=23)', 'description' => 'description of vocabulary 24 (i=23)', - 'help' => '', + 'help' => 'help for vocabulary 24 (i=23)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', diff -Naur drupal-7.23/modules/simpletest/tests/upgrade/drupal-6.upload.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.upload.database.php --- drupal-7.23/modules/simpletest/tests/upgrade/drupal-6.upload.database.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.upload.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -127,6 +127,38 @@ 'status' => '1', 'timestamp' => '1285708958', )) +// On some Drupal 6 sites, more than one file can have the same filepath. See +// https://www.drupal.org/node/1260938. +->values(array( + 'fid' => '12', + 'uid' => '1', + 'filename' => 'duplicate-name.png', + 'filepath' => 'sites/default/files/duplicate-name.png', + 'filemime' => 'image/png', + 'filesize' => '314', + 'status' => '1', + 'timestamp' => '1285708958', +)) +->values(array( + 'fid' => '13', + 'uid' => '1', + 'filename' => 'duplicate-name.png', + 'filepath' => 'sites/default/files/duplicate-name.png', + 'filemime' => 'image/png', + 'filesize' => '315', + 'status' => '1', + 'timestamp' => '1285708958', +)) +->values(array( + 'fid' => '14', + 'uid' => '1', + 'filename' => 'duplicate-name.png', + 'filepath' => 'sites/default/files/duplicate-name.png', + 'filemime' => 'image/png', + 'filesize' => '316', + 'status' => '1', + 'timestamp' => '1285708958', +)) ->execute(); db_insert('node')->fields(array( @@ -197,6 +229,23 @@ 'tnid' => '0', 'translate' => '0', )) +->values(array( + 'nid' => '41', + 'vid' => '55', + 'type' => 'page', + 'language' => '', + 'title' => 'node title 41 revision 55', + 'uid' => '1', + 'status' => '1', + 'created' => '1285709012', + 'changed' => '1285709012', + 'comment' => '0', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '0', + 'translate' => '0', +)) ->execute(); db_insert('node_revisions')->fields(array( @@ -254,6 +303,28 @@ 'timestamp' => '1285709012', 'format' => '1', )) +->values(array( + 'nid' => '41', + 'vid' => '54', + 'uid' => '1', + 'title' => 'node title 41 revision 54', + 'body' => "Attachments:\r\nduplicate-name.png", + 'teaser' => "Attachments:\r\nduplicate-name.png", + 'log' => '', + 'timestamp' => '1285709012', + 'format' => '1', +)) +->values(array( + 'nid' => '41', + 'vid' => '55', + 'uid' => '1', + 'title' => 'node title 41 revision 55', + 'body' => "Attachments:\r\nduplicate-name.png\r\nduplicate-name.png", + 'teaser' => "Attachments:\r\nduplicate-name.png\r\nduplicate-name.png", + 'log' => '', + 'timestamp' => '1285709012', + 'format' => '1', +)) ->execute(); db_create_table('upload', array( @@ -415,6 +486,30 @@ 'list' => '1', 'weight' => '0', )) +->values(array( + 'fid' => '12', + 'nid' => '41', + 'vid' => '54', + 'description' => 'duplicate-name.png', + 'list' => '1', + 'weight' => '0', +)) +->values(array( + 'fid' => '13', + 'nid' => '41', + 'vid' => '55', + 'description' => 'first description', + 'list' => '0', + 'weight' => '0', +)) +->values(array( + 'fid' => '14', + 'nid' => '41', + 'vid' => '55', + 'description' => 'second description', + 'list' => '1', + 'weight' => '0', +)) ->execute(); // Add series of entries for invalid node vids to the {upload} table. @@ -431,7 +526,7 @@ ->values(array( 'fid' => $i, 'nid' => '40', - 'vid' => 24 + $i, + 'vid' => 26 + $i, 'description' => 'crazy-basename.png', 'list' => '1', 'weight' => '0', @@ -440,7 +535,7 @@ ->values(array( 'fid' => 2, 'nid' => '40', - 'vid' => 24 + $i + 1, + 'vid' => 26 + $i + 1, 'description' => 'crazy-basename.png', 'list' => '1', 'weight' => '0', diff -Naur drupal-7.23/modules/simpletest/tests/upgrade/upgrade.taxonomy.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.taxonomy.test --- drupal-7.23/modules/simpletest/tests/upgrade/upgrade.taxonomy.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.taxonomy.test 2019-04-17 22:20:46.000000000 +0200 @@ -56,6 +56,11 @@ $this->assertFalse(db_table_exists('taxonomy_vocabulary_node_type'), 'taxonomy_vocabulary_node_type has been removed.'); $this->assertFalse(db_table_exists('taxonomy_term_node'), 'taxonomy_term_node has been removed.'); + // Check that taxonomy_index has not stored nids of unpublished nodes. + $nids = db_query('SELECT nid from {node} WHERE status = :status', array(':status' => NODE_NOT_PUBLISHED))->fetchCol(); + $indexed_nids = db_query('SELECT DISTINCT nid from {taxonomy_index}')->fetchCol(); + $this->assertFalse(array_intersect($nids, $indexed_nids), 'No unpublished nid present in taxonomy_index'); + // Check that the node type 'page' has been associated to a taxonomy // reference field for each vocabulary. $voc_keys = array(); @@ -69,9 +74,10 @@ $this->assertEqual($voc_keys, $inst_keys, 'Node type page has instances for every vocabulary.'); // Ensure instance variables are getting through. - foreach ($instances as $instance) { - $this->assertTrue(isset($instance['required']), 'The required setting was preserved during the upgrade path.'); - $this->assertTrue($instance['description'], 'The description was preserved during the upgrade path'); + foreach (array_unique($instances) as $instance) { + $field_instance = field_info_instance('node', $instance, 'page'); + $this->assertTrue(isset($field_instance['required']), 'The required setting was preserved during the upgrade path.'); + $this->assertTrue($field_instance['description'], 'The description was preserved during the upgrade path'); } // Node type 'story' was not explicitly in $vocabulary->nodes but diff -Naur drupal-7.23/modules/simpletest/tests/upgrade/upgrade.upload.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.upload.test --- drupal-7.23/modules/simpletest/tests/upgrade/upgrade.upload.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.upload.test 2019-04-17 22:20:46.000000000 +0200 @@ -64,12 +64,35 @@ } $this->assertIdentical($filenames, $recorded_filenames, 'The uploaded files are present in the same order after the upgrade.'); } + // Test for the file with repeating basename to only have the streaming // path replaced. $node = node_load(40, 53); $repeated_basename_file = $node->upload[LANGUAGE_NONE][4]; $this->assertEqual($repeated_basename_file['uri'], 'private://drupal-6/file/directory/path/crazy-basename.png', "The file with the repeated basename path only had the stream portion replaced"); + // Ensure that filepaths are deduplicated. + $node0 = node_load(41, 54); + $node1 = node_load(41, 55); + // Ensure that both revisions point to the same file ID. + $items0 = field_get_items('node', $node0, 'upload'); + $this->assertEqual(count($items0), 1); + $items1 = field_get_items('node', $node1, 'upload'); + $this->assertEqual(count($items1), 2); + $this->assertEqual($items0[0]['fid'], $items1[0]['fid']); + $this->assertEqual($items0[0]['fid'], $items1[1]['fid']); + // The revision with more than one reference to the same file should retain + // the original settings for each reference. + $this->assertEqual($items1[0]['description'], 'first description'); + $this->assertEqual($items1[0]['display'], 0); + $this->assertEqual($items1[1]['description'], 'second description'); + $this->assertEqual($items1[1]['display'], 1); + // Ensure that the latest version of the files are used. + $this->assertEqual($items1[0]['filesize'], 316); + $this->assertEqual($items1[1]['filesize'], 316); + // No duplicate files should remain on the Drupal 7 site. + $this->assertEqual(0, db_query("SELECT COUNT(*) FROM {file_managed} GROUP BY uri HAVING COUNT(fid) > 1")->fetchField()); + // Make sure the file settings were properly migrated. $d6_file_directory_temp = '/drupal-6/file/directory/temp'; $d6_file_directory_path = '/drupal-6/file/directory/path'; diff -Naur drupal-7.23/modules/simpletest/tests/url_alter_test.info drupal-7.66/modules/simpletest/tests/url_alter_test.info --- drupal-7.23/modules/simpletest/tests/url_alter_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/url_alter_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/simpletest/tests/xmlrpc.test drupal-7.66/modules/simpletest/tests/xmlrpc.test --- drupal-7.23/modules/simpletest/tests/xmlrpc.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/xmlrpc.test 2019-04-17 22:20:46.000000000 +0200 @@ -211,6 +211,11 @@ * Make sure that XML-RPC can transfer large messages. */ function testSizedMessages() { + // These tests can produce up to 128 x 160 words in the XML-RPC message + // (see xmlrpc_test_message_sized_in_kb()) with 4 tags used to represent + // each. Set a large enough tag limit to allow this to be tested. + variable_set('xmlrpc_message_maximum_tag_count', 100000); + $xml_url = url(NULL, array('absolute' => TRUE)) . 'xmlrpc.php'; $sizes = array(8, 80, 160); foreach ($sizes as $size) { @@ -241,4 +246,38 @@ $this->assertEqual($removed, 'system.methodSignature', 'Hiding builting system.methodSignature with hook_xmlrpc_alter works'); } + /** + * Test limits on system.multicall that can prevent brute-force attacks. + */ + function testMulticallLimit() { + $url = url(NULL, array('absolute' => TRUE)) . 'xmlrpc.php'; + $multicall_args = array(); + $num_method_calls = 10; + for ($i = 0; $i < $num_method_calls; $i++) { + $struct = array('i' => $i); + $multicall_args[] = array('methodName' => 'validator1.echoStructTest', 'params' => array($struct)); + } + // Test limits of 1, 5, 9, 13. + for ($limit = 1; $limit < $num_method_calls + 4; $limit += 4) { + variable_set('xmlrpc_multicall_duplicate_method_limit', $limit); + $results = xmlrpc($url, array('system.multicall' => array($multicall_args))); + $this->assertEqual($num_method_calls, count($results)); + for ($i = 0; $i < min($limit, $num_method_calls); $i++) { + $x = array_shift($results); + $this->assertTrue(empty($x->is_error), "Result $i is not an error"); + $this->assertEqual($multicall_args[$i]['params'][0], $x); + } + for (; $i < $num_method_calls; $i++) { + $x = array_shift($results); + $this->assertFalse(empty($x->is_error), "Result $i is an error"); + $this->assertEqual(-156579, $x->code); + } + } + variable_set('xmlrpc_multicall_duplicate_method_limit', -1); + $results = xmlrpc($url, array('system.multicall' => array($multicall_args))); + $this->assertEqual($num_method_calls, count($results)); + foreach ($results as $i => $x) { + $this->assertTrue(empty($x->is_error), "Result $i is not an error"); + } + } } diff -Naur drupal-7.23/modules/simpletest/tests/xmlrpc_test.info drupal-7.66/modules/simpletest/tests/xmlrpc_test.info --- drupal-7.23/modules/simpletest/tests/xmlrpc_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/xmlrpc_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/statistics/statistics.admin.inc drupal-7.66/modules/statistics/statistics.admin.inc --- drupal-7.23/modules/statistics/statistics.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/statistics/statistics.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -59,7 +59,7 @@ * statistics settings form, but is dependent on cron running. * * @return - * A render array containing information about the the top pages. + * A render array containing information about the top pages. */ function statistics_top_pages() { $header = array( @@ -137,7 +137,8 @@ ->groupBy('u.name') ->groupBy('bl.iid') ->limit(30) - ->orderByHeader($header); + ->orderByHeader($header) + ->orderBy('a.hostname'); $uniques_query = db_select('accesslog')->distinct(); $uniques_query->fields('accesslog', array('uid', 'hostname')); @@ -263,9 +264,7 @@ ); return $build; } - else { - drupal_not_found(); - } + return MENU_NOT_FOUND; } /** @@ -305,6 +304,17 @@ '#default_value' => variable_get('statistics_count_content_views', 0), '#description' => t('Increment a counter each time content is viewed.'), ); + $form['content']['statistics_count_content_views_ajax'] = array( + '#type' => 'checkbox', + '#title' => t('Use Ajax to increment the counter'), + '#default_value' => variable_get('statistics_count_content_views_ajax', 0), + '#description' => t('Perform the count asynchronously after page load rather than during page generation.'), + '#states' => array( + 'disabled' => array( + ':input[name="statistics_count_content_views"]' => array('checked' => FALSE), + ), + ), + ); return system_settings_form($form); } diff -Naur drupal-7.23/modules/statistics/statistics.info drupal-7.66/modules/statistics/statistics.info --- drupal-7.23/modules/statistics/statistics.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/statistics/statistics.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = statistics.test configure = admin/config/system/statistics -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/statistics/statistics.install drupal-7.66/modules/statistics/statistics.install --- drupal-7.23/modules/statistics/statistics.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/statistics/statistics.install 2019-04-17 22:20:46.000000000 +0200 @@ -11,6 +11,7 @@ function statistics_uninstall() { // Remove variables. variable_del('statistics_count_content_views'); + variable_del('statistics_count_content_views_ajax'); variable_del('statistics_enable_access_log'); variable_del('statistics_flush_accesslog_timer'); variable_del('statistics_day_timestamp'); diff -Naur drupal-7.23/modules/statistics/statistics.js drupal-7.66/modules/statistics/statistics.js --- drupal-7.23/modules/statistics/statistics.js 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/statistics/statistics.js 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,10 @@ +(function ($) { + $(document).ready(function() { + $.ajax({ + type: "POST", + cache: false, + url: Drupal.settings.statistics.url, + data: Drupal.settings.statistics.data + }); + }); +})(jQuery); diff -Naur drupal-7.23/modules/statistics/statistics.module drupal-7.66/modules/statistics/statistics.module --- drupal-7.23/modules/statistics/statistics.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/statistics/statistics.module 2019-04-17 22:20:46.000000000 +0200 @@ -57,7 +57,7 @@ // in which case we need to bootstrap to the session phase anyway. drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES); - if (variable_get('statistics_count_content_views', 0)) { + if (variable_get('statistics_count_content_views', 0) && !variable_get('statistics_count_content_views_ajax', 0)) { // We are counting content views. if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == NULL) { // A node has been viewed, so update the node's counters. @@ -115,6 +115,21 @@ * Implements hook_node_view(). */ function statistics_node_view($node, $view_mode) { + // Attach Ajax node count statistics if configured. + if (variable_get('statistics_count_content_views', 0) && variable_get('statistics_count_content_views_ajax', 0)) { + if (!empty($node->nid) && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) { + $statistics = drupal_get_path('module', 'statistics') . '/statistics.js'; + $node->content['#attached']['js'][$statistics] = array( + 'scope' => 'footer', + ); + $settings = array('data' => array('nid' => $node->nid), 'url' => url(drupal_get_path('module', 'statistics') . '/statistics.php')); + $node->content['#attached']['js'][] = array( + 'data' => array('statistics' => $settings), + 'type' => 'setting', + ); + } + } + if ($view_mode != 'rss') { if (user_access('view post access counter')) { $statistics = statistics_get($node->nid); @@ -230,7 +245,7 @@ * Implements hook_cron(). */ function statistics_cron() { - $statistics_timestamp = variable_get('statistics_day_timestamp', ''); + $statistics_timestamp = variable_get('statistics_day_timestamp', 0); if ((REQUEST_TIME - $statistics_timestamp) >= 86400) { // Reset day counts. diff -Naur drupal-7.23/modules/statistics/statistics.pages.inc drupal-7.66/modules/statistics/statistics.pages.inc --- drupal-7.23/modules/statistics/statistics.pages.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/statistics/statistics.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -54,9 +54,7 @@ $build['statistics_pager'] = array('#theme' => 'pager'); return $build; } - else { - drupal_not_found(); - } + return MENU_NOT_FOUND; } /** @@ -99,7 +97,5 @@ $build['statistics_pager'] = array('#theme' => 'pager'); return $build; } - else { - drupal_not_found(); - } + return MENU_NOT_FOUND; } diff -Naur drupal-7.23/modules/statistics/statistics.php drupal-7.66/modules/statistics/statistics.php --- drupal-7.23/modules/statistics/statistics.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/statistics/statistics.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,33 @@ +key(array('nid' => $nid)) + ->fields(array( + 'daycount' => 1, + 'totalcount' => 1, + 'timestamp' => REQUEST_TIME, + )) + ->expression('daycount', 'daycount + 1') + ->expression('totalcount', 'totalcount + 1') + ->execute(); + } + } +} diff -Naur drupal-7.23/modules/statistics/statistics.test drupal-7.66/modules/statistics/statistics.test --- drupal-7.23/modules/statistics/statistics.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/statistics/statistics.test 2019-04-17 22:20:46.000000000 +0200 @@ -35,7 +35,7 @@ 'title' => 'test', 'path' => 'node/1', 'url' => 'http://example.com', - 'hostname' => '192.168.1.1', + 'hostname' => '1.2.3.3', 'uid' => 0, 'sid' => 10, 'timer' => 10, @@ -118,6 +118,22 @@ $node_counter = statistics_get($this->node->nid); $this->assertIdentical($node_counter['totalcount'], '3'); + // Test that Ajax logging doesn't occur when disabled. + $post = http_build_query(array('nid' => $this->node->nid)); + $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); + global $base_url; + $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics'). '/statistics.php'; + drupal_http_request($stats_path, array('method' => 'POST', 'data' => $post, 'headers' => $headers, 'timeout' => 10000)); + $node_counter = statistics_get($this->node->nid); + $this->assertIdentical($node_counter['totalcount'], '3', 'Page request was not counted via Ajax.'); + + // Test that Ajax logging occurs when enabled. + variable_set('statistics_count_content_views_ajax', 1); + drupal_http_request($stats_path, array('method' => 'POST', 'data' => $post, 'headers' => $headers, 'timeout' => 10000)); + $node_counter = statistics_get($this->node->nid); + $this->assertIdentical($node_counter['totalcount'], '4', 'Page request was counted via Ajax.'); + variable_set('statistics_count_content_views_ajax', 0); + // Visit edit page to generate a title greater than 255. $path = 'node/' . $this->node->nid . '/edit'; $expected = array( @@ -142,7 +158,6 @@ $log = db_query('SELECT * FROM {accesslog}')->fetchAll(PDO::FETCH_ASSOC); $this->assertTrue(is_array($log) && count($log) == 8, 'Page request was logged for a path over 255 characters.'); $this->assertEqual($log[7]['path'], truncate_utf8($long_path, 255)); - } } @@ -253,7 +268,7 @@ */ function testIPAddressBlocking() { // IP address for testing. - $test_ip_address = '192.168.1.1'; + $test_ip_address = '1.2.3.3'; // Verify the IP address from accesslog appears on the top visitors page // and that a 'block IP address' link is displayed. @@ -399,7 +414,7 @@ $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); $this->drupalGet('admin/reports/visitors'); diff -Naur drupal-7.23/modules/syslog/syslog.info drupal-7.66/modules/syslog/syslog.info --- drupal-7.23/modules/syslog/syslog.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/syslog/syslog.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = syslog.test configure = admin/config/development/logging -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/system/form.api.php drupal-7.66/modules/system/form.api.php --- drupal-7.23/modules/system/form.api.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/system/form.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,126 @@ +fetchField(); + } + + // For this example, we decide that we can safely process + // 5 nodes at a time without a timeout. + $limit = 5; + + // With each pass through the callback, retrieve the next group of nids. + $result = db_query_range("SELECT nid FROM {node} WHERE nid > %d ORDER BY nid ASC", $context['sandbox']['current_node'], 0, $limit); + while ($row = db_fetch_array($result)) { + + // Here we actually perform our processing on the current node. + $node = node_load($row['nid'], NULL, TRUE); + $node->value1 = $options1; + $node->value2 = $options2; + node_save($node); + + // Store some result for post-processing in the finished callback. + $context['results'][] = check_plain($node->title); + + // Update our progress information. + $context['sandbox']['progress']++; + $context['sandbox']['current_node'] = $node->nid; + $context['message'] = t('Now processing %node', array('%node' => $node->title)); + } + + // Inform the batch engine that we are not finished, + // and provide an estimation of the completion level we reached. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } +} + +/** + * Complete a batch process. + * + * Callback for batch_set(). + * + * This callback may be specified in a batch to perform clean-up operations, or + * to analyze the results of the batch operations. + * + * @param $success + * A boolean indicating whether the batch has completed successfully. + * @param $results + * The value set in $context['results'] by callback_batch_operation(). + * @param $operations + * If $success is FALSE, contains the operations that remained unprocessed. + */ +function callback_batch_finished($success, $results, $operations) { + if ($success) { + // Here we do something meaningful with the results. + $message = t("!count items were processed.", array( + '!count' => count($results), + )); + $message .= theme('item_list', array('items' => $results)); + drupal_set_message($message); + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = t('An error occurred while processing %error_operation with arguments: @arguments', array( + '%error_operation' => $error_operation[0], + '@arguments' => print_r($error_operation[1], TRUE) + )); + drupal_set_message($message, 'error'); + } +} diff -Naur drupal-7.23/modules/system/image.gd.inc drupal-7.66/modules/system/image.gd.inc --- drupal-7.23/modules/system/image.gd.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/image.gd.inc 2019-04-17 22:20:46.000000000 +0200 @@ -56,13 +56,8 @@ * A boolean indicating if the GD toolkit is available on this machine. */ function image_gd_check_settings() { - if ($check = get_extension_funcs('gd')) { - if (in_array('imagegd2', $check)) { - // GD2 support is available. - return TRUE; - } - } - return FALSE; + // GD2 support is available. + return function_exists('imagegd2'); } /** @@ -121,38 +116,62 @@ return FALSE; } - $width = $image->info['width']; - $height = $image->info['height']; + // PHP 5.5 GD bug: https://bugs.php.net/bug.php?id=65148: To prevent buggy + // behavior on negative multiples of 90 degrees we convert any negative + // angle to a positive one between 0 and 360 degrees. + $degrees -= floor($degrees / 360) * 360; - // Convert the hexadecimal background value to a color index value. + // Convert the hexadecimal background value to a RGBA array. if (isset($background)) { - $rgb = array(); - for ($i = 16; $i >= 0; $i -= 8) { - $rgb[] = (($background >> $i) & 0xFF); - } - $background = imagecolorallocatealpha($image->resource, $rgb[0], $rgb[1], $rgb[2], 0); + $background = array( + 'red' => $background >> 16 & 0xFF, + 'green' => $background >> 8 & 0xFF, + 'blue' => $background & 0xFF, + 'alpha' => 0, + ); } - // Set the background color as transparent if $background is NULL. else { - // Get the current transparent color. - $background = imagecolortransparent($image->resource); - - // If no transparent colors, use white. - if ($background == 0) { - $background = imagecolorallocatealpha($image->resource, 255, 255, 255, 0); - } + // Background color is not specified: use transparent white as background. + $background = array( + 'red' => 255, + 'green' => 255, + 'blue' => 255, + 'alpha' => 127 + ); } + // Store the color index for the background as that is what GD uses. + $background_idx = imagecolorallocatealpha($image->resource, $background['red'], $background['green'], $background['blue'], $background['alpha']); + // Images are assigned a new color palette when rotating, removing any // transparency flags. For GIF images, keep a record of the transparent color. if ($image->info['extension'] == 'gif') { - $transparent_index = imagecolortransparent($image->resource); - if ($transparent_index != 0) { - $transparent_gif_color = imagecolorsforindex($image->resource, $transparent_index); + // GIF does not work with a transparency channel, but can define 1 color + // in its palette to act as transparent. + + // Get the current transparent color, if any. + $gif_transparent_id = imagecolortransparent($image->resource); + if ($gif_transparent_id !== -1) { + // The gif already has a transparent color set: remember it to set it on + // the rotated image as well. + $transparent_gif_color = imagecolorsforindex($image->resource, $gif_transparent_id); + + if ($background['alpha'] >= 127) { + // We want a transparent background: use the color already set to act + // as transparent, as background. + $background_idx = $gif_transparent_id; + } + } + else { + // The gif does not currently have a transparent color set. + if ($background['alpha'] >= 127) { + // But as the background is transparent, it should get one. + $transparent_gif_color = $background; + } } } - $image->resource = imagerotate($image->resource, 360 - $degrees, $background); + $image->resource = imagerotate($image->resource, 360 - $degrees, $background_idx); // GIFs need to reassign the transparent color after performing the rotate. if (isset($transparent_gif_color)) { @@ -234,7 +253,24 @@ function image_gd_load(stdClass $image) { $extension = str_replace('jpg', 'jpeg', $image->info['extension']); $function = 'imagecreatefrom' . $extension; - return (function_exists($function) && $image->resource = $function($image->source)); + if (function_exists($function) && $image->resource = $function($image->source)) { + if (imageistruecolor($image->resource)) { + return TRUE; + } + else { + // Convert indexed images to truecolor, copying the image to a new + // truecolor resource, so that filters work correctly and don't result + // in unnecessary dither. + $resource = image_gd_create_tmp($image, $image->info['width'], $image->info['height']); + if ($resource) { + imagecopy($resource, $image->resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource)); + imagedestroy($image->resource); + $image->resource = $resource; + } + } + return (bool) $image->resource; + } + return FALSE; } /** @@ -302,17 +338,31 @@ $res = imagecreatetruecolor($width, $height); if ($image->info['extension'] == 'gif') { - // Grab transparent color index from image resource. + // Find out if a transparent color is set, will return -1 if no + // transparent color has been defined in the image. $transparent = imagecolortransparent($image->resource); if ($transparent >= 0) { - // The original must have a transparent color, allocate to the new image. - $transparent_color = imagecolorsforindex($image->resource, $transparent); - $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); - - // Flood with our new transparent color. - imagefill($res, 0, 0, $transparent); - imagecolortransparent($res, $transparent); + // Find out the number of colors in the image palette. It will be 0 for + // truecolor images. + $palette_size = imagecolorstotal($image->resource); + if ($palette_size == 0 || $transparent < $palette_size) { + // Set the transparent color in the new resource, either if it is a + // truecolor image or if the transparent color is part of the palette. + // Since the index of the transparency color is a property of the + // image rather than of the palette, it is possible that an image + // could be created with this index set outside the palette size (see + // http://stackoverflow.com/a/3898007). + $transparent_color = imagecolorsforindex($image->resource, $transparent); + $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + + // Flood with our new transparent color. + imagefill($res, 0, 0, $transparent); + imagecolortransparent($res, $transparent); + } + else { + imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255)); + } } } elseif ($image->info['extension'] == 'png') { @@ -346,7 +396,7 @@ */ function image_gd_get_info(stdClass $image) { $details = FALSE; - $data = getimagesize($image->source); + $data = @getimagesize($image->source); if (isset($data) && is_array($data)) { $extensions = array('1' => 'gif', '2' => 'jpg', '3' => 'png'); diff -Naur drupal-7.23/modules/system/language.api.php drupal-7.66/modules/system/language.api.php --- drupal-7.23/modules/system/language.api.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/language.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -111,18 +111,18 @@ * * @return * An associative array of language negotiation provider definitions. The keys - * are provider identifiers, and the values are associative arrays definining + * are provider identifiers, and the values are associative arrays defining * each provider, with the following elements: * - types: An array of allowed language types. If a language negotiation * provider does not specify which language types it should be used with, it * will be available for all the configurable language types. * - callbacks: An associative array of functions that will be called to * perform various tasks. Possible elements are: - * - negotiation: (required) Name of the callback function that determines - * the language value. - * - language_switch: (optional) Name of the callback function that - * determines links for a language switcher block associated with this - * provider. See language_switcher_url() for an example. + * - language: (required) Name of the callback function that determines the + * language value. + * - switcher: (optional) Name of the callback function that determines + * links for a language switcher block associated with this provider. See + * language_switcher_url() for an example. * - url_rewrite: (optional) Name of the callback function that provides URL * rewriting, if needed by this provider. * - file: The file where callback functions are defined (this file will be diff -Naur drupal-7.23/modules/system/system.admin.inc drupal-7.66/modules/system/system.admin.inc --- drupal-7.23/modules/system/system.admin.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -309,7 +309,7 @@ } drupal_goto('admin/appearance'); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } /** @@ -337,7 +337,7 @@ } drupal_goto('admin/appearance'); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } /** @@ -383,7 +383,7 @@ } drupal_goto('admin/appearance'); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } /** @@ -572,9 +572,10 @@ // Process the theme and all its base themes. foreach ($theme_keys as $theme) { // Include the theme-settings.php file. - $filename = DRUPAL_ROOT . '/' . str_replace("/$theme.info", '', $themes[$theme]->filename) . '/theme-settings.php'; - if (file_exists($filename)) { - require_once $filename; + $theme_settings_path = drupal_get_path('theme', $theme) . '/theme-settings.php'; + if (file_exists(DRUPAL_ROOT . '/' . $theme_settings_path)) { + require_once DRUPAL_ROOT . '/' . $theme_settings_path; + $form_state['build_info']['files'][] = $theme_settings_path; } // Call theme-specific settings. @@ -640,13 +641,13 @@ // If the user provided a path for a logo or favicon file, make sure a file // exists at that path. - if ($form_state['values']['logo_path']) { + if (!empty($form_state['values']['logo_path'])) { $path = _system_theme_settings_validate_path($form_state['values']['logo_path']); if (!$path) { form_set_error('logo_path', t('The custom logo path is invalid.')); } } - if ($form_state['values']['favicon_path']) { + if (!empty($form_state['values']['favicon_path'])) { $path = _system_theme_settings_validate_path($form_state['values']['favicon_path']); if (!$path) { form_set_error('favicon_path', t('The custom favicon path is invalid.')); @@ -703,14 +704,16 @@ // If the user uploaded a new logo or favicon, save it to a permanent location // and use it in place of the default theme-provided file. - if ($file = $values['logo_upload']) { + if (!empty($values['logo_upload'])) { + $file = $values['logo_upload']; unset($values['logo_upload']); $filename = file_unmanaged_copy($file->uri); $values['default_logo'] = 0; $values['logo_path'] = $filename; $values['toggle_logo'] = 1; } - if ($file = $values['favicon_upload']) { + if (!empty($values['favicon_upload'])) { + $file = $values['favicon_upload']; unset($values['favicon_upload']); $filename = file_unmanaged_copy($file->uri); $values['default_favicon'] = 0; @@ -950,7 +953,11 @@ } /** - * Array sorting callback; sorts modules or themes by their name. + * Sorts themes by their names, with the default theme listed first. + * + * Callback for uasort() within system_themes_page(). + * + * @see system_sort_modules_by_info_name(). */ function system_sort_themes($a, $b) { if ($a->is_default) { @@ -995,22 +1002,28 @@ $status_short = ''; $status_long = ''; + // Initialize empty arrays of long and short reasons explaining why the + // module is incompatible. + // Add each reason as a separate element in both the arrays. + $reasons_short = array(); + $reasons_long = array(); + // Check the core compatibility. if (!isset($info['core']) || $info['core'] != DRUPAL_CORE_COMPATIBILITY) { $compatible = FALSE; - $status_short .= t('Incompatible with this version of Drupal core.'); - $status_long .= t('This version is not compatible with Drupal !core_version and should be replaced.', array('!core_version' => DRUPAL_CORE_COMPATIBILITY)); + $reasons_short[] = t('Incompatible with this version of Drupal core.'); + $reasons_long[] = t('This version is not compatible with Drupal !core_version and should be replaced.', array('!core_version' => DRUPAL_CORE_COMPATIBILITY)); } // Ensure this module is compatible with the currently installed version of PHP. if (version_compare(phpversion(), $info['php']) < 0) { $compatible = FALSE; - $status_short .= t('Incompatible with this version of PHP'); + $reasons_short[] = t('Incompatible with this version of PHP'); $php_required = $info['php']; if (substr_count($info['php'], '.') < 2) { $php_required .= '.*'; } - $status_long .= t('This module requires PHP version @php_required and is incompatible with PHP version !php_version.', array('@php_required' => $php_required, '!php_version' => phpversion())); + $reasons_long[] = t('This module requires PHP version @php_required and is incompatible with PHP version !php_version.', array('@php_required' => $php_required, '!php_version' => phpversion())); } // If this module is compatible, present a checkbox indicating @@ -1026,6 +1039,8 @@ } } else { + $status_short = implode(' ', $reasons_short); + $status_long = implode(' ', $reasons_long); $form['enable'] = array( '#markup' => theme('image', array('path' => 'misc/watchdog-error.png', 'alt' => $status_short, 'title' => $status_short)), ); @@ -1618,6 +1633,7 @@ $form['cron']['cron_safe_threshold'] = array( '#type' => 'select', '#title' => t('Run cron every'), + '#description' => t('More information about setting up scheduled tasks can be found by reading the cron tutorial on drupal.org.', array('@url' => url('http://drupal.org/cron'))), '#default_value' => variable_get('cron_safe_threshold', DRUPAL_CRON_DEFAULT_THRESHOLD), '#options' => array(0 => t('Never')) + drupal_map_assoc(array(3600, 10800, 21600, 43200, 86400, 604800), 'format_interval'), ); @@ -1797,7 +1813,7 @@ '#title' => t('Private file system path'), '#default_value' => variable_get('file_private_path', ''), '#maxlength' => 255, - '#description' => t('An existing local file system path for storing private files. It should be writable by Drupal and not accessible over the web. See the online handbook for more information about securing private files.', array('@handbook' => 'http://drupal.org/documentation/modules/file')), + '#description' => t('An existing local file system path for storing private files. It should be writable by Drupal and not accessible over the web. See the online handbook for more information about securing private files.', array('@handbook' => 'https://www.drupal.org/docs/7/core/modules/file/overview')), '#after_build' => array('system_check_directory'), ); @@ -1841,7 +1857,7 @@ if (count($toolkits_available) == 0) { variable_del('image_toolkit'); $form['image_toolkit_help'] = array( - '#markup' => t("No image toolkits were detected. Drupal includes support for PHP's built-in image processing functions but they were not detected on this system. You should consult your system administrator to have them enabled, or try using a third party toolkit.", array('gd-link' => url('http://php.net/gd'))), + '#markup' => t("No image toolkits were detected. Drupal includes support for PHP's built-in image processing functions but they were not detected on this system. You should consult your system administrator to have them enabled, or try using a third party toolkit.", array('!gd-link' => url('http://php.net/gd'))), ); return $form; } @@ -2187,6 +2203,11 @@ * Return the date for a given format string via Ajax. */ function system_date_time_lookup() { + // This callback is protected with a CSRF token because user input from the + // query string is reflected in the output. + if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'admin/config/regional/date-time/formats/lookup')) { + return MENU_ACCESS_DENIED; + } $result = format_date(REQUEST_TIME, 'custom', $_GET['format']); drupal_json_output($result); } @@ -2545,9 +2566,21 @@ /** * Returns HTML for the status report. * + * This theme function is dependent on install.inc being loaded, because + * that's where the constants are defined. + * * @param $variables * An associative array containing: - * - requirements: An array of requirements. + * - requirements: An array of requirements/status items. Each requirement + * is an associative array containing the following elements: + * - title: The name of the requirement. + * - value: (optional) The current value (version, time, level, etc). + * - description: (optional) The description of the requirement. + * - severity: (optional) The requirement's result/severity level, one of: + * - REQUIREMENT_INFO: Status information. + * - REQUIREMENT_OK: The requirement is satisfied. + * - REQUIREMENT_WARNING: The requirement failed with a warning. + * - REQUIREMENT_ERROR: The requirement failed with an error. * * @ingroup themeable */ @@ -2575,8 +2608,10 @@ foreach ($requirements as $requirement) { if (empty($requirement['#type'])) { - $severity = $severities[isset($requirement['severity']) ? (int) $requirement['severity'] : 0]; + $severity = $severities[isset($requirement['severity']) ? (int) $requirement['severity'] : REQUIREMENT_OK]; $severity['icon'] = '
          ' . $severity['title'] . '
          '; + // The requirement's 'value' key is optional, provide a default value. + $requirement['value'] = isset($requirement['value']) ? $requirement['value'] : ''; // Output table row(s) if (!empty($requirement['description'])) { @@ -2631,8 +2666,8 @@ } $row[] = array('data' => $description, 'class' => array('description')); // Display links (such as help or permissions) in their own columns. - foreach (array('help', 'permissions', 'configure') as $key) { - $row[] = array('data' => drupal_render($module['links'][$key]), 'class' => array($key)); + foreach (array('help', 'permissions', 'configure') as $link_type) { + $row[] = array('data' => drupal_render($module['links'][$link_type]), 'class' => array($link_type)); } $rows[] = $row; } @@ -2860,13 +2895,14 @@ * Allow users to add additional date formats. */ function system_configure_date_formats_form($form, &$form_state, $dfid = 0) { + $ajax_path = 'admin/config/regional/date-time/formats/lookup'; $js_settings = array( 'type' => 'setting', 'data' => array( 'dateTime' => array( 'date-format' => array( 'text' => t('Displayed as'), - 'lookup' => url('admin/config/regional/date-time/formats/lookup'), + 'lookup' => url($ajax_path, array('query' => array('token' => drupal_get_token($ajax_path)))), ), ), ), diff -Naur drupal-7.23/modules/system/system.api.php drupal-7.66/modules/system/system.api.php --- drupal-7.23/modules/system/system.api.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -113,21 +113,21 @@ * translation handlers. Array keys are the module names, array values * can be any data structure the module uses to provide field translation. * Any empty value disallows the module to appear as a translation handler. - * - entity keys: An array describing how the Field API can extract the - * information it needs from the objects of the type. Elements: + * - entity keys: (optional) An array describing how the Field API can extract + * the information it needs from the objects of the type. Elements: * - id: The name of the property that contains the primary id of the * entity. Every entity object passed to the Field API must have this * property and its value must be numeric. * - revision: The name of the property that contains the revision id of * the entity. The Field API assumes that all revision ids are unique * across all entities of a type. This entry can be omitted if the - * entities of this type are not versionable. + * entities of this type are not versionable. Defaults to an empty string. * - bundle: The name of the property that contains the bundle name for the * entity. The bundle name defines which set of fields are attached to * the entity (e.g. what nodes call "content type"). This entry can be * omitted if this entity type exposes a single bundle (all entities have * the same collection of fields). The name of this single bundle will be - * the same as the entity type. + * the same as the entity type. Defaults to an empty string. * - label: The name of the property that contains the entity label. For * example, if the entity's label is located in $entity->subject, then * 'subject' should be specified here. If complex logic is required to @@ -247,7 +247,7 @@ 'custom settings' => FALSE, ), 'search_result' => array( - 'label' => t('Search result'), + 'label' => t('Search result highlighting input'), 'custom settings' => FALSE, ), ); @@ -606,11 +606,13 @@ * @return * An associative array where the key is the queue name and the value is * again an associative array. Possible keys are: - * - 'worker callback': The name of the function to call. It will be called - * with one argument, the item created via DrupalQueue::createItem() in - * hook_cron(). + * - 'worker callback': The name of an implementation of + * callback_queue_worker(). * - 'time': (optional) How much time Drupal should spend on calling this * worker in seconds. Defaults to 15. + * - 'skip on cron': (optional) Set to TRUE to avoid being processed during + * cron runs (for example, if you want to control all queue execution + * manually). * * @see hook_cron() * @see hook_cron_queue_info_alter() @@ -873,7 +875,7 @@ * * @see ajax_render() */ -function hook_ajax_render_alter($commands) { +function hook_ajax_render_alter(&$commands) { // Inject any new status messages into the content area. $commands[] = ajax_command_prepend('#block-system-main .content', theme('status_messages')); } @@ -1795,6 +1797,8 @@ * the $form_id input matched your module's format for dynamically-generated * form IDs, and if so, act appropriately. * + * Third, forms defined in classes can be defined this way. + * * @param $form_id * The unique string identifying the desired form. * @param $args @@ -1805,19 +1809,22 @@ * @return * An associative array whose keys define form_ids and whose values are an * associative array defining the following keys: - * - callback: The name of the form builder function to invoke. This will be - * used for the base form ID, for example, to target a base form using - * hook_form_BASE_FORM_ID_alter(). + * - callback: The callable returning the form array. If it is the name of + * the form builder function then this will be used for the base + * form ID, for example, to target a base form using + * hook_form_BASE_FORM_ID_alter(). Otherwise use the base_form_id key to + * define the base form ID. * - callback arguments: (optional) Additional arguments to pass to the * function defined in 'callback', which are prepended to $args. - * - wrapper_callback: (optional) The name of a form builder function to - * invoke before the form builder defined in 'callback' is invoked. This - * wrapper callback may prepopulate the $form array with form elements, - * which will then be already contained in the $form that is passed on to - * the form builder defined in 'callback'. For example, a wrapper callback - * could setup wizard-alike form buttons that are the same for a variety of - * forms that belong to the wizard, which all share the same wrapper - * callback. + * - base_form_id: The base form ID can be specified explicitly. This is + * required when callback is not the name of a function. + * - wrapper_callback: (optional) Any callable to invoke before the form + * builder defined in 'callback' is invoked. This wrapper callback may + * prepopulate the $form array with form elements, which will then be + * already contained in the $form that is passed on to the form builder + * defined in 'callback'. For example, a wrapper callback could setup + * wizard-like form buttons that are the same for a variety of forms that + * belong to the wizard, which all share the same wrapper callback. */ function hook_forms($form_id, $args) { // Simply reroute the (non-existing) $form_id 'mymodule_first_form' to @@ -1841,6 +1848,15 @@ 'wrapper_callback' => 'mymodule_main_form_wrapper', ); + // Build a form with a static class callback. + $forms['mymodule_class_generated_form'] = array( + // This will call: MyClass::generateMainForm(). + 'callback' => array('MyClass', 'generateMainForm'), + // The base_form_id is required when the callback is a static function in + // a class. This can also be used to keep newer code backwards compatible. + 'base_form_id' => 'mymodule_main_form', + ); + return $forms; } @@ -1872,8 +1888,8 @@ * * This hook is not run on cached pages. * - * To add CSS or JS that should be present on all pages, modules should not - * implement this hook, but declare these files in their .info file. + * To add CSS or JS files that should be present on all pages, modules should + * not implement this hook, but declare these files in their .info file. * * @see hook_boot() */ @@ -1888,9 +1904,8 @@ /** * Define image toolkits provided by this module. * - * The file which includes each toolkit's functions must be declared as part of - * the files array in the module .info file so that the registry will find and - * parse it. + * The file which includes each toolkit's functions must be included in this + * hook. * * The toolkit's functions must be named image_toolkitname_operation(). * where the operation may be: @@ -2036,6 +2051,22 @@ } /** + * Return additional theme engines provided by modules. + * + * This hook is invoked from _system_rebuild_theme_data() and allows modules to + * register additional theme engines outside of the regular 'themes/engines' + * directories of a Drupal installation. + * + * @return + * An associative array. Each key is the system name of a theme engine and + * each value is the corresponding path to the theme engine's .engine file. + */ +function hook_system_theme_engine_info() { + $theme_engines['izumi'] = drupal_get_path('module', 'mymodule') . '/izumi/izumi.engine'; + return $theme_engines; +} + +/** * Alter the information parsed from module and theme .info files * * This hook is invoked in _system_rebuild_module_data() and in @@ -2108,6 +2139,61 @@ } /** + * Provide online user help. + * + * By implementing hook_help(), a module can make documentation available to + * the user for the module as a whole, or for specific paths. Help for + * developers should usually be provided via function header comments in the + * code, or in special API example files. + * + * The page-specific help information provided by this hook appears as a system + * help block on that page. The module overview help information is displayed + * by the Help module. It can be accessed from the page at admin/help or from + * the Modules page. + * + * For detailed usage examples of: + * - Module overview help, see node_help(). Module overview help should follow + * @link https://drupal.org/node/632280 the standard help template. @endlink + * - Page-specific help with simple paths, see dashboard_help(). + * - Page-specific help using wildcards in path and $arg, see node_help() + * and block_help(). + * + * @param $path + * The router menu path, as defined in hook_menu(), for the help that is + * being requested; e.g., 'admin/people' or 'user/register'. If the router + * path includes a wildcard, then this will appear in $path as %, even if it + * is a named %autoloader wildcard in the hook_menu() implementation; for + * example, node pages would have $path equal to 'node/%' or 'node/%/view'. + * For the help page for the module as a whole, $path will have the value + * 'admin/help#module_name', where 'module_name" is the machine name of your + * module. + * @param $arg + * An array that corresponds to the return value of the arg() function, for + * modules that want to provide help that is specific to certain values + * of wildcards in $path. For example, you could provide help for the path + * 'user/1' by looking for the path 'user/%' and $arg[1] == '1'. This given + * array should always be used rather than directly invoking arg(), because + * your hook implementation may be called for other purposes besides building + * the current page's help. Note that depending on which module is invoking + * hook_help, $arg may contain only empty strings. Regardless, $arg[0] to + * $arg[11] will always be set. + * + * @return + * A localized string containing the help text. + */ +function hook_help($path, $arg) { + switch ($path) { + // Main module help for the block module + case 'admin/help#block': + return '

          ' . 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. * * The implementations declared by this hook have two purposes: either they @@ -2576,6 +2662,8 @@ * module_enable() for a detailed description of the order in which install and * enable hooks are invoked. * + * This hook should be implemented in a .module file, not in an .install file. + * * @param $modules * An array of the modules that were installed. * @@ -3098,37 +3186,41 @@ /** * Define the current version of the database schema. * - * A Drupal schema definition is an array structure representing one or - * more tables and their related keys and indexes. A schema is defined by + * A Drupal schema definition is an array structure representing one or more + * tables and their related keys and indexes. A schema is defined by * hook_schema() which must live in your module's .install file. * - * This hook is called at install and uninstall time, and in the latter - * case, it cannot rely on the .module file being loaded or hooks being known. - * If the .module file is needed, it may be loaded with drupal_load(). - * - * The tables declared by this hook will be automatically created when - * the module is first enabled, and removed when the module is uninstalled. - * This happens before hook_install() is invoked, and after hook_uninstall() - * is invoked, respectively. + * This hook is called at install and uninstall time, and in the latter case, it + * cannot rely on the .module file being loaded or hooks being known. If the + * .module file is needed, it may be loaded with drupal_load(). + * + * The tables declared by this hook will be automatically created when the + * module is first enabled, and removed when the module is uninstalled. This + * happens before hook_install() is invoked, and after hook_uninstall() is + * invoked, respectively. * * By declaring the tables used by your module via an implementation of * hook_schema(), these tables will be available on all supported database * engines. You don't have to deal with the different SQL dialects for table * creation and alteration of the supported database engines. * - * See the Schema API Handbook at http://drupal.org/node/146843 for - * details on schema definition structures. + * See the Schema API Handbook at http://drupal.org/node/146843 for details on + * schema definition structures. Note that foreign key definitions are for + * documentation purposes only; foreign keys are not created in the database, + * nor are they enforced by Drupal. * - * @return + * @return array * A schema definition structure array. For each element of the * array, the key is a table name and the value is a table structure * definition. * + * @see hook_schema_alter() + * * @ingroup schemaapi */ function hook_schema() { $schema['node'] = array( - // example (partial) specification for table "node" + // Example (partial) specification for table "node". 'description' => 'The base table for nodes.', 'fields' => array( 'nid' => array( @@ -3167,6 +3259,8 @@ 'nid_vid' => array('nid', 'vid'), 'vid' => array('vid'), ), + // For documentation purposes only; foreign keys are not created in the + // database. 'foreign keys' => array( 'node_revision' => array( 'table' => 'node_revision', @@ -3367,24 +3461,31 @@ * 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. @@ -3628,8 +3729,9 @@ * * Any tasks you define here will be run, in order, after the installer has * finished the site configuration step but before it has moved on to the - * final import of languages and the end of the installation. You can have any - * number of custom tasks to perform during this phase. + * final import of languages and the end of the installation. This is invoked + * by install_tasks(). You can have any number of custom tasks to perform + * during this phase. * * Each task you define here corresponds to a callback function which you must * separately define and which is called when your task is run. This function @@ -3722,6 +3824,8 @@ * * @see install_state_defaults() * @see batch_set() + * @see hook_install_tasks_alter() + * @see install_tasks() */ function hook_install_tasks(&$install_state) { // Here, we define a variable to allow tasks to indicate that a particular, @@ -3824,6 +3928,8 @@ /** * Alter the full list of installation tasks. * + * This hook is invoked on the install profile in install_tasks(). + * * @param $tasks * An array of all available installation tasks, including those provided by * Drupal core. You can modify this array to change or replace any part of @@ -3831,6 +3937,9 @@ * is selected. * @param $install_state * An array of information about the current installation state. + * + * @see hook_install_tasks() + * @see install_tasks() */ function hook_install_tasks_alter(&$tasks, $install_state) { // Replace the "Choose language" installation task provided by Drupal core @@ -4095,7 +4204,7 @@ * declared in an implementation of hook_date_format_types(). * - 'format': A PHP date format string to use when formatting dates. It * can contain any of the formatting options described at - * http://php.net/manual/en/function.date.php + * http://php.net/manual/function.date.php * - 'locales': (optional) An array of 2 and 5 character locale codes, * defining which locales this format applies to (for example, 'en', * 'en-us', etc.). If your date format is not language-specific, leave this @@ -4718,6 +4827,28 @@ */ /** + * Work on a single queue item. + * + * Callback for hook_cron_queue_info(). + * + * @param $queue_item_data + * The data that was passed to DrupalQueueInterface::createItem() when the + * item was queued. + * + * @throws Exception + * The worker callback may throw an exception to indicate there was a problem. + * The cron process will log the exception, and leave the item in the queue to + * be processed again later. + * + * @see drupal_cron_run() + */ +function callback_queue_worker($queue_item_data) { + $node = node_load($queue_item_data); + $node->title = 'Updated title'; + node_save($node); +} + +/** * Return the URI for an entity. * * Callback for hook_entity_info(). diff -Naur drupal-7.23/modules/system/system.base-rtl.css drupal-7.66/modules/system/system.base-rtl.css --- drupal-7.23/modules/system/system.base-rtl.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.base-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -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 -Naur drupal-7.23/modules/system/system.base.css drupal-7.66/modules/system/system.base.css --- drupal-7.23/modules/system/system.base.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.base.css 2019-04-17 22:20:46.000000000 +0200 @@ -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 @@ 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 -Naur drupal-7.23/modules/system/system.info drupal-7.66/modules/system/system.info --- drupal-7.23/modules/system/system.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/system/system.info 2019-04-17 22:39:36.000000000 +0200 @@ -12,8 +12,7 @@ required = TRUE configure = admin/config/system -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/system/system.install drupal-7.66/modules/system/system.install --- drupal-7.23/modules/system/system.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.install 2019-04-17 22:20:46.000000000 +0200 @@ -6,12 +6,7 @@ */ /** - * Test and report Drupal installation requirements. - * - * @param $phase - * The current system installation phase. - * @return - * An array of system requirements. + * Implements hook_requirements(). */ function system_requirements($phase) { global $base_url; @@ -165,7 +160,7 @@ if (empty($drivers)) { $database_ok = FALSE; $pdo_message = $t('Your web server does not appear to support any common PDO database extensions. Check with your hosting provider to see if they support PDO (PHP Data Objects) and offer any databases that Drupal supports.', array( - '@drupal-databases' => 'http://drupal.org/node/270#database', + '@drupal-databases' => 'https://www.drupal.org/requirements/database', )); } // Make sure the native PDO extension is available, not the older PEAR @@ -201,6 +196,12 @@ ); } + // Test database-specific multi-byte UTF-8 related requirements. + $charset_requirements = _system_check_db_utf8mb4_requirements($phase); + if (!empty($charset_requirements)) { + $requirements['database_charset'] = $charset_requirements; + } + // Test PHP memory_limit $memory_limit = ini_get('memory_limit'); $requirements['php_memory_limit'] = array( @@ -258,6 +259,39 @@ $requirements['settings.php']['title'] = $t('Configuration file'); } + // Test the contents of the .htaccess files. + if ($phase == 'runtime') { + // Try to write the .htaccess files first, to prevent false alarms in case + // (for example) the /tmp directory was wiped. + file_ensure_htaccess(); + $htaccess_files['public://.htaccess'] = array( + 'title' => $t('Public files directory'), + 'directory' => variable_get('file_public_path', conf_path() . '/files'), + ); + if ($private_files_directory = variable_get('file_private_path')) { + $htaccess_files['private://.htaccess'] = array( + 'title' => $t('Private files directory'), + 'directory' => $private_files_directory, + ); + } + $htaccess_files['temporary://.htaccess'] = array( + 'title' => $t('Temporary files directory'), + 'directory' => variable_get('file_temporary_path', file_directory_temp()), + ); + foreach ($htaccess_files as $htaccess_file => $info) { + // Check for the string which was added to the recommended .htaccess file + // in the latest security update. + if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) { + $requirements[$htaccess_file] = array( + 'title' => $info['title'], + 'value' => $t('Not fully protected'), + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('See @url for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', array('@url' => 'http://drupal.org/SA-CORE-2013-003', '%directory' => $info['directory'])), + ); + } + } + } + // Report cron status. if ($phase == 'runtime') { // Cron warning threshold defaults to two days. @@ -490,6 +524,75 @@ } /** + * Checks whether the requirements for multi-byte UTF-8 support are met. + * + * @param string $phase + * The hook_requirements() stage. + * + * @return array + * A requirements array with the result of the charset check. + */ +function _system_check_db_utf8mb4_requirements($phase) { + global $install_state; + // In the requirements check of the installer, skip the utf8mb4 check unless + // the database connection info has been preconfigured by hand with valid + // information before running the installer, as otherwise we cannot get a + // valid database connection object. + if (isset($install_state['settings_verified']) && !$install_state['settings_verified']) { + return array(); + } + + $connection = Database::getConnection(); + $t = get_t(); + $requirements['title'] = $t('Database 4 byte UTF-8 support'); + + $utf8mb4_configurable = $connection->utf8mb4IsConfigurable(); + $utf8mb4_active = $connection->utf8mb4IsActive(); + $utf8mb4_supported = $connection->utf8mb4IsSupported(); + $driver = $connection->driver(); + $documentation_url = 'https://www.drupal.org/node/2754539'; + + if ($utf8mb4_active) { + if ($utf8mb4_supported) { + if ($phase != 'install' && $utf8mb4_configurable && !variable_get('drupal_all_databases_are_utf8mb4', FALSE)) { + // Supported, active, and configurable, but not all database tables + // have been converted yet. + $requirements['value'] = $t('Enabled, but database tables need conversion'); + $requirements['description'] = $t('Please convert all database tables to utf8mb4 prior to enabling it in settings.php. See the documentation on adding 4 byte UTF-8 support for more information.', array('@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_ERROR; + } + else { + // Supported, active. + $requirements['value'] = $t('Enabled'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is enabled.', array('@driver' => $driver)); + $requirements['severity'] = REQUIREMENT_OK; + } + } + else { + // Not supported, active. + $requirements['value'] = $t('Not supported'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is activated, but not supported on your system. Please turn this off in settings.php, or ensure that all database-related requirements are met. See the documentation on adding 4 byte UTF-8 support for more information.', array('@driver' => $driver, '@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_ERROR; + } + } + else { + if ($utf8mb4_supported) { + // Supported, not active. + $requirements['value'] = $t('Not enabled'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is not activated, but it is supported on your system. It is recommended that you enable this to allow 4-byte UTF-8 input such as emojis, Asian symbols and mathematical symbols to be stored correctly. See the documentation on adding 4 byte UTF-8 support for more information.', array('@driver' => $driver, '@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_INFO; + } + else { + // Not supported, not active. + $requirements['value'] = $t('Disabled'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is disabled. See the documentation on adding 4 byte UTF-8 support for more information.', array('@driver' => $driver, '@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_INFO; + } + } + return $requirements; +} + +/** * Implements hook_install(). */ function system_install() { @@ -504,6 +607,9 @@ module_list(TRUE); module_implements('', FALSE, TRUE); + // Ensure the schema versions are not based on a previous module list. + drupal_static_reset('drupal_get_schema_versions'); + // Load system theme data appropriately. system_rebuild_theme_data(); @@ -516,7 +622,7 @@ ->execute(); // Populate the cron key variable. - $cron_key = drupal_hash_base64(drupal_random_bytes(55)); + $cron_key = drupal_random_key(); variable_set('cron_key', $cron_key); } @@ -772,6 +878,7 @@ 'type' => 'varchar', 'length' => 100, 'not null' => TRUE, + 'binary' => TRUE, ), 'type' => array( 'description' => 'The date format type, e.g. medium.', @@ -830,6 +937,7 @@ 'filesize' => array( 'description' => 'The size of the file in bytes.', 'type' => 'int', + 'size' => 'big', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, @@ -1743,7 +1851,7 @@ * Generate a cron key and save it in the variables table. */ function system_update_7001() { - variable_set('cron_key', drupal_hash_base64(drupal_random_bytes(55))); + variable_set('cron_key', drupal_random_key()); } /** @@ -2762,7 +2870,7 @@ if (!db_table_exists('system_update_7061')) { $table = array( 'description' => t('Stores temporary data for system_update_7061.'), - 'fields' => array('vid' => array('type' => 'int')), + 'fields' => array('vid' => array('type' => 'int', 'not null' => TRUE)), 'primary key' => array('vid'), ); db_create_table('system_update_7061', $table); @@ -2774,6 +2882,16 @@ ->from($query) ->execute(); + // Retrieve a list of duplicate files with the same filepath. Only the + // most-recently uploaded of these will be moved to the new {file_managed} + // table (and all references will be updated to point to it), since + // duplicate file URIs are not allowed in Drupal 7. + // Since the Drupal 6 to 7 upgrade path leaves the {files} table behind + // after it's done, custom or contributed modules which need to migrate + // file references of their own can use a similar query to determine the + // file IDs that duplicate filepaths were mapped to. + $sandbox['duplicate_filepath_fids_to_use'] = db_query("SELECT filepath, MAX(fid) FROM {files} GROUP BY filepath HAVING COUNT(*) > 1")->fetchAllKeyed(); + // Initialize batch update information. $sandbox['progress'] = 0; $sandbox['last_vid_processed'] = -1; @@ -2803,6 +2921,16 @@ continue; } + // If this file has a duplicate filepath, replace it with the + // most-recently uploaded file that has the same filepath. + if (isset($sandbox['duplicate_filepath_fids_to_use'][$file['filepath']]) && $record->fid != $sandbox['duplicate_filepath_fids_to_use'][$file['filepath']]) { + $file = db_select('files', 'f') + ->fields('f', array('fid', 'uid', 'filename', 'filepath', 'filemime', 'filesize', 'status', 'timestamp')) + ->condition('f.fid', $sandbox['duplicate_filepath_fids_to_use'][$file['filepath']]) + ->execute() + ->fetchAssoc(); + } + // Add in the file information from the upload table. $file['description'] = $record->description; $file['display'] = $record->list; @@ -2825,7 +2953,14 @@ // 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. @@ -3107,6 +3242,57 @@ } /** + * Convert the 'filesize' column in {file_managed} to a bigint. + */ +function system_update_7079() { + $spec = array( + 'description' => 'The size of the file in bytes.', + 'type' => 'int', + 'size' => 'big', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ); + db_change_field('file_managed', 'filesize', 'filesize', $spec); +} + +/** + * Convert the 'format' column in {date_format_locale} to case sensitive varchar. + */ +function system_update_7080() { + $spec = array( + 'description' => 'The date format string.', + 'type' => 'varchar', + 'length' => 100, + 'not null' => TRUE, + 'binary' => TRUE, + ); + db_change_field('date_format_locale', 'format', 'format', $spec); +} + +/** + * Remove the Drupal 6 default install profile if it is still in the database. + */ +function system_update_7081() { + // Sites which used the default install profile in Drupal 6 and then updated + // to Drupal 7.44 or earlier will still have a record of this install profile + // in the database that needs to be deleted. + db_delete('system') + ->condition('filename', 'profiles/default/default.profile') + ->condition('type', 'module') + ->condition('status', 0) + ->condition('schema_version', 0) + ->execute(); +} + +/** + * Add 'jquery-extend-3.4.0.js' to the 'jquery' library. + */ +function system_update_7082() { + // Empty update to force a rebuild of hook_library() and JS aggregates. +} + +/** * @} End of "defgroup updates-7.x-extra". * The next series of updates should start at 8000. */ diff -Naur drupal-7.23/modules/system/system.js drupal-7.66/modules/system/system.js --- drupal-7.23/modules/system/system.js 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.js 2019-04-17 22:20:46.000000000 +0200 @@ -105,7 +105,7 @@ // Attach keyup handler to custom format inputs. $('input' + source, context).once('date-time').keyup(function () { var input = $(this); - var url = fieldSettings.lookup + (/\?q=/.test(fieldSettings.lookup) ? '&format=' : '?format=') + encodeURIComponent(input.val()); + var url = fieldSettings.lookup + (/\?/.test(fieldSettings.lookup) ? '&format=' : '?format=') + encodeURIComponent(input.val()); $.getJSON(url, function (data) { $(suffix).empty().append(' ' + fieldSettings.text + ': ' + data + ''); }); diff -Naur drupal-7.23/modules/system/system.mail.inc drupal-7.66/modules/system/system.mail.inc --- drupal-7.23/modules/system/system.mail.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.mail.inc 2019-04-17 22:20:46.000000000 +0200 @@ -31,7 +31,7 @@ /** * Send an e-mail message, using Drupal variables and default settings. * - * @see http://php.net/manual/en/function.mail.php + * @see http://php.net/manual/function.mail.php * @see drupal_mail() * * @param $message @@ -70,7 +70,9 @@ // hosts. The return value of this method will still indicate whether mail // was sent successfully. if (!isset($_SERVER['WINDIR']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') === FALSE) { - if (isset($message['Return-Path']) && !ini_get('safe_mode')) { + // We validate the return path, unless it is equal to the site mail, which + // we assume to be safe. + if (isset($message['Return-Path']) && !ini_get('safe_mode') && (variable_get('site_mail', ini_get('sendmail_from')) === $message['Return-Path'] || self::_isShellSafe($message['Return-Path']))) { // On most non-Windows systems, the "-f" option to the sendmail command // is used to set the Return-Path. There is no space between -f and // the value of the return path. @@ -109,6 +111,36 @@ } return $mail_result; } + + /** + * Disallows potentially unsafe shell characters. + * + * Functionally similar to PHPMailer::isShellSafe() which resulted from + * CVE-2016-10045. Note that escapeshellarg and escapeshellcmd are inadequate + * for this purpose. + * + * @param string $string + * The string to be validated. + * + * @return bool + * True if the string is shell-safe. + * + * @see https://github.com/PHPMailer/PHPMailer/issues/924 + * @see https://github.com/PHPMailer/PHPMailer/blob/v5.2.21/class.phpmailer.php#L1430 + * + * @todo Rename to ::isShellSafe() and/or discuss whether this is the correct + * location for this helper. + */ + protected static function _isShellSafe($string) { + if (escapeshellcmd($string) !== $string || !in_array(escapeshellarg($string), array("'$string'", "\"$string\""))) { + return FALSE; + } + if (preg_match('/[^a-zA-Z0-9@_\-.]/', $string) !== 0) { + return FALSE; + } + return TRUE; + } + } /** diff -Naur drupal-7.23/modules/system/system.module drupal-7.66/modules/system/system.module --- drupal-7.23/modules/system/system.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.module 2019-04-17 22:20:46.000000000 +0200 @@ -242,6 +242,7 @@ ), 'access site reports' => array( 'title' => t('View site reports'), + 'restrict access' => TRUE, ), 'block IP addresses' => array( 'title' => t('Block IP addresses'), @@ -358,7 +359,7 @@ '#size' => 60, '#maxlength' => 128, '#autocomplete_path' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('form_process_autocomplete', 'ajax_process_form'), '#theme' => 'textfield', '#theme_wrappers' => array('form_element'), ); @@ -373,6 +374,9 @@ '#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 @@ '#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, @@ -1175,6 +1182,9 @@ 'version' => '1.4.4', 'js' => array( 'misc/jquery.js' => array('group' => JS_LIBRARY, 'weight' => -20), + // This includes a security fix, so assign a weight that makes this load + // as soon after jquery.js is loaded as possible. + 'misc/jquery-extend-3.4.0.js' => array('group' => JS_LIBRARY, 'weight' => -19), ), ); @@ -2023,7 +2033,6 @@ '#description' => t('Select the desired local time and time zone. Dates and times throughout this site will be displayed using this time zone.'), ); if (!isset($account->timezone) && $account->uid == $user->uid && empty($form_state['input']['timezone'])) { - $form['timezone']['#description'] = t('Your time zone setting will be automatically detected if possible. Confirm the selection and click save.'); $form['timezone']['timezone']['#attributes'] = array('class' => array('timezone-detect')); drupal_add_js('misc/timezone.js'); } @@ -2398,9 +2407,17 @@ 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; + // The "name" key is required, but to avoid a fatal error in the menu system + // we set a reasonable default if it is not provided. + $modules[$key]->info += array('name' => $key); + // Prefix stylesheets and scripts with module path. $path = dirname($module->uri); if (isset($module->info['stylesheets'])) { @@ -2507,6 +2524,16 @@ // Find theme engines $engines = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines'); + // Allow modules to add further theme engines. + if ($module_engines = module_invoke_all('system_theme_engine_info')) { + foreach ($module_engines as $name => $theme_engine_path) { + $engines[$name] = (object) array( + 'uri' => $theme_engine_path, + 'filename' => basename($theme_engine_path), + 'name' => $name, + ); + } + } // Set defaults for theme info. $defaults = array( @@ -2536,6 +2563,14 @@ $themes[$key]->filename = $theme->uri; $themes[$key]->info = drupal_parse_info_file($theme->uri) + $defaults; + // The "name" key is required, but to avoid a fatal error in the menu system + // we set a reasonable default if it is not provided. + $themes[$key]->info += array('name' => $key); + + // 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'; @@ -2683,10 +2718,17 @@ * @param $show * Possible values: REGIONS_ALL or REGIONS_VISIBLE. Visible excludes hidden * regions. - * @return - * An array of regions in the form $region['name'] = 'description'. + * @param bool $labels + * (optional) Boolean to specify whether the human readable machine names + * should be returned or not. Defaults to TRUE, but calling code can set + * this to FALSE for better performance, if it only needs machine names. + * + * @return array + * An associative array of regions in the form $region['name'] = 'description' + * if $labels is set to TRUE, or $region['name'] = 'name', if $labels is set + * to FALSE. */ -function system_region_list($theme_key, $show = REGIONS_ALL) { +function system_region_list($theme_key, $show = REGIONS_ALL, $labels = TRUE) { $themes = list_themes(); if (!isset($themes[$theme_key])) { return array(); @@ -2697,10 +2739,14 @@ // If requested, suppress hidden regions. See block_admin_display_form(). foreach ($info['regions'] as $name => $label) { if ($show == REGIONS_ALL || !isset($info['regions_hidden']) || !in_array($name, $info['regions_hidden'])) { - $list[$name] = t($label); + if ($labels) { + $list[$name] = t($label); + } + else { + $list[$name] = $name; + } } } - return $list; } @@ -2721,16 +2767,27 @@ * * @param $theme * The name of a theme. + * * @return * A string that is the region name. */ function system_default_region($theme) { - $regions = array_keys(system_region_list($theme, REGIONS_VISIBLE)); - return isset($regions[0]) ? $regions[0] : ''; + $regions = system_region_list($theme, REGIONS_VISIBLE, FALSE); + return $regions ? reset($regions) : ''; } /** - * Add default buttons to a form and set its prefix. + * Sets up a form to save information automatically. + * + * This function adds a submit handler and a submit button to a form array. The + * submit function saves all the data in the form, using variable_set(), to + * variables named the same as the keys in the form array. Note that this means + * you should normally prefix your form array keys with your module name, so + * that they are unique when passed into variable_set(). + * + * If you need to manipulate the data in a custom manner, you can either put + * your own submission handler in the form array before calling this function, + * or just use your own submission handler instead of calling this function. * * @param $form * An associative array containing the structure of the form. @@ -2739,6 +2796,7 @@ * The form structure. * * @see system_settings_form_submit() + * * @ingroup forms */ function system_settings_form($form) { @@ -2757,7 +2815,7 @@ } /** - * Execute the system_settings_form. + * Form submission handler for system_settings_form(). * * If you want node type configure style handling of your checkboxes, * add an array_filter value to your form. @@ -2782,7 +2840,7 @@ function _system_sort_requirements($a, $b) { if (!isset($a['weight'])) { if (!isset($b['weight'])) { - return strcmp($a['title'], $b['title']); + return strcasecmp($a['title'], $b['title']); } return -$b['weight']; } @@ -2838,7 +2896,7 @@ // Prepare cancel link. if (isset($_GET['destination'])) { - $options = drupal_parse_url(urldecode($_GET['destination'])); + $options = drupal_parse_url($_GET['destination']); } elseif (is_array($path)) { $options = $path; @@ -3023,8 +3081,20 @@ } } - $core = array('cache', 'cache_path', 'cache_filter', 'cache_page', 'cache_form', 'cache_menu'); - $cache_tables = array_merge(module_invoke_all('flush_caches'), $core); + // Delete expired cache entries. + // Avoid invoking hook_flush_cashes() on every cron run because some modules + // use this hook to perform expensive rebuilding operations (which are only + // designed to happen on full cache clears), rather than just returning a + // list of cache tables to be cleared. + $cache_object = cache_get('system_cache_tables'); + if (empty($cache_object)) { + $core = array('cache', 'cache_path', 'cache_filter', 'cache_page', 'cache_form', 'cache_menu'); + $cache_tables = array_merge(module_invoke_all('flush_caches'), $core); + cache_set('system_cache_tables', $cache_tables); + } + else { + $cache_tables = $cache_object->data; + } foreach ($cache_tables as $table) { cache_clear_all(NULL, $table); } @@ -3272,7 +3342,7 @@ $form['url'] = array( '#type' => 'textfield', '#title' => t('URL'), - '#description' => t('The URL to which the user should be redirected. This can be an internal URL like node/1234 or an external URL like http://drupal.org.'), + '#description' => t('The URL to which the user should be redirected. This can be an internal path like node/1234 or an external URL like http://example.com.'), '#default_value' => isset($context['url']) ? $context['url'] : '', '#required' => TRUE, ); @@ -3309,7 +3379,8 @@ */ function system_block_ip_action() { $ip = ip_address(); - db_insert('blocked_ips') + db_merge('blocked_ips') + ->key(array('ip' => $ip)) ->fields(array('ip' => $ip)) ->execute(); watchdog('action', 'Banned IP address %ip', array('%ip' => $ip)); @@ -3374,7 +3445,7 @@ * @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')) . ''; } /** @@ -3411,30 +3482,32 @@ /** * Attempts to get a file using drupal_http_request and to store it locally. * - * @param $url + * @param string $url * The URL of the file to grab. - * - * @param $destination + * @param string $destination * Stream wrapper URI specifying where the file should be placed. If a * directory path is provided, the file is saved into that directory under * its original name. If the path contains a filename as well, that one will * be used instead. * If this value is omitted, the site's default files scheme will be used, * usually "public://". - * - * @param $managed boolean + * @param bool $managed * If this is set to TRUE, the file API hooks will be invoked and the file is * registered in the database. - * - * @param $replace boolean + * @param int $replace * Replace behavior when the destination file already exists: * - FILE_EXISTS_REPLACE: Replace the existing file. * - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is * unique. * - FILE_EXISTS_ERROR: Do nothing and return FALSE. * - * @return - * On success the location the file was saved to, FALSE on failure. + * @return mixed + * One of these possibilities: + * - If it succeeds and $managed is FALSE, the location where the file was + * saved. + * - If it succeeds and $managed is TRUE, a \Drupal\file\FileInterface + * object which describes the file. + * - If it fails, FALSE. */ function system_retrieve_file($url, $destination = NULL, $managed = FALSE, $replace = FILE_EXISTS_RENAME) { $parsed_url = parse_url($url); @@ -3469,8 +3542,7 @@ function system_page_alter(&$page) { // Find all non-empty page regions, and add a theme wrapper function that // allows them to be consistently themed. - $regions = system_region_list($GLOBALS['theme']); - foreach (array_keys($regions) as $region) { + foreach (system_region_list($GLOBALS['theme'], REGIONS_ALL, FALSE) as $region) { if (!empty($page[$region])) { $page[$region]['#theme_wrappers'][] = 'region'; $page[$region]['#region'] = $region; diff -Naur drupal-7.23/modules/system/system.queue.inc drupal-7.66/modules/system/system.queue.inc --- drupal-7.23/modules/system/system.queue.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.queue.inc 2019-04-17 22:20:46.000000000 +0200 @@ -231,7 +231,7 @@ // until an item is successfully claimed or we are reasonably sure there // are no unclaimed items left. while (TRUE) { - $item = db_query_range('SELECT data, item_id FROM {queue} q WHERE expire = 0 AND name = :name ORDER BY created ASC', 0, 1, array(':name' => $this->name))->fetchObject(); + $item = db_query_range('SELECT data, item_id FROM {queue} q WHERE expire = 0 AND name = :name ORDER BY created, item_id ASC', 0, 1, array(':name' => $this->name))->fetchObject(); if ($item) { // Try to update the item. Only one thread can succeed in UPDATEing the // same row. We cannot rely on REQUEST_TIME because items might be @@ -326,6 +326,7 @@ $item->created = time(); $item->expire = 0; $this->queue[$item->item_id] = $item; + return TRUE; } public function numberOfItems() { diff -Naur drupal-7.23/modules/system/system.tar.inc drupal-7.66/modules/system/system.tar.inc --- drupal-7.23/modules/system/system.tar.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.tar.inc 2019-04-17 22:20:46.000000000 +0200 @@ -30,81 +30,148 @@ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * - * @category File_Formats - * @package Archive_Tar - * @author Vincent Blavet - * @copyright 1997-2008 The Authors - * @license http://www.opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: Id: Tar.php,v 1.43 2008/10/30 17:58:42 dufuz Exp - * @link http://pear.php.net/package/Archive_Tar + * @category File_Formats + * @package Archive_Tar + * @author Vincent Blavet + * @copyright 1997-2010 The Authors + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id$ + * @link http://pear.php.net/package/Archive_Tar + */ + + /** + * Note on Drupal 8 porting. + * This file origin is Tar.php, release 1.4.5 (stable) with some code + * from PEAR.php, release 1.10.5 (stable) both at http://pear.php.net. + * To simplify future porting from pear of this file, you should not + * do cosmetic or other non significant changes to this file. + * The following changes have been done: + * Added namespace Drupal\Core\Archiver. + * Removed require_once 'PEAR.php'. + * Added defintion of OS_WINDOWS taken from PEAR.php. + * Renamed class to ArchiveTar. + * Removed extends PEAR from class. + * Removed call parent:: __construct(). + * Changed PEAR::loadExtension($extname) to this->loadExtension($extname). + * Added function loadExtension() taken from PEAR.php. + * Changed all calls of unlink() to drupal_unlink(). + * Changed $this->error_object = &$this->raiseError($p_message) + * to throw new \Exception($p_message). */ -//require_once 'PEAR.php'; -// -// -define ('ARCHIVE_TAR_ATT_SEPARATOR', 90001); -define ('ARCHIVE_TAR_END_BLOCK', pack("a512", '')); + /** + * Note on Drupal 7 backporting from Drupal 8. + * File origin is core/lib/Drupal/Core/Archiver/ArchiveTar.php from Drupal 8. + * The following changes have been done: + * Removed namespace Drupal\Core\Archiver. + * Renamed class to Archive_Tar. + * Changed \Exception to Exception. + */ + + +// Drupal removal require_once 'PEAR.php'. + +// Drupal addition OS_WINDOWS as defined in PEAR.php. +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); +} else { + define('OS_WINDOWS', false); +} + +define('ARCHIVE_TAR_ATT_SEPARATOR', 90001); +define('ARCHIVE_TAR_END_BLOCK', pack("a512", '')); + +if (!function_exists('gzopen') && function_exists('gzopen64')) { + function gzopen($filename, $mode, $use_include_path = 0) + { + return gzopen64($filename, $mode, $use_include_path); + } +} + +if (!function_exists('gztell') && function_exists('gztell64')) { + function gztell($zp) + { + return gztell64($zp); + } +} + +if (!function_exists('gzseek') && function_exists('gzseek64')) { + function gzseek($zp, $offset, $whence = SEEK_SET) + { + return gzseek64($zp, $offset, $whence); + } +} /** -* Creates a (compressed) Tar archive -* -* @author Vincent Blavet -* @version Revision: 1.43 -* @license http://www.opensource.org/licenses/bsd-license.php New BSD License -* @package Archive_Tar -*/ -class Archive_Tar // extends PEAR + * Creates a (compressed) Tar archive + * + * @package Archive_Tar + * @author Vincent Blavet + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version $Revision$ + */ +// Drupal change class Archive_Tar extends PEAR. +class Archive_Tar { /** - * @var string Name of the Tar - */ - var $_tarname=''; + * @var string Name of the Tar + */ + public $_tarname = ''; /** - * @var boolean if true, the Tar file will be gzipped - */ - var $_compress=false; + * @var boolean if true, the Tar file will be gzipped + */ + public $_compress = false; /** - * @var string Type of compression : 'none', 'gz' or 'bz2' - */ - var $_compress_type='none'; + * @var string Type of compression : 'none', 'gz', 'bz2' or 'lzma2' + */ + public $_compress_type = 'none'; /** - * @var string Explode separator - */ - var $_separator=' '; + * @var string Explode separator + */ + public $_separator = ' '; /** - * @var file descriptor - */ - var $_file=0; + * @var file descriptor + */ + public $_file = 0; /** - * @var string Local Tar name of a remote Tar (http:// or ftp://) - */ - var $_temp_tarname=''; + * @var string Local Tar name of a remote Tar (http:// or ftp://) + */ + public $_temp_tarname = ''; - // {{{ constructor /** - * Archive_Tar Class constructor. This flavour of the constructor only - * declare a new Archive_Tar object, identifying it by the name of the - * tar file. - * If the compress argument is set the tar will be read or created as a - * gzip or bz2 compressed TAR file. - * - * @param string $p_tarname The name of the tar archive to create - * @param string $p_compress can be null, 'gz' or 'bz2'. This - * parameter indicates if gzip or bz2 compression - * is required. For compatibility reason the - * boolean value 'true' means 'gz'. - * @access public - */ -// function Archive_Tar($p_tarname, $p_compress = null) - function __construct($p_tarname, $p_compress = null) + * @var string regular expression for ignoring files or directories + */ + public $_ignore_regexp = ''; + + /** + * @var object PEAR_Error object + */ + public $error_object = null; + + /** + * Archive_Tar Class constructor. This flavour of the constructor only + * declare a new Archive_Tar object, identifying it by the name of the + * tar file. + * If the compress argument is set the tar will be read or created as a + * gzip or bz2 compressed TAR file. + * + * @param string $p_tarname The name of the tar archive to create + * @param string $p_compress can be null, 'gz', 'bz2' or 'lzma2'. This + * parameter indicates if gzip, bz2 or lzma2 compression + * is required. For compatibility reason the + * boolean value 'true' means 'gz'. + * + * @return bool + */ + public function __construct($p_tarname, $p_compress = null) { -// $this->PEAR(); + // Drupal removal parent::__construct(). + $this->_compress = false; $this->_compress_type = 'none'; if (($p_compress === null) || ($p_compress == '')) { @@ -116,10 +183,13 @@ if ($data == "\37\213") { $this->_compress = true; $this->_compress_type = 'gz'; - // No sure it's enought for a magic code .... + // No sure it's enought for a magic code .... } elseif ($data == "BZ") { $this->_compress = true; $this->_compress_type = 'bz2'; + } elseif (file_get_contents($p_tarname, false, null, 1, 4) == '7zXZ') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; } } } else { @@ -129,151 +199,189 @@ $this->_compress = true; $this->_compress_type = 'gz'; } elseif ((substr($p_tarname, -3) == 'bz2') || - (substr($p_tarname, -2) == 'bz')) { + (substr($p_tarname, -2) == 'bz') + ) { $this->_compress = true; $this->_compress_type = 'bz2'; + } else { + if (substr($p_tarname, -2) == 'xz') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; + } } } } else { if (($p_compress === true) || ($p_compress == 'gz')) { $this->_compress = true; $this->_compress_type = 'gz'; - } else if ($p_compress == 'bz2') { - $this->_compress = true; - $this->_compress_type = 'bz2'; } else { - die("Unsupported compression type '$p_compress'\n". - "Supported types are 'gz' and 'bz2'.\n"); - return false; + if ($p_compress == 'bz2') { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } else { + if ($p_compress == 'lzma2') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; + } else { + $this->_error( + "Unsupported compression type '$p_compress'\n" . + "Supported types are 'gz', 'bz2' and 'lzma2'.\n" + ); + return false; + } + } } } $this->_tarname = $p_tarname; - if ($this->_compress) { // assert zlib or bz2 extension support - if ($this->_compress_type == 'gz') + if ($this->_compress) { // assert zlib or bz2 or xz extension support + if ($this->_compress_type == 'gz') { $extname = 'zlib'; - else if ($this->_compress_type == 'bz2') - $extname = 'bz2'; + } else { + if ($this->_compress_type == 'bz2') { + $extname = 'bz2'; + } else { + if ($this->_compress_type == 'lzma2') { + $extname = 'xz'; + } + } + } if (!extension_loaded($extname)) { -// PEAR::loadExtension($extname); + // Drupal change PEAR::loadExtension($extname). $this->loadExtension($extname); } if (!extension_loaded($extname)) { - die("The extension '$extname' couldn't be found.\n". - "Please make sure your version of PHP was built ". - "with '$extname' support.\n"); + $this->_error( + "The extension '$extname' couldn't be found.\n" . + "Please make sure your version of PHP was built " . + "with '$extname' support.\n" + ); return false; } } + + + if (version_compare(PHP_VERSION, "5.5.0-dev") < 0) { + $this->_fmt = "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" . + "a8checksum/a1typeflag/a100link/a6magic/a2version/" . + "a32uname/a32gname/a8devmajor/a8devminor/a131prefix"; + } else { + $this->_fmt = "Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/" . + "Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/" . + "Z32uname/Z32gname/Z8devmajor/Z8devminor/Z131prefix"; + } + + } - // }}} + public function __destruct() + { + $this->_close(); + // ----- Look for a local copy to delete + if ($this->_temp_tarname != '') { + @drupal_unlink($this->_temp_tarname); + } + } + + // Drupal addition from PEAR.php. /** * OS independent PHP extension load. Remember to take care * on the correct extension name for case sensitive OSes. - * The function is the copy of PEAR::loadExtension(). * * @param string $ext The extension name * @return bool Success or not on the dl() call */ - function loadExtension($ext) + public static function loadExtension($ext) { - if (!extension_loaded($ext)) { - // if either returns true dl() will produce a FATAL error, stop that - if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) { - return false; - } + if (extension_loaded($ext)) { + return true; + } - if (OS_WINDOWS) { - $suffix = '.dll'; - } elseif (PHP_OS == 'HP-UX') { - $suffix = '.sl'; - } elseif (PHP_OS == 'AIX') { - $suffix = '.a'; - } elseif (PHP_OS == 'OSX') { - $suffix = '.bundle'; - } else { - $suffix = '.so'; - } + // if either returns true dl() will produce a FATAL error, stop that + if ( + function_exists('dl') === false || + ini_get('enable_dl') != 1 + ) { + return false; + } - return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; } - return true; + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); } - // {{{ destructor -// function _Archive_Tar() - function __destruct() - { - $this->_close(); - // ----- Look for a local copy to delete - if ($this->_temp_tarname != '') - @drupal_unlink($this->_temp_tarname); -// $this->_PEAR(); - } - // }}} - - // {{{ create() /** - * This method creates the archive file and add the files / directories - * that are listed in $p_filelist. - * If a file with the same name exist and is writable, it is replaced - * by the new tar. - * The method return false and a PEAR error text. - * The $p_filelist parameter can be an array of string, each string - * representing a filename or a directory name with their path if - * needed. It can also be a single string with names separated by a - * single blank. - * For each directory added in the archive, the files and - * sub-directories are also added. - * See also createModify() method for more details. - * - * @param array $p_filelist An array of filenames and directory names, or a - * single string with names separated by a single - * blank space. - * @return true on success, false on error. - * @see createModify() - * @access public - */ - function create($p_filelist) + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If a file with the same name exist and is writable, it is replaced + * by the new tar. + * The method return false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * For each directory added in the archive, the files and + * sub-directories are also added. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * + * @return true on success, false on error. + * @see createModify() + */ + public function create($p_filelist) { return $this->createModify($p_filelist, '', ''); } - // }}} - // {{{ add() /** - * This method add the files / directories that are listed in $p_filelist in - * the archive. If the archive does not exist it is created. - * The method return false and a PEAR error text. - * The files and directories listed are only added at the end of the archive, - * even if a file with the same name is already archived. - * See also createModify() method for more details. - * - * @param array $p_filelist An array of filenames and directory names, or a - * single string with names separated by a single - * blank space. - * @return true on success, false on error. - * @see createModify() - * @access public - */ - function add($p_filelist) + * This method add the files / directories that are listed in $p_filelist in + * the archive. If the archive does not exist it is created. + * The method return false and a PEAR error text. + * The files and directories listed are only added at the end of the archive, + * even if a file with the same name is already archived. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * + * @return true on success, false on error. + * @see createModify() + * @access public + */ + public function add($p_filelist) { return $this->addModify($p_filelist, '', ''); } - // }}} - // {{{ extract() - function extract($p_path='') + /** + * @param string $p_path + * @param bool $p_preserve + * @return bool + */ + public function extract($p_path = '', $p_preserve = false) { - return $this->extractModify($p_path, ''); + return $this->extractModify($p_path, '', $p_preserve); } - // }}} - // {{{ listContent() - function listContent() + /** + * @return array|int + */ + public function listContent() { $v_list_detail = array(); @@ -287,57 +395,56 @@ return $v_list_detail; } - // }}} - // {{{ createModify() /** - * This method creates the archive file and add the files / directories - * that are listed in $p_filelist. - * If the file already exists and is writable, it is replaced by the - * new tar. It is a create and not an add. If the file exists and is - * read-only or is a directory it is not replaced. The method return - * false and a PEAR error text. - * The $p_filelist parameter can be an array of string, each string - * representing a filename or a directory name with their path if - * needed. It can also be a single string with names separated by a - * single blank. - * The path indicated in $p_remove_dir will be removed from the - * memorized path of each file / directory listed when this path - * exists. By default nothing is removed (empty path '') - * The path indicated in $p_add_dir will be added at the beginning of - * the memorized path of each file / directory listed. However it can - * be set to empty ''. The adding of a path is done after the removing - * of path. - * The path add/remove ability enables the user to prepare an archive - * for extraction in a different path than the origin files are. - * See also addModify() method for file adding properties. - * - * @param array $p_filelist An array of filenames and directory names, - * or a single string with names separated by - * a single blank space. - * @param string $p_add_dir A string which contains a path to be added - * to the memorized path of each element in - * the list. - * @param string $p_remove_dir A string which contains a path to be - * removed from the memorized path of each - * element in the list, when relevant. - * @return boolean true on success, false on error. - * @access public - * @see addModify() - */ - function createModify($p_filelist, $p_add_dir, $p_remove_dir='') + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If the file already exists and is writable, it is replaced by the + * new tar. It is a create and not an add. If the file exists and is + * read-only or is a directory it is not replaced. The method return + * false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * See also addModify() method for file adding properties. + * + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated by + * a single blank space. + * @param string $p_add_dir A string which contains a path to be added + * to the memorized path of each element in + * the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of each + * element in the list, when relevant. + * + * @return boolean true on success, false on error. + * @see addModify() + */ + public function createModify($p_filelist, $p_add_dir, $p_remove_dir = '') { $v_result = true; - if (!$this->_openWrite()) + if (!$this->_openWrite()) { return false; + } if ($p_filelist != '') { - if (is_array($p_filelist)) + if (is_array($p_filelist)) { $v_list = $p_filelist; - elseif (is_string($p_filelist)) + } elseif (is_string($p_filelist)) { $v_list = explode($this->_separator, $p_filelist); - else { + } else { $this->_cleanFile(); $this->_error('Invalid file list'); return false; @@ -349,67 +456,69 @@ if ($v_result) { $this->_writeFooter(); $this->_close(); - } else + } else { $this->_cleanFile(); + } return $v_result; } - // }}} - // {{{ addModify() /** - * This method add the files / directories listed in $p_filelist at the - * end of the existing archive. If the archive does not yet exists it - * is created. - * The $p_filelist parameter can be an array of string, each string - * representing a filename or a directory name with their path if - * needed. It can also be a single string with names separated by a - * single blank. - * The path indicated in $p_remove_dir will be removed from the - * memorized path of each file / directory listed when this path - * exists. By default nothing is removed (empty path '') - * The path indicated in $p_add_dir will be added at the beginning of - * the memorized path of each file / directory listed. However it can - * be set to empty ''. The adding of a path is done after the removing - * of path. - * The path add/remove ability enables the user to prepare an archive - * for extraction in a different path than the origin files are. - * If a file/dir is already in the archive it will only be added at the - * end of the archive. There is no update of the existing archived - * file/dir. However while extracting the archive, the last file will - * replace the first one. This results in a none optimization of the - * archive size. - * If a file/dir does not exist the file/dir is ignored. However an - * error text is send to PEAR error. - * If a file/dir is not readable the file/dir is ignored. However an - * error text is send to PEAR error. - * - * @param array $p_filelist An array of filenames and directory - * names, or a single string with names - * separated by a single blank space. - * @param string $p_add_dir A string which contains a path to be - * added to the memorized path of each - * element in the list. - * @param string $p_remove_dir A string which contains a path to be - * removed from the memorized path of - * each element in the list, when - * relevant. - * @return true on success, false on error. - * @access public - */ - function addModify($p_filelist, $p_add_dir, $p_remove_dir='') + * This method add the files / directories listed in $p_filelist at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * If a file/dir is already in the archive it will only be added at the + * end of the archive. There is no update of the existing archived + * file/dir. However while extracting the archive, the last file will + * replace the first one. This results in a none optimization of the + * archive size. + * If a file/dir does not exist the file/dir is ignored. However an + * error text is send to PEAR error. + * If a file/dir is not readable the file/dir is ignored. However an + * error text is send to PEAR error. + * + * @param array $p_filelist An array of filenames and directory + * names, or a single string with names + * separated by a single blank space. + * @param string $p_add_dir A string which contains a path to be + * added to the memorized path of each + * element in the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of + * each element in the list, when + * relevant. + * + * @return true on success, false on error. + */ + public function addModify($p_filelist, $p_add_dir, $p_remove_dir = '') { $v_result = true; - if (!$this->_isArchive()) - $v_result = $this->createModify($p_filelist, $p_add_dir, - $p_remove_dir); - else { - if (is_array($p_filelist)) + if (!$this->_isArchive()) { + $v_result = $this->createModify( + $p_filelist, + $p_add_dir, + $p_remove_dir + ); + } else { + if (is_array($p_filelist)) { $v_list = $p_filelist; - elseif (is_string($p_filelist)) + } elseif (is_string($p_filelist)) { $v_list = explode($this->_separator, $p_filelist); - else { + } else { $this->_error('Invalid file list'); return false; } @@ -419,24 +528,41 @@ return $v_result; } - // }}} - // {{{ addString() /** - * This method add a single string as a file at the - * end of the existing archive. If the archive does not yet exists it - * is created. - * - * @param string $p_filename A string which contains the full - * filename path that will be associated - * with the string. - * @param string $p_string The content of the file added in - * the archive. - * @return true on success, false on error. - * @access public - */ - function addString($p_filename, $p_string) + * This method add a single string as a file at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * + * @param string $p_filename A string which contains the full + * filename path that will be associated + * with the string. + * @param string $p_string The content of the file added in + * the archive. + * @param bool|int $p_datetime A custom date/time (unix timestamp) + * for the file (optional). + * @param array $p_params An array of optional params: + * stamp => the datetime (replaces + * datetime above if it exists) + * mode => the permissions on the + * file (600 by default) + * type => is this a link? See the + * tar specification for details. + * (default = regular file) + * uid => the user ID of the file + * (default = 0 = root) + * gid => the group ID of the file + * (default = 0 = root) + * + * @return true on success, false on error. + */ + public function addString($p_filename, $p_string, $p_datetime = false, $p_params = array()) { + $p_stamp = @$p_params["stamp"] ? $p_params["stamp"] : ($p_datetime ? $p_datetime : time()); + $p_mode = @$p_params["mode"] ? $p_params["mode"] : 0600; + $p_type = @$p_params["type"] ? $p_params["type"] : ""; + $p_uid = @$p_params["uid"] ? $p_params["uid"] : ""; + $p_gid = @$p_params["gid"] ? $p_params["gid"] : ""; $v_result = true; if (!$this->_isArchive()) { @@ -446,11 +572,12 @@ $this->_close(); } - if (!$this->_openAppend()) + if (!$this->_openAppend()) { return false; + } // Need to check the get back to the temporary file ? .... - $v_result = $this->_addString($p_filename, $p_string); + $v_result = $this->_addString($p_filename, $p_string, $p_datetime, $p_params); $this->_writeFooter(); @@ -458,131 +585,138 @@ return $v_result; } - // }}} - // {{{ extractModify() /** - * This method extract all the content of the archive in the directory - * indicated by $p_path. When relevant the memorized path of the - * files/dir can be modified by removing the $p_remove_path path at the - * beginning of the file/dir path. - * While extracting a file, if the directory path does not exists it is - * created. - * While extracting a file, if the file already exists it is replaced - * without looking for last modification date. - * While extracting a file, if the file already exists and is write - * protected, the extraction is aborted. - * While extracting a file, if a directory with the same name already - * exists, the extraction is aborted. - * While extracting a directory, if a file with the same name already - * exists, the extraction is aborted. - * While extracting a file/directory if the destination directory exist - * and is write protected, or does not exist but can not be created, - * the extraction is aborted. - * If after extraction an extracted file does not show the correct - * stored file size, the extraction is aborted. - * When the extraction is aborted, a PEAR error text is set and false - * is returned. However the result can be a partial extraction that may - * need to be manually cleaned. - * - * @param string $p_path The path of the directory where the - * files/dir need to by extracted. - * @param string $p_remove_path Part of the memorized path that can be - * removed if present at the beginning of - * the file/dir path. - * @return boolean true on success, false on error. - * @access public - * @see extractList() - */ - function extractModify($p_path, $p_remove_path) + * This method extract all the content of the archive in the directory + * indicated by $p_path. When relevant the memorized path of the + * files/dir can be modified by removing the $p_remove_path path at the + * beginning of the file/dir path. + * While extracting a file, if the directory path does not exists it is + * created. + * While extracting a file, if the file already exists it is replaced + * without looking for last modification date. + * While extracting a file, if the file already exists and is write + * protected, the extraction is aborted. + * While extracting a file, if a directory with the same name already + * exists, the extraction is aborted. + * While extracting a directory, if a file with the same name already + * exists, the extraction is aborted. + * While extracting a file/directory if the destination directory exist + * and is write protected, or does not exist but can not be created, + * the extraction is aborted. + * If after extraction an extracted file does not show the correct + * stored file size, the extraction is aborted. + * When the extraction is aborted, a PEAR error text is set and false + * is returned. However the result can be a partial extraction that may + * need to be manually cleaned. + * + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @param boolean $p_preserve Preserve user/group ownership of files + * + * @return boolean true on success, false on error. + * @see extractList() + */ + public function extractModify($p_path, $p_remove_path, $p_preserve = false) { $v_result = true; $v_list_detail = array(); if ($v_result = $this->_openRead()) { - $v_result = $this->_extractList($p_path, $v_list_detail, - "complete", 0, $p_remove_path); + $v_result = $this->_extractList( + $p_path, + $v_list_detail, + "complete", + 0, + $p_remove_path, + $p_preserve + ); $this->_close(); } return $v_result; } - // }}} - // {{{ extractInString() /** - * This method extract from the archive one file identified by $p_filename. - * The return value is a string with the file content, or NULL on error. - * @param string $p_filename The path of the file to extract in a string. - * @return a string with the file content or NULL. - * @access public - */ - function extractInString($p_filename) + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or NULL on error. + * + * @param string $p_filename The path of the file to extract in a string. + * + * @return a string with the file content or NULL. + */ + public function extractInString($p_filename) { if ($this->_openRead()) { $v_result = $this->_extractInString($p_filename); $this->_close(); } else { - $v_result = NULL; + $v_result = null; } return $v_result; } - // }}} - // {{{ extractList() /** - * This method extract from the archive only the files indicated in the - * $p_filelist. These files are extracted in the current directory or - * in the directory indicated by the optional $p_path parameter. - * If indicated the $p_remove_path can be used in the same way as it is - * used in extractModify() method. - * @param array $p_filelist An array of filenames and directory names, - * or a single string with names separated - * by a single blank space. - * @param string $p_path The path of the directory where the - * files/dir need to by extracted. - * @param string $p_remove_path Part of the memorized path that can be - * removed if present at the beginning of - * the file/dir path. - * @return true on success, false on error. - * @access public - * @see extractModify() - */ - function extractList($p_filelist, $p_path='', $p_remove_path='') + * This method extract from the archive only the files indicated in the + * $p_filelist. These files are extracted in the current directory or + * in the directory indicated by the optional $p_path parameter. + * If indicated the $p_remove_path can be used in the same way as it is + * used in extractModify() method. + * + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated + * by a single blank space. + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @param boolean $p_preserve Preserve user/group ownership of files + * + * @return true on success, false on error. + * @see extractModify() + */ + public function extractList($p_filelist, $p_path = '', $p_remove_path = '', $p_preserve = false) { $v_result = true; $v_list_detail = array(); - if (is_array($p_filelist)) + if (is_array($p_filelist)) { $v_list = $p_filelist; - elseif (is_string($p_filelist)) + } elseif (is_string($p_filelist)) { $v_list = explode($this->_separator, $p_filelist); - else { + } else { $this->_error('Invalid string list'); return false; } if ($v_result = $this->_openRead()) { - $v_result = $this->_extractList($p_path, $v_list_detail, "partial", - $v_list, $p_remove_path); + $v_result = $this->_extractList( + $p_path, + $v_list_detail, + "partial", + $v_list, + $p_remove_path, + $p_preserve + ); $this->_close(); } return $v_result; } - // }}} - // {{{ setAttribute() /** - * This method set specific attributes of the archive. It uses a variable - * list of parameters, in the format attribute code + attribute values : - * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ','); - * @param mixed $argv variable list of attributes and values - * @return true on success, false on error. - * @access public - */ - function setAttribute() + * This method set specific attributes of the archive. It uses a variable + * list of parameters, in the format attribute code + attribute values : + * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ','); + * + * @return true on success, false on error. + */ + public function setAttribute() { $v_result = true; @@ -592,30 +726,32 @@ } // ----- Get the arguments - $v_att_list = &func_get_args(); + $v_att_list = func_get_args(); // ----- Read the attributes - $i=0; - while ($i<$v_size) { + $i = 0; + while ($i < $v_size) { // ----- Look for next option switch ($v_att_list[$i]) { // ----- Look for options that request a string value case ARCHIVE_TAR_ATT_SEPARATOR : // ----- Check the number of parameters - if (($i+1) >= $v_size) { - $this->_error('Invalid number of parameters for ' - .'attribute ARCHIVE_TAR_ATT_SEPARATOR'); + if (($i + 1) >= $v_size) { + $this->_error( + 'Invalid number of parameters for ' + . 'attribute ARCHIVE_TAR_ATT_SEPARATOR' + ); return false; } // ----- Get the value - $this->_separator = $v_att_list[$i+1]; + $this->_separator = $v_att_list[$i + 1]; $i++; - break; + break; default : - $this->_error('Unknow attribute code '.$v_att_list[$i].''); + $this->_error('Unknown attribute code ' . $v_att_list[$i] . ''); return false; } @@ -625,151 +761,248 @@ return $v_result; } - // }}} - // {{{ _error() - function _error($p_message) + /** + * This method sets the regular expression for ignoring files and directories + * at import, for example: + * $arch->setIgnoreRegexp("#CVS|\.svn#"); + * + * @param string $regexp regular expression defining which files or directories to ignore + */ + public function setIgnoreRegexp($regexp) + { + $this->_ignore_regexp = $regexp; + } + + /** + * This method sets the regular expression for ignoring all files and directories + * matching the filenames in the array list at import, for example: + * $arch->setIgnoreList(array('CVS', '.svn', 'bin/tool')); + * + * @param array $list a list of file or directory names to ignore + * + * @access public + */ + public function setIgnoreList($list) + { + $regexp = str_replace(array('#', '.', '^', '$'), array('\#', '\.', '\^', '\$'), $list); + $regexp = '#/' . join('$|/', $list) . '#'; + $this->setIgnoreRegexp($regexp); + } + + /** + * @param string $p_message + */ + public function _error($p_message) { - // ----- To be completed -// $this->raiseError($p_message); + // Drupal change $this->error_object = $this->raiseError($p_message). throw new Exception($p_message); } - // }}} - // {{{ _warning() - function _warning($p_message) + /** + * @param string $p_message + */ + public function _warning($p_message) { - // ----- To be completed -// $this->raiseError($p_message); + // Drupal change $this->error_object = $this->raiseError($p_message). throw new Exception($p_message); } - // }}} - // {{{ _isArchive() - function _isArchive($p_filename=NULL) + /** + * @param string $p_filename + * @return bool + */ + public function _isArchive($p_filename = null) { - if ($p_filename == NULL) { + if ($p_filename == null) { $p_filename = $this->_tarname; } clearstatcache(); return @is_file($p_filename) && !@is_link($p_filename); } - // }}} - // {{{ _openWrite() - function _openWrite() + /** + * @return bool + */ + public function _openWrite() { - if ($this->_compress_type == 'gz') + if ($this->_compress_type == 'gz' && function_exists('gzopen')) { $this->_file = @gzopen($this->_tarname, "wb9"); - else if ($this->_compress_type == 'bz2') - $this->_file = @bzopen($this->_tarname, "w"); - else if ($this->_compress_type == 'none') - $this->_file = @fopen($this->_tarname, "wb"); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2' && function_exists('bzopen')) { + $this->_file = @bzopen($this->_tarname, "w"); + } else { + if ($this->_compress_type == 'lzma2' && function_exists('xzopen')) { + $this->_file = @xzopen($this->_tarname, 'w'); + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($this->_tarname, "wb"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } if ($this->_file == 0) { - $this->_error('Unable to open in write mode \'' - .$this->_tarname.'\''); + $this->_error( + 'Unable to open in write mode \'' + . $this->_tarname . '\'' + ); return false; } return true; } - // }}} - // {{{ _openRead() - function _openRead() + /** + * @return bool + */ + public function _openRead() { if (strtolower(substr($this->_tarname, 0, 7)) == 'http://') { - // ----- Look if a local copy need to be done - if ($this->_temp_tarname == '') { - $this->_temp_tarname = uniqid('tar').'.tmp'; - if (!$v_file_from = @fopen($this->_tarname, 'rb')) { - $this->_error('Unable to open in read mode \'' - .$this->_tarname.'\''); - $this->_temp_tarname = ''; - return false; - } - if (!$v_file_to = @fopen($this->_temp_tarname, 'wb')) { - $this->_error('Unable to open in write mode \'' - .$this->_temp_tarname.'\''); - $this->_temp_tarname = ''; - return false; - } - while ($v_data = @fread($v_file_from, 1024)) - @fwrite($v_file_to, $v_data); - @fclose($v_file_from); - @fclose($v_file_to); - } - - // ----- File to open if the local copy - $v_filename = $this->_temp_tarname; - - } else - // ----- File to open if the normal Tar file - $v_filename = $this->_tarname; + // ----- Look if a local copy need to be done + if ($this->_temp_tarname == '') { + $this->_temp_tarname = uniqid('tar') . '.tmp'; + if (!$v_file_from = @fopen($this->_tarname, 'rb')) { + $this->_error( + 'Unable to open in read mode \'' + . $this->_tarname . '\'' + ); + $this->_temp_tarname = ''; + return false; + } + if (!$v_file_to = @fopen($this->_temp_tarname, 'wb')) { + $this->_error( + 'Unable to open in write mode \'' + . $this->_temp_tarname . '\'' + ); + $this->_temp_tarname = ''; + return false; + } + while ($v_data = @fread($v_file_from, 1024)) { + @fwrite($v_file_to, $v_data); + } + @fclose($v_file_from); + @fclose($v_file_to); + } - if ($this->_compress_type == 'gz') + // ----- File to open if the local copy + $v_filename = $this->_temp_tarname; + } else { + // ----- File to open if the normal Tar file + + $v_filename = $this->_tarname; + } + + if ($this->_compress_type == 'gz' && function_exists('gzopen')) { $this->_file = @gzopen($v_filename, "rb"); - else if ($this->_compress_type == 'bz2') - $this->_file = @bzopen($v_filename, "r"); - else if ($this->_compress_type == 'none') - $this->_file = @fopen($v_filename, "rb"); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2' && function_exists('bzopen')) { + $this->_file = @bzopen($v_filename, "r"); + } else { + if ($this->_compress_type == 'lzma2' && function_exists('xzopen')) { + $this->_file = @xzopen($v_filename, "r"); + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($v_filename, "rb"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } if ($this->_file == 0) { - $this->_error('Unable to open in read mode \''.$v_filename.'\''); + $this->_error('Unable to open in read mode \'' . $v_filename . '\''); return false; } return true; } - // }}} - // {{{ _openReadWrite() - function _openReadWrite() + /** + * @return bool + */ + public function _openReadWrite() { - if ($this->_compress_type == 'gz') + if ($this->_compress_type == 'gz') { $this->_file = @gzopen($this->_tarname, "r+b"); - else if ($this->_compress_type == 'bz2') { - $this->_error('Unable to open bz2 in read/write mode \'' - .$this->_tarname.'\' (limitation of bz2 extension)'); - return false; - } else if ($this->_compress_type == 'none') - $this->_file = @fopen($this->_tarname, "r+b"); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2') { + $this->_error( + 'Unable to open bz2 in read/write mode \'' + . $this->_tarname . '\' (limitation of bz2 extension)' + ); + return false; + } else { + if ($this->_compress_type == 'lzma2') { + $this->_error( + 'Unable to open lzma2 in read/write mode \'' + . $this->_tarname . '\' (limitation of lzma2 extension)' + ); + return false; + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($this->_tarname, "r+b"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } if ($this->_file == 0) { - $this->_error('Unable to open in read/write mode \'' - .$this->_tarname.'\''); + $this->_error( + 'Unable to open in read/write mode \'' + . $this->_tarname . '\'' + ); return false; } return true; } - // }}} - // {{{ _close() - function _close() + /** + * @return bool + */ + public function _close() { //if (isset($this->_file)) { if (is_resource($this->_file)) { - if ($this->_compress_type == 'gz') + if ($this->_compress_type == 'gz') { @gzclose($this->_file); - else if ($this->_compress_type == 'bz2') - @bzclose($this->_file); - else if ($this->_compress_type == 'none') - @fclose($this->_file); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2') { + @bzclose($this->_file); + } else { + if ($this->_compress_type == 'lzma2') { + @xzclose($this->_file); + } else { + if ($this->_compress_type == 'none') { + @fclose($this->_file); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } $this->_file = 0; } @@ -783,348 +1016,495 @@ return true; } - // }}} - // {{{ _cleanFile() - function _cleanFile() + /** + * @return bool + */ + public function _cleanFile() + { + $this->_close(); + + // ----- Look for a local copy + if ($this->_temp_tarname != '') { + // ----- Remove the local copy but not the remote tarname + @drupal_unlink($this->_temp_tarname); + $this->_temp_tarname = ''; + } else { + // ----- Remove the local tarname file + @drupal_unlink($this->_tarname); + } + $this->_tarname = ''; + + return true; + } + + /** + * @param mixed $p_binary_data + * @param integer $p_len + * @return bool + */ + public function _writeBlock($p_binary_data, $p_len = null) + { + if (is_resource($this->_file)) { + if ($p_len === null) { + if ($this->_compress_type == 'gz') { + @gzputs($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'bz2') { + @bzwrite($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'lzma2') { + @xzwrite($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'none') { + @fputs($this->_file, $p_binary_data); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } else { + if ($this->_compress_type == 'gz') { + @gzputs($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'bz2') { + @bzwrite($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'lzma2') { + @xzwrite($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'none') { + @fputs($this->_file, $p_binary_data, $p_len); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + } + return true; + } + + /** + * @return null|string + */ + public function _readBlock() + { + $v_block = null; + if (is_resource($this->_file)) { + if ($this->_compress_type == 'gz') { + $v_block = @gzread($this->_file, 512); + } else { + if ($this->_compress_type == 'bz2') { + $v_block = @bzread($this->_file, 512); + } else { + if ($this->_compress_type == 'lzma2') { + $v_block = @xzread($this->_file, 512); + } else { + if ($this->_compress_type == 'none') { + $v_block = @fread($this->_file, 512); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + return $v_block; + } + + /** + * @param null $p_len + * @return bool + */ + public function _jumpBlock($p_len = null) + { + if (is_resource($this->_file)) { + if ($p_len === null) { + $p_len = 1; + } + + if ($this->_compress_type == 'gz') { + @gzseek($this->_file, gztell($this->_file) + ($p_len * 512)); + } else { + if ($this->_compress_type == 'bz2') { + // ----- Replace missing bztell() and bzseek() + for ($i = 0; $i < $p_len; $i++) { + $this->_readBlock(); + } + } else { + if ($this->_compress_type == 'lzma2') { + // ----- Replace missing xztell() and xzseek() + for ($i = 0; $i < $p_len; $i++) { + $this->_readBlock(); + } + } else { + if ($this->_compress_type == 'none') { + @fseek($this->_file, $p_len * 512, SEEK_CUR); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + return true; + } + + /** + * @return bool + */ + public function _writeFooter() + { + if (is_resource($this->_file)) { + // ----- Write the last 0 filled block for end of archive + $v_binary_data = pack('a1024', ''); + $this->_writeBlock($v_binary_data); + } + return true; + } + + /** + * @param array $p_list + * @param string $p_add_dir + * @param string $p_remove_dir + * @return bool + */ + public function _addList($p_list, $p_add_dir, $p_remove_dir) + { + $v_result = true; + $v_header = array(); + + // ----- Remove potential windows directory separator + $p_add_dir = $this->_translateWinPath($p_add_dir); + $p_remove_dir = $this->_translateWinPath($p_remove_dir, false); + + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if (sizeof($p_list) == 0) { + return true; + } + + foreach ($p_list as $v_filename) { + if (!$v_result) { + break; + } + + // ----- Skip the current tar name + if ($v_filename == $this->_tarname) { + continue; + } + + if ($v_filename == '') { + continue; + } + + // ----- ignore files and directories matching the ignore regular expression + if ($this->_ignore_regexp && preg_match($this->_ignore_regexp, '/' . $v_filename)) { + $this->_warning("File '$v_filename' ignored"); + continue; + } + + if (!file_exists($v_filename) && !is_link($v_filename)) { + $this->_warning("File '$v_filename' does not exist"); + continue; + } + + // ----- Add the file or directory header + if (!$this->_addFile($v_filename, $v_header, $p_add_dir, $p_remove_dir)) { + return false; + } + + if (@is_dir($v_filename) && !@is_link($v_filename)) { + if (!($p_hdir = opendir($v_filename))) { + $this->_warning("Directory '$v_filename' can not be read"); + continue; + } + while (false !== ($p_hitem = readdir($p_hdir))) { + if (($p_hitem != '.') && ($p_hitem != '..')) { + if ($v_filename != ".") { + $p_temp_list[0] = $v_filename . '/' . $p_hitem; + } else { + $p_temp_list[0] = $p_hitem; + } + + $v_result = $this->_addList( + $p_temp_list, + $p_add_dir, + $p_remove_dir + ); + } + } + + unset($p_temp_list); + unset($p_hdir); + unset($p_hitem); + } + } + + return $v_result; + } + + /** + * @param string $p_filename + * @param mixed $p_header + * @param string $p_add_dir + * @param string $p_remove_dir + * @param null $v_stored_filename + * @return bool + */ + public function _addFile($p_filename, &$p_header, $p_add_dir, $p_remove_dir, $v_stored_filename = null) + { + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; + } + + if (is_null($v_stored_filename)) { + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false); + $v_stored_filename = $p_filename; + + if (strcmp($p_filename, $p_remove_dir) == 0) { + return true; + } + + if ($p_remove_dir != '') { + if (substr($p_remove_dir, -1) != '/') { + $p_remove_dir .= '/'; + } + + if (substr($p_filename, 0, strlen($p_remove_dir)) == $p_remove_dir) { + $v_stored_filename = substr($p_filename, strlen($p_remove_dir)); + } + } + + $v_stored_filename = $this->_translateWinPath($v_stored_filename); + if ($p_add_dir != '') { + if (substr($p_add_dir, -1) == '/') { + $v_stored_filename = $p_add_dir . $v_stored_filename; + } else { + $v_stored_filename = $p_add_dir . '/' . $v_stored_filename; + } + } + + $v_stored_filename = $this->_pathReduction($v_stored_filename); + } + + if ($this->_isArchive($p_filename)) { + if (($v_file = @fopen($p_filename, "rb")) == 0) { + $this->_warning( + "Unable to open file '" . $p_filename + . "' in binary read mode" + ); + return true; + } + + if (!$this->_writeHeader($p_filename, $v_stored_filename)) { + return false; + } + + while (($v_buffer = fread($v_file, 512)) != '') { + $v_binary_data = pack("a512", "$v_buffer"); + $this->_writeBlock($v_binary_data); + } + + fclose($v_file); + } else { + // ----- Only header for dir + if (!$this->_writeHeader($p_filename, $v_stored_filename)) { + return false; + } + } + + return true; + } + + /** + * @param string $p_filename + * @param string $p_string + * @param bool $p_datetime + * @param array $p_params + * @return bool + */ + public function _addString($p_filename, $p_string, $p_datetime = false, $p_params = array()) { - $this->_close(); + $p_stamp = @$p_params["stamp"] ? $p_params["stamp"] : ($p_datetime ? $p_datetime : time()); + $p_mode = @$p_params["mode"] ? $p_params["mode"] : 0600; + $p_type = @$p_params["type"] ? $p_params["type"] : ""; + $p_uid = @$p_params["uid"] ? $p_params["uid"] : 0; + $p_gid = @$p_params["gid"] ? $p_params["gid"] : 0; + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } - // ----- Look for a local copy - if ($this->_temp_tarname != '') { - // ----- Remove the local copy but not the remote tarname - @drupal_unlink($this->_temp_tarname); - $this->_temp_tarname = ''; - } else { - // ----- Remove the local tarname file - @drupal_unlink($this->_tarname); + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; } - $this->_tarname = ''; - return true; - } - // }}} + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false); - // {{{ _writeBlock() - function _writeBlock($p_binary_data, $p_len=null) - { - if (is_resource($this->_file)) { - if ($p_len === null) { - if ($this->_compress_type == 'gz') - @gzputs($this->_file, $p_binary_data); - else if ($this->_compress_type == 'bz2') - @bzwrite($this->_file, $p_binary_data); - else if ($this->_compress_type == 'none') - @fputs($this->_file, $p_binary_data); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - } else { - if ($this->_compress_type == 'gz') - @gzputs($this->_file, $p_binary_data, $p_len); - else if ($this->_compress_type == 'bz2') - @bzwrite($this->_file, $p_binary_data, $p_len); - else if ($this->_compress_type == 'none') - @fputs($this->_file, $p_binary_data, $p_len); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - - } - } - return true; - } - // }}} - - // {{{ _readBlock() - function _readBlock() - { - $v_block = null; - if (is_resource($this->_file)) { - if ($this->_compress_type == 'gz') - $v_block = @gzread($this->_file, 512); - else if ($this->_compress_type == 'bz2') - $v_block = @bzread($this->_file, 512); - else if ($this->_compress_type == 'none') - $v_block = @fread($this->_file, 512); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - } - return $v_block; - } - // }}} - - // {{{ _jumpBlock() - function _jumpBlock($p_len=null) - { - if (is_resource($this->_file)) { - if ($p_len === null) - $p_len = 1; - - if ($this->_compress_type == 'gz') { - @gzseek($this->_file, gztell($this->_file)+($p_len*512)); - } - else if ($this->_compress_type == 'bz2') { - // ----- Replace missing bztell() and bzseek() - for ($i=0; $i<$p_len; $i++) - $this->_readBlock(); - } else if ($this->_compress_type == 'none') - @fseek($this->_file, ftell($this->_file)+($p_len*512)); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - - } - return true; - } - // }}} - - // {{{ _writeFooter() - function _writeFooter() - { - if (is_resource($this->_file)) { - // ----- Write the last 0 filled block for end of archive - $v_binary_data = pack('a1024', ''); - $this->_writeBlock($v_binary_data); - } - return true; - } - // }}} - - // {{{ _addList() - function _addList($p_list, $p_add_dir, $p_remove_dir) - { - $v_result=true; - $v_header = array(); - - // ----- Remove potential windows directory separator - $p_add_dir = $this->_translateWinPath($p_add_dir); - $p_remove_dir = $this->_translateWinPath($p_remove_dir, false); - - if (!$this->_file) { - $this->_error('Invalid file descriptor'); - return false; - } - - if (sizeof($p_list) == 0) - return true; - - foreach ($p_list as $v_filename) { - if (!$v_result) { - break; - } - - // ----- Skip the current tar name - if ($v_filename == $this->_tarname) - continue; - - if ($v_filename == '') - continue; - - if (!file_exists($v_filename)) { - $this->_warning("File '$v_filename' does not exist"); - continue; - } - - // ----- Add the file or directory header - if (!$this->_addFile($v_filename, $v_header, $p_add_dir, $p_remove_dir)) - return false; - - if (@is_dir($v_filename) && !@is_link($v_filename)) { - if (!($p_hdir = opendir($v_filename))) { - $this->_warning("Directory '$v_filename' can not be read"); - continue; - } - while (false !== ($p_hitem = readdir($p_hdir))) { - if (($p_hitem != '.') && ($p_hitem != '..')) { - if ($v_filename != ".") - $p_temp_list[0] = $v_filename.'/'.$p_hitem; - else - $p_temp_list[0] = $p_hitem; - - $v_result = $this->_addList($p_temp_list, - $p_add_dir, - $p_remove_dir); - } - } - - unset($p_temp_list); - unset($p_hdir); - unset($p_hitem); - } - } - - return $v_result; - } - // }}} - - // {{{ _addFile() - function _addFile($p_filename, &$p_header, $p_add_dir, $p_remove_dir) - { - if (!$this->_file) { - $this->_error('Invalid file descriptor'); - return false; - } - - if ($p_filename == '') { - $this->_error('Invalid file name'); - return false; - } - - // ----- Calculate the stored filename - $p_filename = $this->_translateWinPath($p_filename, false);; - $v_stored_filename = $p_filename; - if (strcmp($p_filename, $p_remove_dir) == 0) { - return true; - } - if ($p_remove_dir != '') { - if (substr($p_remove_dir, -1) != '/') - $p_remove_dir .= '/'; - - if (substr($p_filename, 0, strlen($p_remove_dir)) == $p_remove_dir) - $v_stored_filename = substr($p_filename, strlen($p_remove_dir)); - } - $v_stored_filename = $this->_translateWinPath($v_stored_filename); - if ($p_add_dir != '') { - if (substr($p_add_dir, -1) == '/') - $v_stored_filename = $p_add_dir.$v_stored_filename; - else - $v_stored_filename = $p_add_dir.'/'.$v_stored_filename; - } - - $v_stored_filename = $this->_pathReduction($v_stored_filename); - - if ($this->_isArchive($p_filename)) { - if (($v_file = @fopen($p_filename, "rb")) == 0) { - $this->_warning("Unable to open file '".$p_filename - ."' in binary read mode"); - return true; - } - - if (!$this->_writeHeader($p_filename, $v_stored_filename)) - return false; - - while (($v_buffer = fread($v_file, 512)) != '') { - $v_binary_data = pack("a512", "$v_buffer"); - $this->_writeBlock($v_binary_data); - } - - fclose($v_file); - - } else { - // ----- Only header for dir - if (!$this->_writeHeader($p_filename, $v_stored_filename)) - return false; - } - - return true; - } - // }}} - - // {{{ _addString() - function _addString($p_filename, $p_string) - { - if (!$this->_file) { - $this->_error('Invalid file descriptor'); - return false; - } - - if ($p_filename == '') { - $this->_error('Invalid file name'); - return false; - } - - // ----- Calculate the stored filename - $p_filename = $this->_translateWinPath($p_filename, false);; - - if (!$this->_writeHeaderBlock($p_filename, strlen($p_string), - time(), 384, "", 0, 0)) - return false; - - $i=0; - while (($v_buffer = substr($p_string, (($i++)*512), 512)) != '') { - $v_binary_data = pack("a512", $v_buffer); - $this->_writeBlock($v_binary_data); - } + // ----- If datetime is not specified, set current time + if ($p_datetime === false) { + $p_datetime = time(); + } + + if (!$this->_writeHeaderBlock( + $p_filename, + strlen($p_string), + $p_stamp, + $p_mode, + $p_type, + $p_uid, + $p_gid + ) + ) { + return false; + } - return true; + $i = 0; + while (($v_buffer = substr($p_string, (($i++) * 512), 512)) != '') { + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + return true; } - // }}} - // {{{ _writeHeader() - function _writeHeader($p_filename, $p_stored_filename) + /** + * @param string $p_filename + * @param string $p_stored_filename + * @return bool + */ + public function _writeHeader($p_filename, $p_stored_filename) { - if ($p_stored_filename == '') + if ($p_stored_filename == '') { $p_stored_filename = $p_filename; - $v_reduce_filename = $this->_pathReduction($p_stored_filename); - - if (strlen($v_reduce_filename) > 99) { - if (!$this->_writeLongHeader($v_reduce_filename)) - return false; } - $v_info = lstat($p_filename); - $v_uid = sprintf("%6s ", DecOct($v_info[4])); - $v_gid = sprintf("%6s ", DecOct($v_info[5])); - $v_perms = sprintf("%6s ", DecOct($v_info['mode'])); + $v_reduced_filename = $this->_pathReduction($p_stored_filename); - $v_mtime = sprintf("%11s", DecOct($v_info['mode'])); + if (strlen($v_reduced_filename) > 99) { + if (!$this->_writeLongHeader($v_reduced_filename, false)) { + return false; + } + } $v_linkname = ''; + if (@is_link($p_filename)) { + $v_linkname = readlink($p_filename); + } + + if (strlen($v_linkname) > 99) { + if (!$this->_writeLongHeader($v_linkname, true)) { + return false; + } + } + + $v_info = lstat($p_filename); + $v_uid = sprintf("%07s", DecOct($v_info[4])); + $v_gid = sprintf("%07s", DecOct($v_info[5])); + $v_perms = sprintf("%07s", DecOct($v_info['mode'] & 000777)); + $v_mtime = sprintf("%011s", DecOct($v_info['mtime'])); if (@is_link($p_filename)) { - $v_typeflag = '2'; - $v_linkname = readlink($p_filename); - $v_size = sprintf("%11s ", DecOct(0)); + $v_typeflag = '2'; + $v_size = sprintf("%011s", DecOct(0)); } elseif (@is_dir($p_filename)) { - $v_typeflag = "5"; - $v_size = sprintf("%11s ", DecOct(0)); + $v_typeflag = "5"; + $v_size = sprintf("%011s", DecOct(0)); } else { - $v_typeflag = ''; - clearstatcache(); - $v_size = sprintf("%11s ", DecOct($v_info['size'])); + $v_typeflag = '0'; + clearstatcache(); + $v_size = sprintf("%011s", DecOct($v_info['size'])); } - $v_magic = ''; + $v_magic = 'ustar '; + $v_version = ' '; - $v_version = ''; - - $v_uname = ''; + if (function_exists('posix_getpwuid')) { + $userinfo = posix_getpwuid($v_info[4]); + $groupinfo = posix_getgrgid($v_info[5]); - $v_gname = ''; + $v_uname = $userinfo['name']; + $v_gname = $groupinfo['name']; + } else { + $v_uname = ''; + $v_gname = ''; + } $v_devmajor = ''; - $v_devminor = ''; - $v_prefix = ''; - $v_binary_data_first = pack("a100a8a8a8a12A12", - $v_reduce_filename, $v_perms, $v_uid, - $v_gid, $v_size, $v_mtime); - $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", - $v_typeflag, $v_linkname, $v_magic, - $v_version, $v_uname, $v_gname, - $v_devmajor, $v_devminor, $v_prefix, ''); + $v_binary_data_first = pack( + "a100a8a8a8a12a12", + $v_reduced_filename, + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum += ord(substr($v_binary_data_first,$i,1)); + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) + for ($i = 148; $i < 156; $i++) { $v_checksum += ord(' '); + } // ..... Last part of the header - for ($i=156, $j=0; $i<512; $i++, $j++) - $v_checksum += ord(substr($v_binary_data_last,$j,1)); + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } // ----- Write the first 148 bytes of the header in the archive $this->_writeBlock($v_binary_data_first, 148); // ----- Write the calculated checksum - $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_checksum = sprintf("%06s\0 ", DecOct($v_checksum)); $v_binary_data = pack("a8", $v_checksum); $this->_writeBlock($v_binary_data, 8); @@ -1133,40 +1513,62 @@ return true; } - // }}} - // {{{ _writeHeaderBlock() - function _writeHeaderBlock($p_filename, $p_size, $p_mtime=0, $p_perms=0, - $p_type='', $p_uid=0, $p_gid=0) - { + /** + * @param string $p_filename + * @param int $p_size + * @param int $p_mtime + * @param int $p_perms + * @param string $p_type + * @param int $p_uid + * @param int $p_gid + * @return bool + */ + public function _writeHeaderBlock( + $p_filename, + $p_size, + $p_mtime = 0, + $p_perms = 0, + $p_type = '', + $p_uid = 0, + $p_gid = 0 + ) { $p_filename = $this->_pathReduction($p_filename); if (strlen($p_filename) > 99) { - if (!$this->_writeLongHeader($p_filename)) - return false; + if (!$this->_writeLongHeader($p_filename, false)) { + return false; + } } if ($p_type == "5") { - $v_size = sprintf("%11s ", DecOct(0)); + $v_size = sprintf("%011s", DecOct(0)); } else { - $v_size = sprintf("%11s ", DecOct($p_size)); + $v_size = sprintf("%011s", DecOct($p_size)); } - $v_uid = sprintf("%6s ", DecOct($p_uid)); - $v_gid = sprintf("%6s ", DecOct($p_gid)); - $v_perms = sprintf("%6s ", DecOct($p_perms)); + $v_uid = sprintf("%07s", DecOct($p_uid)); + $v_gid = sprintf("%07s", DecOct($p_gid)); + $v_perms = sprintf("%07s", DecOct($p_perms & 000777)); $v_mtime = sprintf("%11s", DecOct($p_mtime)); $v_linkname = ''; - $v_magic = ''; + $v_magic = 'ustar '; - $v_version = ''; + $v_version = ' '; - $v_uname = ''; + if (function_exists('posix_getpwuid')) { + $userinfo = posix_getpwuid($p_uid); + $groupinfo = posix_getgrgid($p_gid); - $v_gname = ''; + $v_uname = $userinfo['name']; + $v_gname = $groupinfo['name']; + } else { + $v_uname = ''; + $v_gname = ''; + } $v_devmajor = ''; @@ -1174,31 +1576,49 @@ $v_prefix = ''; - $v_binary_data_first = pack("a100a8a8a8a12A12", - $p_filename, $v_perms, $v_uid, $v_gid, - $v_size, $v_mtime); - $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", - $p_type, $v_linkname, $v_magic, - $v_version, $v_uname, $v_gname, - $v_devmajor, $v_devminor, $v_prefix, ''); + $v_binary_data_first = pack( + "a100a8a8a8a12A12", + $p_filename, + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $p_type, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum += ord(substr($v_binary_data_first,$i,1)); + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) + for ($i = 148; $i < 156; $i++) { $v_checksum += ord(' '); + } // ..... Last part of the header - for ($i=156, $j=0; $i<512; $i++, $j++) - $v_checksum += ord(substr($v_binary_data_last,$j,1)); + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } // ----- Write the first 148 bytes of the header in the archive $this->_writeBlock($v_binary_data_first, 148); // ----- Write the calculated checksum - $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_checksum = sprintf("%06s ", DecOct($v_checksum)); $v_binary_data = pack("a8", $v_checksum); $this->_writeBlock($v_binary_data, 8); @@ -1207,55 +1627,71 @@ return true; } - // }}} - // {{{ _writeLongHeader() - function _writeLongHeader($p_filename) + /** + * @param string $p_filename + * @return bool + */ + public function _writeLongHeader($p_filename, $is_link = false) { - $v_size = sprintf("%11s ", DecOct(strlen($p_filename))); - - $v_typeflag = 'L'; - + $v_uid = sprintf("%07s", 0); + $v_gid = sprintf("%07s", 0); + $v_perms = sprintf("%07s", 0); + $v_size = sprintf("%'011s", DecOct(strlen($p_filename))); + $v_mtime = sprintf("%011s", 0); + $v_typeflag = ($is_link ? 'K' : 'L'); $v_linkname = ''; - - $v_magic = ''; - - $v_version = ''; - + $v_magic = 'ustar '; + $v_version = ' '; $v_uname = ''; - $v_gname = ''; - $v_devmajor = ''; - $v_devminor = ''; - $v_prefix = ''; - $v_binary_data_first = pack("a100a8a8a8a12A12", - '././@LongLink', 0, 0, 0, $v_size, 0); - $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", - $v_typeflag, $v_linkname, $v_magic, - $v_version, $v_uname, $v_gname, - $v_devmajor, $v_devminor, $v_prefix, ''); + $v_binary_data_first = pack( + "a100a8a8a8a12a12", + '././@LongLink', + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum += ord(substr($v_binary_data_first,$i,1)); + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) + for ($i = 148; $i < 156; $i++) { $v_checksum += ord(' '); + } // ..... Last part of the header - for ($i=156, $j=0; $i<512; $i++, $j++) - $v_checksum += ord(substr($v_binary_data_last,$j,1)); + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } // ----- Write the first 148 bytes of the header in the archive $this->_writeBlock($v_binary_data_first, 148); // ----- Write the calculated checksum - $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_checksum = sprintf("%06s\0 ", DecOct($v_checksum)); $v_binary_data = pack("a8", $v_checksum); $this->_writeBlock($v_binary_data, 8); @@ -1263,27 +1699,30 @@ $this->_writeBlock($v_binary_data_last, 356); // ----- Write the filename as content of the block - $i=0; - while (($v_buffer = substr($p_filename, (($i++)*512), 512)) != '') { + $i = 0; + while (($v_buffer = substr($p_filename, (($i++) * 512), 512)) != '') { $v_binary_data = pack("a512", "$v_buffer"); $this->_writeBlock($v_binary_data); } return true; } - // }}} - // {{{ _readHeader() - function _readHeader($v_binary_data, &$v_header) + /** + * @param mixed $v_binary_data + * @param mixed $v_header + * @return bool + */ + public function _readHeader($v_binary_data, &$v_header) { - if (strlen($v_binary_data)==0) { + if (strlen($v_binary_data) == 0) { $v_header['filename'] = ''; return true; } if (strlen($v_binary_data) != 512) { $v_header['filename'] = ''; - $this->_error('Invalid block size : '.strlen($v_binary_data)); + $this->_error('Invalid block size : ' . strlen($v_binary_data)); return false; } @@ -1293,19 +1732,17 @@ // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum+=ord(substr($v_binary_data,$i,1)); - // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) - $v_checksum += ord(' '); - // ..... Last part of the header - for ($i=156; $i<512; $i++) - $v_checksum+=ord(substr($v_binary_data,$i,1)); + $v_binary_split = str_split($v_binary_data); + $v_checksum += array_sum(array_map('ord', array_slice($v_binary_split, 0, 148))); + $v_checksum += array_sum(array_map('ord', array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',))); + $v_checksum += array_sum(array_map('ord', array_slice($v_binary_split, 156, 512))); + - $v_data = unpack("a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" - ."a8checksum/a1typeflag/a100link/a6magic/a2version/" - ."a32uname/a32gname/a8devmajor/a8devminor", - $v_binary_data); + $v_data = unpack($this->_fmt, $v_binary_data); + + if (strlen($v_data["prefix"]) > 0) { + $v_data["filename"] = "$v_data[prefix]/$v_data[filename]"; + } // ----- Extract the checksum $v_header['checksum'] = OctDec(trim($v_data['checksum'])); @@ -1313,33 +1750,38 @@ $v_header['filename'] = ''; // ----- Look for last block (empty block) - if (($v_checksum == 256) && ($v_header['checksum'] == 0)) + if (($v_checksum == 256) && ($v_header['checksum'] == 0)) { return true; + } - $this->_error('Invalid checksum for file "'.$v_data['filename'] - .'" : '.$v_checksum.' calculated, ' - .$v_header['checksum'].' expected'); + $this->_error( + 'Invalid checksum for file "' . $v_data['filename'] + . '" : ' . $v_checksum . ' calculated, ' + . $v_header['checksum'] . ' expected' + ); return false; } // ----- Extract the properties - $v_header['filename'] = trim($v_data['filename']); + $v_header['filename'] = rtrim($v_data['filename'], "\0"); if ($this->_maliciousFilename($v_header['filename'])) { - $this->_error('Malicious .tar detected, file "' . $v_header['filename'] . - '" will not install in desired directory tree'); + $this->_error( + 'Malicious .tar detected, file "' . $v_header['filename'] . + '" will not install in desired directory tree' + ); return false; } $v_header['mode'] = OctDec(trim($v_data['mode'])); $v_header['uid'] = OctDec(trim($v_data['uid'])); $v_header['gid'] = OctDec(trim($v_data['gid'])); - $v_header['size'] = OctDec(trim($v_data['size'])); + $v_header['size'] = $this->_tarRecToSize($v_data['size']); $v_header['mtime'] = OctDec(trim($v_data['mtime'])); if (($v_header['typeflag'] = $v_data['typeflag']) == "5") { - $v_header['size'] = 0; + $v_header['size'] = 0; } $v_header['link'] = trim($v_data['link']); /* ----- All these fields are removed form the header because - they do not carry interesting info + they do not carry interesting info $v_header[magic] = trim($v_data[magic]); $v_header[version] = trim($v_data[version]); $v_header[uname] = trim($v_data[uname]); @@ -1350,406 +1792,580 @@ return true; } - // }}} - // {{{ _maliciousFilename() + /** + * Convert Tar record size to actual size + * + * @param string $tar_size + * @return size of tar record in bytes + */ + private function _tarRecToSize($tar_size) + { + /* + * First byte of size has a special meaning if bit 7 is set. + * + * Bit 7 indicates base-256 encoding if set. + * Bit 6 is the sign bit. + * Bits 5:0 are most significant value bits. + */ + $ch = ord($tar_size[0]); + if ($ch & 0x80) { + // Full 12-bytes record is required. + $rec_str = $tar_size . "\x00"; + + $size = ($ch & 0x40) ? -1 : 0; + $size = ($size << 6) | ($ch & 0x3f); + + for ($num_ch = 1; $num_ch < 12; ++$num_ch) { + $size = ($size * 256) + ord($rec_str[$num_ch]); + } + + return $size; + + } else { + return OctDec(trim($tar_size)); + } + } + /** * Detect and report a malicious file name * * @param string $file + * * @return bool - * @access private */ - function _maliciousFilename($file) + private function _maliciousFilename($file) { - if (strpos($file, '/../') !== false) { + if (strpos($file, 'phar://') === 0) { + return true; + } + if (strpos($file, DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR) !== false) { return true; } - if (strpos($file, '../') === 0) { + if (strpos($file, '..' . DIRECTORY_SEPARATOR) === 0) { return true; } return false; } - // }}} - // {{{ _readLongHeader() - function _readLongHeader(&$v_header) + /** + * @param $v_header + * @return bool + */ + public function _readLongHeader(&$v_header) { - $v_filename = ''; - $n = floor($v_header['size']/512); - for ($i=0; $i<$n; $i++) { - $v_content = $this->_readBlock(); - $v_filename .= $v_content; - } - if (($v_header['size'] % 512) != 0) { - $v_content = $this->_readBlock(); - $v_filename .= $v_content; - } + $v_filename = ''; + $v_filesize = $v_header['size']; + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; + } - // ----- Read the next header - $v_binary_data = $this->_readBlock(); + // ----- Read the next header + $v_binary_data = $this->_readBlock(); - if (!$this->_readHeader($v_binary_data, $v_header)) - return false; + if (!$this->_readHeader($v_binary_data, $v_header)) { + return false; + } - $v_filename = trim($v_filename); - $v_header['filename'] = $v_filename; + $v_filename = rtrim(substr($v_filename, 0, $v_filesize), "\0"); + $v_header['filename'] = $v_filename; if ($this->_maliciousFilename($v_filename)) { - $this->_error('Malicious .tar detected, file "' . $v_filename . - '" will not install in desired directory tree'); + $this->_error( + 'Malicious .tar detected, file "' . $v_filename . + '" will not install in desired directory tree' + ); return false; - } + } - return true; + return true; } - // }}} - // {{{ _extractInString() /** - * This method extract from the archive one file identified by $p_filename. - * The return value is a string with the file content, or NULL on error. - * @param string $p_filename The path of the file to extract in a string. - * @return a string with the file content or NULL. - * @access private - */ - function _extractInString($p_filename) + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or null on error. + * + * @param string $p_filename The path of the file to extract in a string. + * + * @return a string with the file content or null. + */ + private function _extractInString($p_filename) { $v_result_str = ""; - While (strlen($v_binary_data = $this->_readBlock()) != 0) - { - if (!$this->_readHeader($v_binary_data, $v_header)) - return NULL; - - if ($v_header['filename'] == '') - continue; - - // ----- Look for long filename - if ($v_header['typeflag'] == 'L') { - if (!$this->_readLongHeader($v_header)) - return NULL; - } - - if ($v_header['filename'] == $p_filename) { - if ($v_header['typeflag'] == "5") { - $this->_error('Unable to extract in string a directory ' - .'entry {'.$v_header['filename'].'}'); - return NULL; - } else { - $n = floor($v_header['size']/512); - for ($i=0; $i<$n; $i++) { - $v_result_str .= $this->_readBlock(); - } - if (($v_header['size'] % 512) != 0) { - $v_content = $this->_readBlock(); - $v_result_str .= substr($v_content, 0, - ($v_header['size'] % 512)); - } - return $v_result_str; - } - } else { - $this->_jumpBlock(ceil(($v_header['size']/512))); - } - } - - return NULL; - } - // }}} - - // {{{ _extractList() - function _extractList($p_path, &$p_list_detail, $p_mode, - $p_file_list, $p_remove_path) - { - $v_result=true; - $v_nb = 0; - $v_extract_all = true; - $v_listing = false; - - $p_path = $this->_translateWinPath($p_path, false); - if ($p_path == '' || (substr($p_path, 0, 1) != '/' - && substr($p_path, 0, 3) != "../" && !strpos($p_path, ':'))) { - $p_path = "./".$p_path; - } - $p_remove_path = $this->_translateWinPath($p_remove_path); - - // ----- Look for path to remove format (should end by /) - if (($p_remove_path != '') && (substr($p_remove_path, -1) != '/')) - $p_remove_path .= '/'; - $p_remove_path_size = strlen($p_remove_path); - - switch ($p_mode) { - case "complete" : - $v_extract_all = TRUE; - $v_listing = FALSE; - break; - case "partial" : - $v_extract_all = FALSE; - $v_listing = FALSE; - break; - case "list" : - $v_extract_all = FALSE; - $v_listing = TRUE; - break; - default : - $this->_error('Invalid extract mode ('.$p_mode.')'); - return false; + while (strlen($v_binary_data = $this->_readBlock()) != 0) { + if (!$this->_readHeader($v_binary_data, $v_header)) { + return null; + } + + if ($v_header['filename'] == '') { + continue; + } + + switch ($v_header['typeflag']) { + case 'L': { + if (!$this->_readLongHeader($v_header)) { + return null; + } + } break; + + case 'K': { + $v_link_header = $v_header; + if (!$this->_readLongHeader($v_link_header)) { + return null; + } + $v_header['link'] = $v_link_header['filename']; + } break; + } + + if ($v_header['filename'] == $p_filename) { + if ($v_header['typeflag'] == "5") { + $this->_error( + 'Unable to extract in string a directory ' + . 'entry {' . $v_header['filename'] . '}' + ); + return null; + } else { + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_result_str .= $this->_readBlock(); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_result_str .= substr( + $v_content, + 0, + ($v_header['size'] % 512) + ); + } + return $v_result_str; + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + } + + return null; } - clearstatcache(); + /** + * @param string $p_path + * @param string $p_list_detail + * @param string $p_mode + * @param string $p_file_list + * @param string $p_remove_path + * @param bool $p_preserve + * @return bool + */ + public function _extractList( + $p_path, + &$p_list_detail, + $p_mode, + $p_file_list, + $p_remove_path, + $p_preserve = false + ) { + $v_result = true; + $v_nb = 0; + $v_extract_all = true; + $v_listing = false; + + $p_path = $this->_translateWinPath($p_path, false); + if ($p_path == '' || (substr($p_path, 0, 1) != '/' + && substr($p_path, 0, 3) != "../" && !strpos($p_path, ':')) + ) { + $p_path = "./" . $p_path; + } + $p_remove_path = $this->_translateWinPath($p_remove_path); + + // ----- Look for path to remove format (should end by /) + if (($p_remove_path != '') && (substr($p_remove_path, -1) != '/')) { + $p_remove_path .= '/'; + } + $p_remove_path_size = strlen($p_remove_path); + + switch ($p_mode) { + case "complete" : + $v_extract_all = true; + $v_listing = false; + break; + case "partial" : + $v_extract_all = false; + $v_listing = false; + break; + case "list" : + $v_extract_all = false; + $v_listing = true; + break; + default : + $this->_error('Invalid extract mode (' . $p_mode . ')'); + return false; + } - while (strlen($v_binary_data = $this->_readBlock()) != 0) - { - $v_extract_file = FALSE; - $v_extraction_stopped = 0; + clearstatcache(); - if (!$this->_readHeader($v_binary_data, $v_header)) - return false; + while (strlen($v_binary_data = $this->_readBlock()) != 0) { + $v_extract_file = false; + $v_extraction_stopped = 0; + + if (!$this->_readHeader($v_binary_data, $v_header)) { + return false; + } + + if ($v_header['filename'] == '') { + continue; + } + + switch ($v_header['typeflag']) { + case 'L': { + if (!$this->_readLongHeader($v_header)) { + return null; + } + } break; + + case 'K': { + $v_link_header = $v_header; + if (!$this->_readLongHeader($v_link_header)) { + return null; + } + $v_header['link'] = $v_link_header['filename']; + } break; + } + + // ignore extended / pax headers + if ($v_header['typeflag'] == 'x' || $v_header['typeflag'] == 'g') { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + continue; + } + + if ((!$v_extract_all) && (is_array($p_file_list))) { + // ----- By default no unzip if the file is not found + $v_extract_file = false; + + for ($i = 0; $i < sizeof($p_file_list); $i++) { + // ----- Look if it is a directory + if (substr($p_file_list[$i], -1) == '/') { + // ----- Look if the directory is in the filename path + if ((strlen($v_header['filename']) > strlen($p_file_list[$i])) + && (substr($v_header['filename'], 0, strlen($p_file_list[$i])) + == $p_file_list[$i]) + ) { + $v_extract_file = true; + break; + } + } // ----- It is a file, so compare the file names + elseif ($p_file_list[$i] == $v_header['filename']) { + $v_extract_file = true; + break; + } + } + } else { + $v_extract_file = true; + } + + // ----- Look if this file need to be extracted + if (($v_extract_file) && (!$v_listing)) { + if (($p_remove_path != '') + && (substr($v_header['filename'] . '/', 0, $p_remove_path_size) + == $p_remove_path) + ) { + $v_header['filename'] = substr( + $v_header['filename'], + $p_remove_path_size + ); + if ($v_header['filename'] == '') { + continue; + } + } + if (($p_path != './') && ($p_path != '/')) { + while (substr($p_path, -1) == '/') { + $p_path = substr($p_path, 0, strlen($p_path) - 1); + } - if ($v_header['filename'] == '') { - continue; - } - - // ----- Look for long filename - if ($v_header['typeflag'] == 'L') { - if (!$this->_readLongHeader($v_header)) - return false; - } - - if ((!$v_extract_all) && (is_array($p_file_list))) { - // ----- By default no unzip if the file is not found - $v_extract_file = false; - - for ($i=0; $i strlen($p_file_list[$i])) - && (substr($v_header['filename'], 0, strlen($p_file_list[$i])) - == $p_file_list[$i])) { - $v_extract_file = TRUE; - break; - } - } - - // ----- It is a file, so compare the file names - elseif ($p_file_list[$i] == $v_header['filename']) { - $v_extract_file = TRUE; - break; - } - } - } else { - $v_extract_file = TRUE; - } - - // ----- Look if this file need to be extracted - if (($v_extract_file) && (!$v_listing)) - { - if (($p_remove_path != '') - && (substr($v_header['filename'], 0, $p_remove_path_size) - == $p_remove_path)) - $v_header['filename'] = substr($v_header['filename'], - $p_remove_path_size); - if (($p_path != './') && ($p_path != '/')) { - while (substr($p_path, -1) == '/') - $p_path = substr($p_path, 0, strlen($p_path)-1); - - if (substr($v_header['filename'], 0, 1) == '/') - $v_header['filename'] = $p_path.$v_header['filename']; - else - $v_header['filename'] = $p_path.'/'.$v_header['filename']; - } - if (file_exists($v_header['filename'])) { - if ( (@is_dir($v_header['filename'])) - && ($v_header['typeflag'] == '')) { - $this->_error('File '.$v_header['filename'] - .' already exists as a directory'); - return false; - } - if ( ($this->_isArchive($v_header['filename'])) - && ($v_header['typeflag'] == "5")) { - $this->_error('Directory '.$v_header['filename'] - .' already exists as a file'); - return false; - } - if (!is_writeable($v_header['filename'])) { - $this->_error('File '.$v_header['filename'] - .' already exists and is write protected'); - return false; - } - if (filemtime($v_header['filename']) > $v_header['mtime']) { - // To be completed : An error or silent no replace ? - } - } - - // ----- Check the directory availability and create it if necessary - elseif (($v_result - = $this->_dirCheck(($v_header['typeflag'] == "5" - ?$v_header['filename'] - :dirname($v_header['filename'])))) != 1) { - $this->_error('Unable to create path for '.$v_header['filename']); - return false; - } - - if ($v_extract_file) { - if ($v_header['typeflag'] == "5") { - if (!@file_exists($v_header['filename'])) { - // Drupal integration. - // Changed the code to use drupal_mkdir() instead of mkdir(). - if (!@drupal_mkdir($v_header['filename'], 0777)) { - $this->_error('Unable to create directory {' - .$v_header['filename'].'}'); + if (substr($v_header['filename'], 0, 1) == '/') { + $v_header['filename'] = $p_path . $v_header['filename']; + } else { + $v_header['filename'] = $p_path . '/' . $v_header['filename']; + } + } + if (file_exists($v_header['filename'])) { + if ((@is_dir($v_header['filename'])) + && ($v_header['typeflag'] == '') + ) { + $this->_error( + 'File ' . $v_header['filename'] + . ' already exists as a directory' + ); + return false; + } + if (($this->_isArchive($v_header['filename'])) + && ($v_header['typeflag'] == "5") + ) { + $this->_error( + 'Directory ' . $v_header['filename'] + . ' already exists as a file' + ); + return false; + } + if (!is_writeable($v_header['filename'])) { + $this->_error( + 'File ' . $v_header['filename'] + . ' already exists and is write protected' + ); + return false; + } + if (filemtime($v_header['filename']) > $v_header['mtime']) { + // To be completed : An error or silent no replace ? + } + } // ----- Check the directory availability and create it if necessary + elseif (($v_result + = $this->_dirCheck( + ($v_header['typeflag'] == "5" + ? $v_header['filename'] + : dirname($v_header['filename'])) + )) != 1 + ) { + $this->_error('Unable to create path for ' . $v_header['filename']); return false; } + + if ($v_extract_file) { + if ($v_header['typeflag'] == "5") { + if (!@file_exists($v_header['filename'])) { + if (!@mkdir($v_header['filename'], 0777)) { + $this->_error( + 'Unable to create directory {' + . $v_header['filename'] . '}' + ); + return false; + } + } + } elseif ($v_header['typeflag'] == "2") { + if (@file_exists($v_header['filename'])) { + @drupal_unlink($v_header['filename']); + } + if (!@symlink($v_header['link'], $v_header['filename'])) { + $this->_error( + 'Unable to extract symbolic link {' + . $v_header['filename'] . '}' + ); + return false; + } + } else { + if (($v_dest_file = @fopen($v_header['filename'], "wb")) == 0) { + $this->_error( + 'Error while opening {' . $v_header['filename'] + . '} in write binary mode' + ); + return false; + } else { + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, 512); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, ($v_header['size'] % 512)); + } + + @fclose($v_dest_file); + + if ($p_preserve) { + @chown($v_header['filename'], $v_header['uid']); + @chgrp($v_header['filename'], $v_header['gid']); + } + + // ----- Change the file mode, mtime + @touch($v_header['filename'], $v_header['mtime']); + if ($v_header['mode'] & 0111) { + // make file executable, obey umask + $mode = fileperms($v_header['filename']) | (~umask() & 0111); + @chmod($v_header['filename'], $mode); + } + } + + // ----- Check the file size + clearstatcache(); + if (!is_file($v_header['filename'])) { + $this->_error( + 'Extracted file ' . $v_header['filename'] + . 'does not exist. Archive may be corrupted.' + ); + return false; + } + + $filesize = filesize($v_header['filename']); + if ($filesize != $v_header['size']) { + $this->_error( + 'Extracted file ' . $v_header['filename'] + . ' does not have the correct file size \'' + . $filesize + . '\' (' . $v_header['size'] + . ' expected). Archive may be corrupted.' + ); + return false; + } + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); } - } elseif ($v_header['typeflag'] == "2") { - if (@file_exists($v_header['filename'])) { - @drupal_unlink($v_header['filename']); - } - if (!@symlink($v_header['link'], $v_header['filename'])) { - $this->_error('Unable to extract symbolic link {' - .$v_header['filename'].'}'); - return false; - } - } else { - if (($v_dest_file = @fopen($v_header['filename'], "wb")) == 0) { - $this->_error('Error while opening {'.$v_header['filename'] - .'} in write binary mode'); - return false; - } else { - $n = floor($v_header['size']/512); - for ($i=0; $i<$n; $i++) { - $v_content = $this->_readBlock(); - fwrite($v_dest_file, $v_content, 512); - } - if (($v_header['size'] % 512) != 0) { - $v_content = $this->_readBlock(); - fwrite($v_dest_file, $v_content, ($v_header['size'] % 512)); - } - - @fclose($v_dest_file); - - // ----- Change the file mode, mtime - @touch($v_header['filename'], $v_header['mtime']); - if ($v_header['mode'] & 0111) { - // make file executable, obey umask - $mode = fileperms($v_header['filename']) | (~umask() & 0111); - @chmod($v_header['filename'], $mode); - } - } - - // ----- Check the file size - clearstatcache(); - if (filesize($v_header['filename']) != $v_header['size']) { - $this->_error('Extracted file '.$v_header['filename'] - .' does not have the correct file size \'' - .filesize($v_header['filename']) - .'\' ('.$v_header['size'] - .' expected). Archive may be corrupted.'); - return false; - } - } - } else { - $this->_jumpBlock(ceil(($v_header['size']/512))); - } - } else { - $this->_jumpBlock(ceil(($v_header['size']/512))); - } - - /* TBC : Seems to be unused ... - if ($this->_compress) - $v_end_of_file = @gzeof($this->_file); - else - $v_end_of_file = @feof($this->_file); - */ - if ($v_listing || $v_extract_file || $v_extraction_stopped) { - // ----- Log extracted files - if (($v_file_dir = dirname($v_header['filename'])) - == $v_header['filename']) - $v_file_dir = ''; - if ((substr($v_header['filename'], 0, 1) == '/') && ($v_file_dir == '')) - $v_file_dir = '/'; + /* TBC : Seems to be unused ... + if ($this->_compress) + $v_end_of_file = @gzeof($this->_file); + else + $v_end_of_file = @feof($this->_file); + */ - $p_list_detail[$v_nb++] = $v_header; - if (is_array($p_file_list) && (count($p_list_detail) == count($p_file_list))) { - return true; + if ($v_listing || $v_extract_file || $v_extraction_stopped) { + // ----- Log extracted files + if (($v_file_dir = dirname($v_header['filename'])) + == $v_header['filename'] + ) { + $v_file_dir = ''; + } + if ((substr($v_header['filename'], 0, 1) == '/') && ($v_file_dir == '')) { + $v_file_dir = '/'; + } + + $p_list_detail[$v_nb++] = $v_header; + if (is_array($p_file_list) && (count($p_list_detail) == count($p_file_list))) { + return true; + } + } } - } - } return true; } - // }}} - // {{{ _openAppend() - function _openAppend() + /** + * @return bool + */ + public function _openAppend() { - if (filesize($this->_tarname) == 0) - return $this->_openWrite(); + if (filesize($this->_tarname) == 0) { + return $this->_openWrite(); + } if ($this->_compress) { $this->_close(); - if (!@rename($this->_tarname, $this->_tarname.".tmp")) { - $this->_error('Error while renaming \''.$this->_tarname - .'\' to temporary file \''.$this->_tarname - .'.tmp\''); + if (!@rename($this->_tarname, $this->_tarname . ".tmp")) { + $this->_error( + 'Error while renaming \'' . $this->_tarname + . '\' to temporary file \'' . $this->_tarname + . '.tmp\'' + ); return false; } - if ($this->_compress_type == 'gz') - $v_temp_tar = @gzopen($this->_tarname.".tmp", "rb"); - elseif ($this->_compress_type == 'bz2') - $v_temp_tar = @bzopen($this->_tarname.".tmp", "r"); + if ($this->_compress_type == 'gz') { + $v_temp_tar = @gzopen($this->_tarname . ".tmp", "rb"); + } elseif ($this->_compress_type == 'bz2') { + $v_temp_tar = @bzopen($this->_tarname . ".tmp", "r"); + } elseif ($this->_compress_type == 'lzma2') { + $v_temp_tar = @xzopen($this->_tarname . ".tmp", "r"); + } + if ($v_temp_tar == 0) { - $this->_error('Unable to open file \''.$this->_tarname - .'.tmp\' in binary read mode'); - @rename($this->_tarname.".tmp", $this->_tarname); + $this->_error( + 'Unable to open file \'' . $this->_tarname + . '.tmp\' in binary read mode' + ); + @rename($this->_tarname . ".tmp", $this->_tarname); return false; } if (!$this->_openWrite()) { - @rename($this->_tarname.".tmp", $this->_tarname); + @rename($this->_tarname . ".tmp", $this->_tarname); return false; } if ($this->_compress_type == 'gz') { + $end_blocks = 0; + while (!@gzeof($v_temp_tar)) { $v_buffer = @gzread($v_temp_tar, 512); - if ($v_buffer == ARCHIVE_TAR_END_BLOCK) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; // do not copy end blocks, we will re-make them // after appending continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; } $v_binary_data = pack("a512", $v_buffer); $this->_writeBlock($v_binary_data); } @gzclose($v_temp_tar); - } - elseif ($this->_compress_type == 'bz2') { + } elseif ($this->_compress_type == 'bz2') { + $end_blocks = 0; + while (strlen($v_buffer = @bzread($v_temp_tar, 512)) > 0) { - if ($v_buffer == ARCHIVE_TAR_END_BLOCK) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; + // do not copy end blocks, we will re-make them + // after appending continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; } $v_binary_data = pack("a512", $v_buffer); $this->_writeBlock($v_binary_data); } @bzclose($v_temp_tar); - } + } elseif ($this->_compress_type == 'lzma2') { + $end_blocks = 0; + + while (strlen($v_buffer = @xzread($v_temp_tar, 512)) > 0) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; + // do not copy end blocks, we will re-make them + // after appending + continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; + } + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } - if (!@drupal_unlink($this->_tarname.".tmp")) { - $this->_error('Error while deleting temporary file \'' - .$this->_tarname.'.tmp\''); + @xzclose($v_temp_tar); } + if (!@drupal_unlink($this->_tarname . ".tmp")) { + $this->_error( + 'Error while deleting temporary file \'' + . $this->_tarname . '.tmp\'' + ); + } } else { // ----- For not compressed tar, just add files before the last - // one or two 512 bytes block - if (!$this->_openReadWrite()) - return false; + // one or two 512 bytes block + if (!$this->_openReadWrite()) { + return false; + } clearstatcache(); $v_size = filesize($this->_tarname); @@ -1760,32 +2376,34 @@ fseek($this->_file, $v_size - 1024); if (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { fseek($this->_file, $v_size - 1024); - } - elseif (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { + } elseif (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { fseek($this->_file, $v_size - 512); } } return true; } - // }}} - // {{{ _append() - function _append($p_filelist, $p_add_dir='', $p_remove_dir='') + /** + * @param $p_filelist + * @param string $p_add_dir + * @param string $p_remove_dir + * @return bool + */ + public function _append($p_filelist, $p_add_dir = '', $p_remove_dir = '') { - if (!$this->_openAppend()) + if (!$this->_openAppend()) { return false; + } - if ($this->_addList($p_filelist, $p_add_dir, $p_remove_dir)) - $this->_writeFooter(); + if ($this->_addList($p_filelist, $p_add_dir, $p_remove_dir)) { + $this->_writeFooter(); + } $this->_close(); return true; } - // }}} - - // {{{ _dirCheck() /** * Check if a directory exists and create it (including parent @@ -1793,24 +2411,25 @@ * * @param string $p_dir directory to check * - * @return bool TRUE if the directory exists or was created + * @return bool true if the directory exists or was created */ - function _dirCheck($p_dir) + public function _dirCheck($p_dir) { clearstatcache(); - if ((@is_dir($p_dir)) || ($p_dir == '')) + if ((@is_dir($p_dir)) || ($p_dir == '')) { return true; + } $p_parent_dir = dirname($p_dir); if (($p_parent_dir != $p_dir) && ($p_parent_dir != '') && - (!$this->_dirCheck($p_parent_dir))) - return false; + (!$this->_dirCheck($p_parent_dir)) + ) { + return false; + } - // Drupal integration. - // Changed the code to use drupal_mkdir() instead of mkdir(). - if (!@drupal_mkdir($p_dir, 0777)) { + if (!@mkdir($p_dir, 0777)) { $this->_error("Unable to create directory '$p_dir'"); return false; } @@ -1818,10 +2437,6 @@ return true; } - // }}} - - // {{{ _pathReduction() - /** * Compress path by changing for example "/dir/foo/../bar" to "/dir/bar", * rand emove double slashes. @@ -1829,11 +2444,8 @@ * @param string $p_dir path to reduce * * @return string reduced path - * - * @access private - * */ - function _pathReduction($p_dir) + private function _pathReduction($p_dir) { $v_result = ''; @@ -1843,50 +2455,57 @@ $v_list = explode('/', $p_dir); // ----- Study directories from last to first - for ($i=sizeof($v_list)-1; $i>=0; $i--) { + for ($i = sizeof($v_list) - 1; $i >= 0; $i--) { // ----- Look for current path if ($v_list[$i] == ".") { // ----- Ignore this directory // Should be the first $i=0, but no check is done - } - else if ($v_list[$i] == "..") { - // ----- Ignore it and ignore the $i-1 - $i--; - } - else if ( ($v_list[$i] == '') - && ($i!=(sizeof($v_list)-1)) - && ($i!=0)) { - // ----- Ignore only the double '//' in path, - // but not the first and last / } else { - $v_result = $v_list[$i].($i!=(sizeof($v_list)-1)?'/' - .$v_result:''); + if ($v_list[$i] == "..") { + // ----- Ignore it and ignore the $i-1 + $i--; + } else { + if (($v_list[$i] == '') + && ($i != (sizeof($v_list) - 1)) + && ($i != 0) + ) { + // ----- Ignore only the double '//' in path, + // but not the first and last / + } else { + $v_result = $v_list[$i] . ($i != (sizeof($v_list) - 1) ? '/' + . $v_result : ''); + } + } } } } - $v_result = strtr($v_result, '\\', '/'); + + if (defined('OS_WINDOWS') && OS_WINDOWS) { + $v_result = strtr($v_result, '\\', '/'); + } + return $v_result; } - // }}} - - // {{{ _translateWinPath() - function _translateWinPath($p_path, $p_remove_disk_letter=true) + /** + * @param $p_path + * @param bool $p_remove_disk_letter + * @return string + */ + public function _translateWinPath($p_path, $p_remove_disk_letter = true) { - if (defined('OS_WINDOWS') && OS_WINDOWS) { - // ----- Look for potential disk letter - if ( ($p_remove_disk_letter) - && (($v_position = strpos($p_path, ':')) != false)) { - $p_path = substr($p_path, $v_position+1); - } - // ----- Change potential windows directory separator - if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0,1) == '\\')) { - $p_path = strtr($p_path, '\\', '/'); - } - } - return $p_path; + if (defined('OS_WINDOWS') && OS_WINDOWS) { + // ----- Look for potential disk letter + if (($p_remove_disk_letter) + && (($v_position = strpos($p_path, ':')) != false) + ) { + $p_path = substr($p_path, $v_position + 1); + } + // ----- Change potential windows directory separator + if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) { + $p_path = strtr($p_path, '\\', '/'); + } + } + return $p_path; } - // }}} - } -?> diff -Naur drupal-7.23/modules/system/system.test drupal-7.66/modules/system/system.test --- drupal-7.23/modules/system/system.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.test 2019-04-17 22:20:46.000000000 +0200 @@ -390,6 +390,18 @@ } /** + * Checks functionality of project namespaces for dependencies. + */ + function testProjectNamespaceForDependencies() { + // Enable module with project namespace to ensure nothing breaks. + $edit = array( + 'modules[Testing][system_project_namespace_test][enable]' => TRUE, + ); + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->assertModules(array('system_project_namespace_test'), TRUE); + } + + /** * Attempt to enable translation module without locale enabled. */ function testEnableWithoutDependency() { @@ -556,6 +568,34 @@ $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.'); + } } /** @@ -686,7 +726,7 @@ // Block a valid IP address. $edit = array(); - $edit['ip'] = '192.168.1.1'; + $edit['ip'] = '1.2.3.3'; $this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add')); $ip = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $edit['ip']))->fetchField(); $this->assertTrue($ip, t('IP address found in database.')); @@ -694,7 +734,7 @@ // Try to block an IP address that's already blocked. $edit = array(); - $edit['ip'] = '192.168.1.1'; + $edit['ip'] = '1.2.3.3'; $this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add')); $this->assertText(t('This IP address is already blocked.')); @@ -730,6 +770,25 @@ // $this->drupalPost('admin/config/people/ip-blocking', $edit, t('Save')); // $this->assertText(t('You may not block your own IP address.')); } + + /** + * Test duplicate IP addresses are not present in the 'blocked_ips' table. + */ + function testDuplicateIpAddress() { + drupal_static_reset('ip_address'); + $submit_ip = $_SERVER['REMOTE_ADDR'] = '192.168.1.1'; + system_block_ip_action(); + system_block_ip_action(); + $ip_count = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->rowCount(); + $this->assertEqual('1', $ip_count); + drupal_static_reset('ip_address'); + $submit_ip = $_SERVER['REMOTE_ADDR'] = ' '; + system_block_ip_action(); + system_block_ip_action(); + system_block_ip_action(); + $ip_count = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->rowCount(); + $this->assertEqual('1', $ip_count); + } } class CronRunTestCase extends DrupalWebTestCase { @@ -865,6 +924,84 @@ $result = variable_get('common_test_cron'); $this->assertEqual($result, 'success', 'Cron correctly handles exceptions thrown during hook_cron() invocations.'); } + + /** + * Tests that hook_flush_caches() is not invoked on every single cron run. + * + * @see system_cron() + */ + public function testCronCacheExpiration() { + module_enable(array('system_cron_test')); + variable_del('system_cron_test_flush_caches'); + + // Invoke cron the first time: hook_flush_caches() should be called and then + // get cached. + drupal_cron_run(); + $this->assertEqual(variable_get('system_cron_test_flush_caches'), 1, 'hook_flush_caches() was invoked the first time.'); + $cache = cache_get('system_cache_tables'); + $this->assertEqual(empty($cache), FALSE, 'Cache is filled with cache table data.'); + + // Run cron again and ensure that hook_flush_caches() is not called. + variable_del('system_cron_test_flush_caches'); + drupal_cron_run(); + $this->assertNull(variable_get('system_cron_test_flush_caches'), 'hook_flush_caches() was not invoked the second time.'); + } + +} + +/** + * Test execution of the cron queue. + */ +class CronQueueTestCase extends DrupalWebTestCase { + /** + * Implement getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Cron queue functionality', + 'description' => 'Tests the cron queue runner.', + 'group' => 'System' + ); + } + + function setUp() { + parent::setUp(array('common_test', 'common_test_cron_helper', 'cron_queue_test')); + } + + /** + * Tests that exceptions thrown by workers are handled properly. + */ + function testExceptions() { + $queue = DrupalQueue::get('cron_queue_test_exception'); + + // Enqueue an item for processing. + $queue->createItem(array($this->randomName() => $this->randomName())); + + // Run cron; the worker for this queue should throw an exception and handle + // it. + $this->cronRun(); + + // The item should be left in the queue. + $this->assertEqual($queue->numberOfItems(), 1, 'Failing item still in the queue after throwing an exception.'); + } + + /** + * Tests worker defined as a class method callable. + */ + function testCallable() { + $queue = DrupalQueue::get('cron_queue_test_callback'); + + // Enqueue an item for processing. + $queue->createItem(array($this->randomName() => $this->randomName())); + + // Run cron; the worker should perform the task and delete the item from the + // queue. + $this->cronRun(); + + // The queue should be empty. + $this->assertEqual($queue->numberOfItems(), 0); + } + } class AdminMetaTagTestCase extends DrupalWebTestCase { @@ -1002,6 +1139,11 @@ ); $node = $this->drupalCreateNode($edit); + // As node IDs must be integers, make sure requests for non-integer IDs + // return a page not found error. + $this->drupalGet('node/invalid'); + $this->assertResponse(404); + // Use a custom 404 page. $this->drupalPost('admin/config/system/site-information', array('site_404' => 'node/' . $node->nid), t('Save configuration')); @@ -1227,7 +1369,23 @@ $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), 'Correct page redirection.'); $this->assertText(t('Custom date format updated.'), 'Custom date format successfully updated.'); + // Check that ajax callback is protected by CSRF token. + $this->drupalGet('admin/config/regional/date-time/formats/lookup', array('query' => array('format' => 'Y m d'))); + $this->assertResponse(403, 'Access denied with no token'); + $this->drupalGet('admin/config/regional/date-time/formats/lookup', array('query' => array('token' => 'invalid', 'format' => 'Y m d'))); + $this->assertResponse(403, 'Access denied with invalid token'); + $this->drupalGet('admin/config/regional/date-time/formats'); + $this->clickLink(t('edit')); + $settings = $this->drupalGetSettings(); + $lookup_url = $settings['dateTime']['date-format']['lookup']; + preg_match('/token=([^&]+)/', $lookup_url, $matches); + $this->assertFalse(empty($matches[1]), 'Found token value'); + $this->drupalGet('admin/config/regional/date-time/formats/lookup', array('query' => array('token' => $matches[1], 'format' => 'Y m d'))); + $this->assertResponse(200, 'Access allowed with valid token'); + $this->assertText(format_date(time(), 'custom', 'Y m d')); + // Delete custom date format. + $this->drupalGet('admin/config/regional/date-time/formats'); $this->clickLink(t('delete')); $this->drupalPost($this->getUrl(), array(), t('Remove')); $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), 'Correct page redirection.'); @@ -1303,6 +1461,60 @@ } } +/** + * Tests date format configuration. + */ +class DateFormatTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Date format', + 'description' => 'Test date format configuration and defaults.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp(); + + // Create admin user and log in admin user. + $this->admin_user = $this->drupalCreateUser(array('administer site configuration')); + $this->drupalLogin($this->admin_user); + } + + /** + * Test the default date type formats are consistent. + */ + function testDefaultDateFormats() { + // These are the default format values from format_date(). + $default_formats = array( + 'short' => 'm/d/Y - H:i', + 'medium' => 'D, m/d/Y - H:i', + 'long' => 'l, F j, Y - H:i', + ); + + // Clear the date format variables. + variable_del('date_format_short'); + variable_del('date_format_medium'); + variable_del('date_format_long'); + + $this->drupalGet('admin/config/regional/date-time'); + + foreach ($default_formats as $format_name => $format_value) { + $id = 'edit-date-format-' . $format_name; + // Check that the configuration fields match the default format. + $this->assertOptionSelected( + $id, + $format_value, + format_string('The @type format type matches the expected format @format.', + array( + '@type' => $format_name, + '@format' => $format_value, + ) + )); + } + } +} + class PageTitleFiltering extends DrupalWebTestCase { protected $content_user; protected $saved_title; @@ -1732,6 +1944,30 @@ $this->assertEqual($elements[0]['src'], file_create_url($uploaded_filename)); } + + /** + * Test the individual per-theme settings form. + */ + function testPerThemeSettings() { + // Enable the test theme and the module that controls it. Clear caches in + // between so that the module's hook_system_theme_info() implementation is + // correctly registered. + module_enable(array('theme_test')); + drupal_flush_all_caches(); + theme_enable(array('test_theme')); + + // Test that the theme-specific settings form can be saved and that the + // theme-specific checkbox is checked and unchecked as appropriate. + $this->drupalGet('admin/appearance/settings/test_theme'); + $this->assertNoFieldChecked('edit-test-theme-checkbox', 'The test_theme_checkbox setting is unchecked.'); + $this->drupalPost(NULL, array('test_theme_checkbox' => TRUE), t('Save configuration')); + $this->assertText('The test theme setting was saved.'); + $this->assertFieldChecked('edit-test-theme-checkbox', 'The test_theme_checkbox setting is checked.'); + $this->drupalPost(NULL, array('test_theme_checkbox' => FALSE), t('Save configuration')); + $this->assertText('The test theme setting was saved.'); + $this->assertNoFieldChecked('edit-test-theme-checkbox', 'The test_theme_checkbox setting is unchecked.'); + } + /** * Test the administration theme functionality. */ @@ -2216,6 +2452,20 @@ } /** + * Tests that there are no pending updates for the first test method. + */ + function testNoPendingUpdates() { + // Ensure that for the first test method in a class, there are no pending + // updates. This tests a drupal_get_schema_versions() bug that previously + // led to the wrong schema version being recorded for the initial install + // of a child site during automated testing. + $this->drupalLogin($this->update_user); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->drupalPost(NULL, array(), t('Continue')); + $this->assertText(t('No pending updates.'), 'End of update process was reached.'); + } + + /** * Tests access to the update script. */ function testUpdateAccess() { @@ -2296,6 +2546,12 @@ $this->assertText('This is a requirements error provided by the update_script_test module.'); $this->clickLink('try again'); $this->assertText('This is a requirements error provided by the update_script_test module.'); + + // Check if the optional 'value' key displays without a notice. + variable_set('update_script_test_requirement_type', REQUIREMENT_INFO); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->assertText('This is a requirements info provided by the update_script_test module.'); + $this->assertNoText('Notice: Undefined index: value in theme_status_report()'); } /** @@ -2712,3 +2968,122 @@ } } +/** + * Test case for drupal_valid_token(). + */ +class SystemValidTokenTest extends DrupalUnitTestCase { + + /** + * Flag to indicate whether PHP error reportings should be asserted. + * + * @var bool + */ + protected $assertErrors = TRUE; + + public static function getInfo() { + return array( + 'name' => 'Token validation', + 'description' => 'Test the security token validation.', + 'group' => 'System', + ); + } + + /** + * Tests invalid invocations of drupal_valid_token() that must return FALSE. + */ + public function testTokenValidation() { + // The following checks will throw PHP notices, so we disable error + // assertions. + $this->assertErrors = FALSE; + $this->assertFalse(drupal_valid_token(NULL, new stdClass()), 'Token NULL, value object returns FALSE.'); + $this->assertFalse(drupal_valid_token(0, array()), 'Token 0, value array returns FALSE.'); + $this->assertFalse(drupal_valid_token('', array()), "Token '', value array returns FALSE."); + $this->assertFalse('' === drupal_get_token(array()), 'Token generation does not return an empty string on invalid parameters.'); + $this->assertErrors = TRUE; + + $this->assertFalse(drupal_valid_token(TRUE, 'foo'), 'Token TRUE, value foo returns FALSE.'); + $this->assertFalse(drupal_valid_token(0, 'foo'), 'Token 0, value foo returns FALSE.'); + } + + /** + * Overrides DrupalTestCase::errorHandler(). + */ + public function errorHandler($severity, $message, $file = NULL, $line = NULL) { + if ($this->assertErrors) { + return parent::errorHandler($severity, $message, $file, $line); + } + 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 -Naur drupal-7.23/modules/system/system.updater.inc drupal-7.66/modules/system/system.updater.inc --- drupal-7.23/modules/system/system.updater.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/system/system.updater.inc 2019-04-17 22:20:46.000000000 +0200 @@ -24,7 +24,7 @@ * found on your system, and if there was a copy in sites/all, we'd see it. */ public function getInstallDirectory() { - if ($relative_path = drupal_get_path('module', $this->name)) { + if ($this->isInstalled() && ($relative_path = drupal_get_path('module', $this->name))) { $relative_path = dirname($relative_path); } else { @@ -34,7 +34,7 @@ } public function isInstalled() { - return (bool) drupal_get_path('module', $this->name); + return (bool) drupal_get_filename('module', $this->name, NULL, FALSE); } public static function canUpdateDirectory($directory) { @@ -109,7 +109,7 @@ * found on your system, and if there was a copy in sites/all, we'd see it. */ public function getInstallDirectory() { - if ($relative_path = drupal_get_path('theme', $this->name)) { + if ($this->isInstalled() && ($relative_path = drupal_get_path('theme', $this->name))) { $relative_path = dirname($relative_path); } else { @@ -119,7 +119,7 @@ } public function isInstalled() { - return (bool) drupal_get_path('theme', $this->name); + return (bool) drupal_get_filename('theme', $this->name, NULL, FALSE); } static function canUpdateDirectory($directory) { diff -Naur drupal-7.23/modules/system/tests/cron_queue_test.info drupal-7.66/modules/system/tests/cron_queue_test.info --- drupal-7.23/modules/system/tests/cron_queue_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/system/tests/cron_queue_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = Cron Queue test +description = 'Support module for the cron queue runner.' +package = Testing +version = VERSION +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/system/tests/cron_queue_test.module drupal-7.66/modules/system/tests/cron_queue_test.module --- drupal-7.23/modules/system/tests/cron_queue_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/system/tests/cron_queue_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,27 @@ + 'cron_queue_test_exception', + ); + $queues['cron_queue_test_callback'] = array( + 'worker callback' => array('CronQueueTestCallbackClass', 'foo'), + ); + + return $queues; +} + +function cron_queue_test_exception($item) { + throw new Exception('That is not supposed to happen.'); +} + +class CronQueueTestCallbackClass { + + static public function foo() { + // Do nothing. + } + +} diff -Naur drupal-7.23/modules/system/tests/system_cron_test.info drupal-7.66/modules/system/tests/system_cron_test.info --- drupal-7.23/modules/system/tests/system_cron_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/system/tests/system_cron_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = System Cron Test +description = 'Support module for testing the system_cron().' +package = Testing +version = VERSION +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/system/tests/system_cron_test.module drupal-7.66/modules/system/tests/system_cron_test.module --- drupal-7.23/modules/system/tests/system_cron_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/system/tests/system_cron_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,15 @@ + $bundle->type, 'settings' => array(), 'description' => 'Debris left over after upgrade from Drupal 6', + 'required' => FALSE, 'widget' => array( 'type' => 'taxonomy_autocomplete', 'module' => 'taxonomy', @@ -557,7 +558,7 @@ // of term references stored so far for the current revision, which // provides the delta value for each term reference data insert. The // deltas are reset for each new revision. - + $conditions = array( 'type' => 'taxonomy_term_reference', 'deleted' => 0, @@ -638,6 +639,10 @@ 'type' => 'int', 'not null' => FALSE, ), + 'status' => array( + 'type' => 'int', + 'not null' => FALSE, + ), 'is_current' => array( 'type' => 'int', 'unsigned' => TRUE, @@ -670,6 +675,7 @@ $query->addField('n', 'type'); $query->addField('n2', 'created'); $query->addField('n2', 'sticky'); + $query->addField('n2', 'status'); $query->addField('n2', 'nid', 'is_current'); // This query must return a consistent ordering across multiple calls. // We need them ordered by node vid (since we use that to decide when @@ -698,7 +704,7 @@ // We do each pass in batches of 1000. $batch = 1000; - $result = db_query_range('SELECT vocab_id, tid, nid, vid, type, created, sticky, is_current FROM {taxonomy_update_7005} ORDER BY n', $sandbox['last'], $batch); + $result = db_query_range('SELECT vocab_id, tid, nid, vid, type, created, sticky, status, is_current FROM {taxonomy_update_7005} ORDER BY n', $sandbox['last'], $batch); if (isset($sandbox['cursor'])) { $values = $sandbox['cursor']['values']; $deltas = $sandbox['cursor']['deltas']; @@ -765,12 +771,14 @@ // is_current column is a node ID if this revision is also current. if ($record->is_current) { db_insert($table_name)->fields($columns)->values($values)->execute(); - - // Update the {taxonomy_index} table. - db_insert('taxonomy_index') - ->fields(array('nid', 'tid', 'sticky', 'created',)) - ->values(array($record->nid, $record->tid, $record->sticky, $record->created)) - ->execute(); + // Only insert a record in the taxonomy index if the node is published. + if ($record->status) { + // Update the {taxonomy_index} table. + db_insert('taxonomy_index') + ->fields(array('nid', 'tid', 'sticky', 'created',)) + ->values(array($record->nid, $record->tid, $record->sticky, $record->created)) + ->execute(); + } } } @@ -888,3 +896,44 @@ )); } +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Drop unpublished nodes from the index. + */ +function taxonomy_update_7011(&$sandbox) { + // Initialize information needed by the batch update system. + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['max'] = db_query('SELECT COUNT(DISTINCT n.nid) FROM {node} n INNER JOIN {taxonomy_index} t ON n.nid = t.nid WHERE n.status = :status', array(':status' => NODE_NOT_PUBLISHED))->fetchField(); + // If there's no data, don't bother with the extra work. + if (empty($sandbox['max'])) { + return; + } + } + + // Process records in groups of 5000. + $limit = 5000; + $nids = db_query_range('SELECT DISTINCT n.nid FROM {node} n INNER JOIN {taxonomy_index} t ON n.nid = t.nid WHERE n.status = :status', 0, $limit, array(':status' => NODE_NOT_PUBLISHED))->fetchCol(); + if (!empty($nids)) { + db_delete('taxonomy_index') + ->condition('nid', $nids) + ->execute(); + } + + // Update our progress information for the batch update. + $sandbox['progress'] += $limit; + + // Indicate our current progress to the batch update system, if the update is + // not yet complete. + if ($sandbox['progress'] < $sandbox['max']) { + $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max']; + } +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ diff -Naur drupal-7.23/modules/taxonomy/taxonomy.module drupal-7.66/modules/taxonomy/taxonomy.module --- drupal-7.23/modules/taxonomy/taxonomy.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/taxonomy/taxonomy.module 2019-04-17 22:20:46.000000000 +0200 @@ -25,7 +25,7 @@ $output .= '

          ' . t('Uses') . '

          '; $output .= '
          '; $output .= '
          ' . t('Creating vocabularies') . '
          '; - $output .= '
          ' . t('Users with sufficient permissions can create vocabularies and terms through the Taxonomy page. The page listing the terms provides a drag-and-drop interface for controlling the order of the terms and sub-terms within a vocabulary, in a hierarchical fashion. A controlled vocabulary classifying music by genre with terms and sub-terms could look as follows:', array('@taxo' => url('admin/structure/taxonomy'), '@perm' => url('admin/people/permissions', array('fragment'=>'module-taxonomy')))); + $output .= '
          ' . t('Users with sufficient permissions can create vocabularies and terms through the Taxonomy page. The page listing the terms provides a drag-and-drop interface for controlling the order of the terms and sub-terms within a vocabulary, in a hierarchical fashion. A controlled vocabulary classifying music by genre with terms and sub-terms could look as follows:', array('@taxo' => url('admin/structure/taxonomy'), '@perm' => url('admin/people/permissions', array('fragment' => 'module-taxonomy')))); $output .= '
          • ' . t('vocabulary: Music') . '
          • '; $output .= '
            • ' . t('term: Jazz') . '
            • '; $output .= '
              • ' . t('sub-term: Swing') . '
              • '; @@ -283,6 +283,8 @@ 'title' => 'Taxonomy term', 'title callback' => 'taxonomy_term_title', 'title arguments' => array(2), + // The page callback also invokes drupal_set_title() in case + // the menu router's title is overridden by a menu link. 'page callback' => 'taxonomy_term_page', 'page arguments' => array(2), 'access arguments' => array('access content'), @@ -457,7 +459,12 @@ } /** - * Delete a vocabulary. + * Deletes a vocabulary. + * + * This will update all Taxonomy fields so that they don't reference the + * deleted vocabulary. It also will delete fields that have no remaining + * vocabulary references. All taxonomy terms of the deleted vocabulary + * will be deleted as well. * * @param $vid * A vocabulary ID. @@ -748,7 +755,7 @@ * @param term * A taxonomy term object. * @return - * A $page element suitable for use by drupal_page_render(). + * A $page element suitable for use by drupal_render(). */ function taxonomy_term_show($term) { return taxonomy_term_view_multiple(array($term->tid => $term), 'full'); @@ -771,15 +778,26 @@ * 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; } @@ -812,12 +830,7 @@ $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'; @@ -1012,7 +1025,7 @@ $query->join('taxonomy_term_hierarchy', 'h', 'h.parent = t.tid'); $query->addField('t', 'tid'); $query->condition('h.tid', $tid); - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); $query->orderBy('t.weight'); $query->orderBy('t.name'); $tids = $query->execute()->fetchCol(); @@ -1070,7 +1083,7 @@ if ($vid) { $query->condition('t.vid', $vid); } - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); $query->orderBy('t.weight'); $query->orderBy('t.name'); $tids = $query->execute()->fetchCol(); @@ -1118,7 +1131,7 @@ $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid'); $result = $query ->addTag('translatable') - ->addTag('term_access') + ->addTag('taxonomy_term_access') ->fields('t') ->fields('h', array('parent')) ->condition('t.vid', $vid) @@ -1238,7 +1251,7 @@ protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { $query = parent::buildQuery($ids, $conditions, $revision_id); $query->addTag('translatable'); - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); // When name is passed as a condition use LIKE. if (isset($conditions['name'])) { $query_conditions = &$query->conditions(); @@ -1703,13 +1716,15 @@ } /** - * Title callback for term pages. + * Title callback: Returns the title of the taxonomy term. * * @param $term * A term object. * * @return - * The term name to be used as the page title. + * An unsanitized string that is the title of the taxonomy term. + * + * @see taxonomy_menu() */ function taxonomy_term_title($term) { return $term->name; diff -Naur drupal-7.23/modules/taxonomy/taxonomy.pages.inc drupal-7.66/modules/taxonomy/taxonomy.pages.inc --- drupal-7.23/modules/taxonomy/taxonomy.pages.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/taxonomy/taxonomy.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -150,7 +150,7 @@ $query = db_select('taxonomy_term_data', 't'); $query->addTag('translatable'); - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); // Do not select already entered terms. if (!empty($tags_typed)) { diff -Naur drupal-7.23/modules/taxonomy/taxonomy.test drupal-7.66/modules/taxonomy/taxonomy.test --- drupal-7.23/modules/taxonomy/taxonomy.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/taxonomy/taxonomy.test 2019-04-17 22:20:46.000000000 +0200 @@ -690,15 +690,20 @@ $term_objects[$key] = reset($term_objects[$key]); } + // Test editing the node. + $node = $this->drupalGetNodeByTitle($edit["title"]); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + foreach ($terms as $term) { + $this->assertText($term, 'The term was retained after edit and still appears on the node page.'); + } + // Delete term 1. $this->drupalPost('taxonomy/term/' . $term_objects['term1']->tid . '/edit', array(), t('Delete')); $this->drupalPost(NULL, NULL, t('Delete')); $term_names = array($term_objects['term2']->name, $term_objects['term3']->name); - // Get the node. - $node = $this->drupalGetNodeByTitle($edit["title"]); + // Get the node and verify that the terms that should be there still are. $this->drupalGet('node/' . $node->nid); - foreach ($term_names as $term_name) { $this->assertText($term_name, format_string('The term %name appears on the node page after one term %deleted was deleted', array('%name' => $term_name, '%deleted' => $term_objects['term1']->name))); } @@ -1020,7 +1025,7 @@ function setUp() { parent::setUp('taxonomy'); - $this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access', 'administer content types')); + $this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access', 'administer content types', 'administer fields')); $this->drupalLogin($this->admin_user); $this->vocabulary = $this->createVocabulary(); @@ -1978,3 +1983,113 @@ } } + +/** + * Tests that appropriate query tags are added. + */ +class TaxonomyQueryAlterTestCase extends TaxonomyWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Taxonomy query tags', + 'description' => 'Verifies that taxonomy_term_access tags are added to queries.', + 'group' => 'Taxonomy', + ); + } + + public function setUp() { + parent::setUp('taxonomy_test'); + } + + /** + * Tests that appropriate tags are added when querying the database. + */ + public function testTaxonomyQueryAlter() { + // Create a new vocabulary and add a few terms to it. + $vocabulary = $this->createVocabulary(); + $terms = array(); + for ($i = 0; $i < 5; $i++) { + $terms[$i] = $this->createTerm($vocabulary); + } + + // Set up hierarchy. Term 2 is a child of 1. + $terms[2]->parent = array($terms[1]->tid); + taxonomy_term_save($terms[2]); + + $this->setupQueryTagTestHooks(); + $loaded_term = taxonomy_term_load($terms[0]->tid); + $this->assertEqual($loaded_term->tid, $terms[0]->tid, 'First term was loaded'); + $this->assertQueryTagTestResult(1, 'taxonomy_term_load()'); + + $this->setupQueryTagTestHooks(); + $loaded_terms = taxonomy_get_tree($vocabulary->vid); + $this->assertEqual(count($loaded_terms), count($terms), 'All terms were loaded'); + $this->assertQueryTagTestResult(1, 'taxonomy_get_tree()'); + + $this->setupQueryTagTestHooks(); + $loaded_terms = taxonomy_get_parents($terms[2]->tid); + $this->assertEqual(count($loaded_terms), 1, 'All parent terms were loaded'); + $this->assertQueryTagTestResult(2, 'taxonomy_get_parents()'); + + $this->setupQueryTagTestHooks(); + $loaded_terms = taxonomy_get_children($terms[1]->tid); + $this->assertEqual(count($loaded_terms), 1, 'All child terms were loaded'); + $this->assertQueryTagTestResult(2, 'taxonomy_get_children()'); + + $this->setupQueryTagTestHooks(); + $query = db_select('taxonomy_term_data', 't'); + $query->addField('t', 'tid'); + $query->addTag('taxonomy_term_access'); + $tids = $query->execute()->fetchCol(); + $this->assertEqual(count($tids), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom db_select() with taxonomy_term_access tag (preferred)'); + + $this->setupQueryTagTestHooks(); + $query = db_select('taxonomy_term_data', 't'); + $query->addField('t', 'tid'); + $query->addTag('term_access'); + $tids = $query->execute()->fetchCol(); + $this->assertEqual(count($tids), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom db_select() with term_access tag (deprecated)'); + + $this->setupQueryTagTestHooks(); + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'taxonomy_term'); + $query->addTag('taxonomy_term_access'); + $result = $query->execute(); + $this->assertEqual(count($result['taxonomy_term']), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom EntityFieldQuery with taxonomy_term_access tag (preferred)'); + + $this->setupQueryTagTestHooks(); + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'taxonomy_term'); + $query->addTag('term_access'); + $result = $query->execute(); + $this->assertEqual(count($result['taxonomy_term']), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom EntityFieldQuery with term_access tag (deprecated)'); + } + + /** + * Sets up the hooks in the test module. + */ + protected function setupQueryTagTestHooks() { + taxonomy_terms_static_reset(); + variable_set('taxonomy_test_query_alter', 0); + variable_set('taxonomy_test_query_term_access_alter', 0); + variable_set('taxonomy_test_query_taxonomy_term_access_alter', 0); + } + + /** + * Verifies invocation of the hooks in the test module. + * + * @param int $expected_invocations + * The number of times the hooks are expected to have been invoked. + * @param string $method + * A string describing the invoked function which generated the query. + */ + protected function assertQueryTagTestResult($expected_invocations, $method) { + $this->assertIdentical($expected_invocations, variable_get('taxonomy_test_query_alter'), 'hook_query_alter() invoked when executing ' . $method); + $this->assertIdentical($expected_invocations, variable_get('taxonomy_test_query_term_access_alter'), 'Deprecated hook_query_term_access_alter() invoked when executing ' . $method); + $this->assertIdentical($expected_invocations, variable_get('taxonomy_test_query_taxonomy_term_access_alter'), 'Preferred hook_query_taxonomy_term_access_alter() invoked when executing ' . $method); + } + +} diff -Naur drupal-7.23/modules/toolbar/toolbar.info drupal-7.66/modules/toolbar/toolbar.info --- drupal-7.23/modules/toolbar/toolbar.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/toolbar/toolbar.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ package = Core version = VERSION -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/tracker/tracker.info drupal-7.66/modules/tracker/tracker.info --- drupal-7.23/modules/tracker/tracker.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/tracker/tracker.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ core = 7.x files[] = tracker.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/tracker/tracker.module drupal-7.66/modules/tracker/tracker.module --- drupal-7.23/modules/tracker/tracker.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/tracker/tracker.module 2019-04-17 22:20:46.000000000 +0200 @@ -263,7 +263,7 @@ )) ->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 @@ '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 -Naur drupal-7.23/modules/tracker/tracker.pages.inc drupal-7.66/modules/tracker/tracker.pages.inc --- drupal-7.23/modules/tracker/tracker.pages.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/tracker/tracker.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -120,7 +120,6 @@ ); $page['pager'] = array( '#theme' => 'pager', - '#quantity' => 25, '#weight' => 10, ); $page['#sorted'] = TRUE; diff -Naur drupal-7.23/modules/tracker/tracker.test drupal-7.66/modules/tracker/tracker.test --- drupal-7.23/modules/tracker/tracker.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/tracker/tracker.test 2019-04-17 22:20:46.000000000 +0200 @@ -151,7 +151,6 @@ $node = $this->drupalCreateNode(array( 'comment' => 2, - 'title' => array(LANGUAGE_NONE => array(array('value' => $this->randomName(8)))), )); // Add a comment to the page. @@ -183,6 +182,72 @@ } /** + * 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. */ function testTrackerCronIndexing() { diff -Naur drupal-7.23/modules/translation/tests/translation_test.info drupal-7.66/modules/translation/tests/translation_test.info --- drupal-7.23/modules/translation/tests/translation_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/translation/tests/translation_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/translation/translation.info drupal-7.66/modules/translation/translation.info --- drupal-7.23/modules/translation/translation.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/translation/translation.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ core = 7.x files[] = translation.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/translation/translation.module drupal-7.66/modules/translation/translation.module --- drupal-7.23/modules/translation/translation.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/translation/translation.module 2019-04-17 22:20:46.000000000 +0200 @@ -336,9 +336,13 @@ '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 @@ )) ->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); } } } @@ -414,17 +428,22 @@ * A node object. */ function translation_remove_from_set($node) { - if (isset($node->tnid)) { + if (isset($node->tnid) && $node->tnid) { $query = db_update('node') ->fields(array( '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 @@ ->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 -Naur drupal-7.23/modules/trigger/tests/trigger_test.info drupal-7.66/modules/trigger/tests/trigger_test.info --- drupal-7.23/modules/trigger/tests/trigger_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/trigger/tests/trigger_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/trigger/trigger.info drupal-7.66/modules/trigger/trigger.info --- drupal-7.23/modules/trigger/trigger.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/trigger/trigger.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = trigger.test configure = admin/structure/trigger -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/trigger/trigger.test drupal-7.66/modules/trigger/trigger.test --- drupal-7.23/modules/trigger/trigger.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/trigger/trigger.test 2019-04-17 22:20:46.000000000 +0200 @@ -82,10 +82,10 @@ $edit[$info['property']] = !$info['expected']; $this->drupalPost('node/add/page', $edit, t('Save')); // Make sure the text we want appears. - $this->assertRaw(t('!post %title has been created.', array('!post' => 'Basic page', '%title' => $edit["title"])), t('Make sure the Basic page has actually been created')); + $this->assertRaw(t('!post %title has been created.', array('!post' => 'Basic page', '%title' => $edit["title"])), 'Make sure the Basic page has actually been created'); // Action should have been fired. $loaded_node = $this->drupalGetNodeByTitle($edit["title"]); - $this->assertTrue($loaded_node->$info['property'] == $info['expected'], t('Make sure the @action action fired.', array('@action' => $info['name']))); + $this->assertTrue($loaded_node->{$info['property']} == $info['expected'], format_string('Make sure the @action action fired.', array('@action' => $info['name']))); // Leave action assigned for next test // There should be an error when the action is assigned to the trigger @@ -94,13 +94,13 @@ // This action already assigned in this test. $edit = array('aid' => $hash); $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'), array(), array(), 'trigger-node-presave-assign-form'); - $this->assertRaw(t('The action you chose is already assigned to that trigger.'), t('Check to make sure an error occurs when assigning an action to a trigger twice.')); + $this->assertRaw(t('The action you chose is already assigned to that trigger.'), 'Check to make sure an error occurs when assigning an action to a trigger twice.'); // The action should be able to be unassigned from a trigger. $this->drupalPost('admin/structure/trigger/unassign/node/node_presave/' . $hash, array(), t('Unassign')); - $this->assertRaw(t('Action %action has been unassigned.', array('%action' => ucfirst($info['name']))), t('Check to make sure the @action action can be unassigned from the trigger.', array('@action' => $info['name']))); + $this->assertRaw(t('Action %action has been unassigned.', array('%action' => ucfirst($info['name']))), format_string('Check to make sure the @action action can be unassigned from the trigger.', array('@action' => $info['name']))); $assigned = db_query("SELECT COUNT(*) FROM {trigger_assignments} WHERE aid IN (:keys)", array(':keys' => $content_actions))->fetchField(); - $this->assertFalse($assigned, t('Check to make sure unassign worked properly at the database level.')); + $this->assertFalse($assigned, 'Check to make sure unassign worked properly at the database level.'); } } @@ -132,7 +132,7 @@ ); $this->drupalPost('admin/content', $edit, t('Update')); $count = variable_get('trigger_test_generic_any_action', 0); - $this->assertTrue($count == 2, t('Action was triggered 2 times. Actual: %count', array('%count' => $count))); + $this->assertTrue($count == 2, format_string('Action was triggered 2 times. Actual: %count', array('%count' => $count))); } /** @@ -242,11 +242,11 @@ // Make sure the non-configurable action has fired. $action_run = variable_get('trigger_test_system_cron_action', FALSE); - $this->assertTrue($action_run, t('Check that the cron run triggered the test action.')); + $this->assertTrue($action_run, 'Check that the cron run triggered the test action.'); // Make sure that both configurable actions have fired. $action_run = variable_get('trigger_test_system_cron_conf_action', 0) == 2; - $this->assertTrue($action_run, t('Check that the cron run triggered both complex actions.')); + $this->assertTrue($action_run, 'Check that the cron run triggered both complex actions.'); } } @@ -321,7 +321,7 @@ $trigger_type = preg_replace('/_.*/', '', $trigger); $this->drupalPost("admin/structure/trigger/$trigger_type", $edit, t('Assign'), array(), array(), $form_html_id); $actions = trigger_get_assigned_actions($trigger); - $this->assertTrue(!empty($actions[$action]), t('Simple action @action assigned to trigger @trigger', array('@action' => $action, '@trigger' => $trigger))); + $this->assertTrue(!empty($actions[$action]), format_string('Simple action @action assigned to trigger @trigger', array('@action' => $action, '@trigger' => $trigger))); } /** @@ -402,7 +402,7 @@ function assertSystemMessageTokenReplacement($trigger, $account) { $expected = $this->generateTokenExpandedComparison($trigger, $account); $this->assertText($expected, - t('Expected system message to contain token-replaced text "@expected" found in configured system message action', array('@expected' => $expected )) ); + format_string('Expected system message to contain token-replaced text "@expected" found in configured system message action', array('@expected' => $expected )) ); } @@ -421,7 +421,7 @@ $expected = $this->generateTokenExpandedComparison($trigger, $account); $this->assertMailString('subject', $expected, $email_depth); $this->assertMailString('body', $expected, $email_depth); - $this->assertMail('to', $account->mail, t('Mail sent to correct destination')); + $this->assertMail('to', $account->mail, 'Mail sent to correct destination'); } } @@ -517,7 +517,7 @@ $this->drupalPost("node/{$node->nid}", array('comment_body[und][0][value]' => t("my comment"), 'subject' => t("my comment subject")), t('Save')); // Posting a comment should have blocked this user. $account = user_load($test_user->uid, TRUE); - $this->assertTrue($account->status == 0, t('Account is blocked')); + $this->assertTrue($account->status == 0, 'Account is blocked'); $comment_author_uid = $account->uid; // Now rehabilitate the comment author so it can be be blocked again when // the comment is updated. @@ -529,7 +529,7 @@ // Our original comment will have been comment 1. $this->drupalPost("comment/1/edit", array('comment_body[und][0][value]' => t("my comment, updated"), 'subject' => t("my comment subject")), t('Save')); $comment_author_account = user_load($comment_author_uid, TRUE); - $this->assertTrue($comment_author_account->status == 0, t('Comment author account (uid=@uid) is blocked after update to comment', array('@uid' => $comment_author_uid))); + $this->assertTrue($comment_author_account->status == 0, format_string('Comment author account (uid=@uid) is blocked after update to comment', array('@uid' => $comment_author_uid))); // Verify that the comment was updated. $test_user = $this->drupalCreateUser(array('administer actions', 'create article content', 'access comments', 'administer comments', 'skip comment approval', 'edit own comments')); @@ -589,7 +589,7 @@ $this->drupalPost('admin/people/create', $edit, t('Create new account')); // Verify that the action variable has been set. - $this->assertTrue(variable_get($action_id, FALSE), t('Check that creating a user triggered the test action.')); + $this->assertTrue(variable_get($action_id, FALSE), 'Check that creating a user triggered the test action.'); // Reset the action variable. variable_set($action_id, FALSE); @@ -608,8 +608,8 @@ // Verify that the action has been assigned to the correct hook. $actions = trigger_get_assigned_actions('user_login'); - $this->assertEqual(1, count($actions), t('One Action assigned to the hook')); - $this->assertEqual($actions[$aid]['label'], $action_edit['actions_label'], t('Correct action label found.')); + $this->assertEqual(1, count($actions), 'One Action assigned to the hook'); + $this->assertEqual($actions[$aid]['label'], $action_edit['actions_label'], 'Correct action label found.'); // User should get the configured message at login. $contact_user = $this->drupalCreateUser(array('access site-wide contact form'));; @@ -643,7 +643,7 @@ $this->drupalPost(NULL, $edit, t('Save')); // Verify that the action variable has been set. - $this->assertTrue(variable_get($action_id, FALSE), t('Check that creating a comment triggered the action.')); + $this->assertTrue(variable_get($action_id, FALSE), 'Check that creating a comment triggered the action.'); } /** @@ -679,7 +679,7 @@ taxonomy_term_save($term); // Verify that the action variable has been set. - $this->assertTrue(variable_get($action_id, FALSE), t('Check that creating a taxonomy term triggered the action.')); + $this->assertTrue(variable_get($action_id, FALSE), 'Check that creating a taxonomy term triggered the action.'); } } @@ -723,10 +723,10 @@ $edit["title"] = '!SimpleTest test node! ' . $this->randomName(10); $edit["body[$langcode][0][value]"] = '!SimpleTest test body! ' . $this->randomName(32) . ' ' . $this->randomName(32); $this->drupalPost('node/add/page', $edit, t('Save')); - $this->assertRaw(t('!post %title has been created.', array('!post' => 'Basic page', '%title' => $edit["title"])), t('Make sure the Basic page has actually been created')); + $this->assertRaw(t('!post %title has been created.', array('!post' => 'Basic page', '%title' => $edit["title"])), 'Make sure the Basic page has actually been created'); // Action should have been fired. - $this->assertTrue(variable_get('trigger_test_generic_any_action', FALSE), t('Trigger test action successfully fired.')); + $this->assertTrue(variable_get('trigger_test_generic_any_action', FALSE), 'Trigger test action successfully fired.'); // Disable the module that provides the action and make sure the trigger // doesn't white screen. @@ -737,7 +737,7 @@ // If the node body was updated successfully we have dealt with the // unavailable action. - $this->assertRaw(t('!post %title has been updated.', array('!post' => 'Basic page', '%title' => $edit["title"])), t('Make sure the Basic page can be updated with the missing trigger function.')); + $this->assertRaw(t('!post %title has been updated.', array('!post' => 'Basic page', '%title' => $edit["title"])), 'Make sure the Basic page can be updated with the missing trigger function.'); } } diff -Naur drupal-7.23/modules/update/tests/aaa_update_test.info drupal-7.66/modules/update/tests/aaa_update_test.info --- drupal-7.23/modules/update/tests/aaa_update_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/update/tests/aaa_update_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/tests/bbb_update_test.info drupal-7.66/modules/update/tests/bbb_update_test.info --- drupal-7.23/modules/update/tests/bbb_update_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/update/tests/bbb_update_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/tests/ccc_update_test.info drupal-7.66/modules/update/tests/ccc_update_test.info --- drupal-7.23/modules/update/tests/ccc_update_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/update/tests/ccc_update_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info drupal-7.66/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info --- drupal-7.23/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,9 @@ +name = Update test admin theme +description = Test theme which is used as admin theme. +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info drupal-7.66/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info --- drupal-7.23/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info 2019-04-17 22:39:36.000000000 +0200 @@ -3,8 +3,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info drupal-7.66/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info --- drupal-7.23/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ base theme = update_test_basetheme hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/tests/update_test.info drupal-7.66/modules/update/tests/update_test.info --- drupal-7.23/modules/update/tests/update_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/update/tests/update_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/tests/update_test.module drupal-7.66/modules/update/tests/update_test.module --- drupal-7.23/modules/update/tests/update_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/tests/update_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -11,6 +11,7 @@ function update_test_system_theme_info() { $themes['update_test_basetheme'] = drupal_get_path('module', 'update_test') . '/themes/update_test_basetheme/update_test_basetheme.info'; $themes['update_test_subtheme'] = drupal_get_path('module', 'update_test') . '/themes/update_test_subtheme/update_test_subtheme.info'; + $themes['update_test_admintheme'] = drupal_get_path('module', 'update_test') . '/themes/update_test_admintheme/update_test_admintheme.info'; return $themes; } diff -Naur drupal-7.23/modules/update/update.authorize.inc drupal-7.66/modules/update/update.authorize.inc --- drupal-7.23/modules/update/update.authorize.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/update.authorize.inc 2019-04-17 22:20:46.000000000 +0200 @@ -97,7 +97,9 @@ } /** - * Batch callback: Copies project to its proper place when authorized to do so. + * Implements callback_batch_operation(). + * + * Copies project to its proper place when authorized to do so. * * @param string $project * The canonical short name of the project being installed. @@ -168,7 +170,9 @@ } /** - * Batch callback: Performs actions when the authorized update batch is done. + * Implements callback_batch_finished(). + * + * Performs actions when the authorized update batch is done. * * This processes the results and stashes them into SESSION such that * authorize.php will render a report. Also responsible for putting the site @@ -235,7 +239,9 @@ } /** - * Batch callback: Performs actions when the authorized install batch is done. + * Implements callback_batch_finished(). + * + * Performs actions when the authorized install batch is done. * * This processes the results and stashes them into SESSION such that * authorize.php will render a report. Also responsible for putting the site diff -Naur drupal-7.23/modules/update/update.compare.inc drupal-7.66/modules/update/update.compare.inc --- drupal-7.23/modules/update/update.compare.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/update.compare.inc 2019-04-17 22:20:46.000000000 +0200 @@ -104,7 +104,13 @@ * @see update_get_projects() */ function _update_process_info_list(&$projects, $list, $project_type, $status) { + $admin_theme = variable_get('admin_theme', 'seven'); foreach ($list as $file) { + // The admin theme is a special case. It should always be considered enabled + // for the purposes of update checking. + if ($file->name === $admin_theme) { + $file->status = TRUE; + } // A disabled base theme of an enabled sub-theme still has all of its code // run by the sub-theme, so we include it in our "enabled" projects list. if ($status && !$file->status && !empty($file->sub_themes)) { @@ -417,14 +423,15 @@ * 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 -Naur drupal-7.23/modules/update/update.fetch.inc drupal-7.66/modules/update/update.fetch.inc --- drupal-7.23/modules/update/update.fetch.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/update.fetch.inc 2019-04-17 22:20:46.000000000 +0200 @@ -29,7 +29,9 @@ } /** - * Batch callback: Processes a step in batch for fetching available update data. + * Implements callback_batch_operation(). + * + * Processes a step in batch for fetching available update data. * * @param $context * Reference to an array used for Batch API storage. @@ -77,7 +79,9 @@ } /** - * Batch callback: Performs actions when all fetch tasks have been completed. + * Implements callback_batch_finished(). + * + * Performs actions when all fetch tasks have been completed. * * @param $success * TRUE if the batch operation was successful; FALSE if there were errors. @@ -289,7 +293,7 @@ $url = _update_get_fetch_url_base($project); $url .= '/' . $name . '/' . DRUPAL_CORE_COMPATIBILITY; - // Only append usage infomation if we have a site key and the project is + // Only append usage information if we have a site key and the project is // enabled. We do not want to record usage statistics for disabled projects. if (!empty($site_key) && (strpos($project['project_type'], 'disabled') === FALSE)) { // Append the site key. diff -Naur drupal-7.23/modules/update/update.info drupal-7.66/modules/update/update.info --- drupal-7.23/modules/update/update.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/update/update.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ files[] = update.test configure = admin/reports/updates/settings -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/update/update.manager.inc drupal-7.66/modules/update/update.manager.inc --- drupal-7.23/modules/update/update.manager.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/update.manager.inc 2019-04-17 22:20:46.000000000 +0200 @@ -59,7 +59,7 @@ * @see update_menu() * @ingroup forms */ -function update_manager_update_form($form, $form_state = array(), $context) { +function update_manager_update_form($form, $form_state, $context) { if (!_update_manager_check_backends($form, 'update')) { return $form; } @@ -335,6 +335,8 @@ } /** + * Implements callback_batch_finished(). + * * Batch callback: Performs actions when the download batch is completed. * * @param $success @@ -847,7 +849,9 @@ } /** - * Batch callback: Downloads, unpacks, and verifies a project. + * Implements callback_batch_operation(). + * + * Downloads, unpacks, and verifies a project. * * This function assumes that the provided URL points to a file archive of some * sort. The URL can have any scheme that we have a file stream wrapper to diff -Naur drupal-7.23/modules/update/update.module drupal-7.66/modules/update/update.module --- drupal-7.23/modules/update/update.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/update.module 2019-04-17 22:20:46.000000000 +0200 @@ -71,7 +71,7 @@ /** * Maximum number of seconds to try fetching available update data at a time. */ -define('UPDATE_MAX_FETCH_TIME', 5); +define('UPDATE_MAX_FETCH_TIME', 30); /** * Implements hook_help(). @@ -278,12 +278,15 @@ ), 'update_report' => array( 'variables' => array('data' => NULL), + 'file' => 'update.report.inc', ), 'update_version' => array( 'variables' => array('version' => NULL, 'tag' => NULL, 'class' => array()), + 'file' => 'update.report.inc', ), 'update_status_label' => array( 'variables' => array('status' => NULL), + 'file' => 'update.report.inc', ), ); } diff -Naur drupal-7.23/modules/update/update.settings.inc drupal-7.66/modules/update/update.settings.inc --- drupal-7.23/modules/update/update.settings.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/update.settings.inc 2019-04-17 22:20:46.000000000 +0200 @@ -26,7 +26,7 @@ $form['update_check_disabled'] = array( '#type' => 'checkbox', - '#title' => t('Check for updates of disabled modules and themes'), + '#title' => t('Check for updates of disabled and uninstalled modules and themes'), '#default_value' => variable_get('update_check_disabled', FALSE), ); @@ -98,10 +98,11 @@ * Form submission handler for update_settings(). * * Also invalidates the cache of available updates if the "Check for updates of - * disabled modules and themes" setting is being changed. The available updates - * report needs to refetch available update data after this setting changes or - * it would show misleading things (e.g., listing the disabled projects on the - * site with the "No available releases found" warning). + * disabled and uninstalled modules and themes" setting is being changed. The + * available updates report needs to refetch available update data after this + * setting changes or it would show misleading things (e.g., listing the + * disabled projects on the site with the "No available releases found" + * warning). * * @see update_settings_validate() */ diff -Naur drupal-7.23/modules/update/update.test drupal-7.66/modules/update/update.test --- drupal-7.23/modules/update/update.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/update/update.test 2019-04-17 22:20:46.000000000 +0200 @@ -463,6 +463,55 @@ } /** + * Tests that the admin theme is always notified about security updates. + */ + function testUpdateAdminThemeSecurityUpdate() { + // Disable the admin theme. + db_update('system') + ->fields(array('status' => 0)) + ->condition('type', 'theme') + ->condition('name', 'update_test_%', 'LIKE') + ->execute(); + + variable_set('admin_theme', 'update_test_admintheme'); + + // Define the initial state for core and the themes. + $system_info = array( + '#all' => array( + 'version' => '7.0', + ), + 'update_test_admintheme' => array( + 'project' => 'update_test_admintheme', + 'version' => '7.x-1.0', + 'hidden' => FALSE, + ), + 'update_test_basetheme' => array( + 'project' => 'update_test_basetheme', + 'version' => '7.x-1.1', + 'hidden' => FALSE, + ), + 'update_test_subtheme' => array( + 'project' => 'update_test_subtheme', + 'version' => '7.x-1.0', + 'hidden' => FALSE, + ), + ); + variable_set('update_test_system_info', $system_info); + variable_set('update_check_disabled', FALSE); + $xml_mapping = array( + // This is enough because we don't check the update status of the admin + // theme. We want to check that the admin theme is included in the list. + 'drupal' => '0', + ); + $this->refreshUpdateStatus($xml_mapping); + // The admin theme is displayed even if it's disabled. + $this->assertText('update_test_admintheme', "The admin theme is checked for update even if it's disabled"); + // The other disabled themes are not displayed. + $this->assertNoText('update_test_basetheme', 'Disabled theme is not checked for update in the list.'); + $this->assertNoText('update_test_subtheme', 'Disabled theme is not checked for update in the list.'); + } + + /** * Tests that disabled themes are only shown when desired. */ function testUpdateShowDisabledThemes() { @@ -800,4 +849,4 @@ $this->assertEqual($url, $expected, "When ? is present, '$url' should be '$expected'."); } -} \ No newline at end of file +} diff -Naur drupal-7.23/modules/user/tests/user_form_test.info drupal-7.66/modules/user/tests/user_form_test.info --- drupal-7.23/modules/user/tests/user_form_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/user/tests/user_form_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/user/tests/user_form_test.module drupal-7.66/modules/user/tests/user_form_test.module --- drupal-7.23/modules/user/tests/user_form_test.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/tests/user_form_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -62,3 +62,21 @@ function user_form_test_current_password_submit($form, &$form_state) { drupal_set_message(t('The password has been validated and the form submitted successfully.')); } + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function user_form_test_form_user_profile_form_alter(&$form, &$form_state) { + if (variable_get('user_form_test_user_profile_form_rebuild', FALSE)) { + $form['#submit'][] = 'user_form_test_user_account_submit'; + } +} + +/** + * Submit function for user_profile_form(). + */ +function user_form_test_user_account_submit($form, &$form_state) { + // Rebuild the form instead of letting the process end. This allows us to + // test for bugs that can be triggered in contributed modules. + $form_state['rebuild'] = TRUE; +} diff -Naur drupal-7.23/modules/user/user-picture.tpl.php drupal-7.66/modules/user/user-picture.tpl.php --- drupal-7.23/modules/user/user-picture.tpl.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/user-picture.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -17,7 +17,7 @@ */ ?> -
                +
                diff -Naur drupal-7.23/modules/user/user.api.php drupal-7.66/modules/user/user.api.php --- drupal-7.23/modules/user/user.api.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/user.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -123,8 +123,8 @@ * description is NOT used for the radio button, but instead should provide * additional explanation to the user seeking to cancel their account. * - access: (optional) A boolean value indicating whether the user can access - * a method. If #access is defined, the method cannot be configured as default - * method. + * a method. If access is defined, the method cannot be configured as the + * default method. * * @param $methods * An array containing user account cancellation methods, keyed by method id. @@ -183,7 +183,23 @@ } /** - * Retrieve a list of user setting or profile information categories. + * Define a list of user settings or profile information categories. + * + * There are two steps to using hook_user_categories(): + * - Create the category with hook_user_categories(). + * - Display that category on the form ID of "user_profile_form" with + * hook_form_FORM_ID_alter(). + * + * Step one builds out the category but it won't be visible on your form until + * you explicitly tell it to do so. + * + * The function in step two should contain the following code in order to + * display your new category: + * @code + * if ($form['#user_category'] == 'mycategory') { + * // Return your form here. + * } + * @endcode * * @return * An array of associative arrays. Each inner array has elements: @@ -327,14 +343,6 @@ * 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 +394,7 @@ } /** - * 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 +413,7 @@ } /** - * 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 +434,7 @@ } /** - * 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 +455,7 @@ } /** - * 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 -Naur drupal-7.23/modules/user/user.info drupal-7.66/modules/user/user.info --- drupal-7.23/modules/user/user.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/modules/user/user.info 2019-04-17 22:39:36.000000000 +0200 @@ -9,8 +9,7 @@ configure = admin/config/people stylesheets[all][] = user.css -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/modules/user/user.install drupal-7.66/modules/user/user.install --- drupal-7.23/modules/user/user.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/user.install 2019-04-17 22:20:46.000000000 +0200 @@ -49,6 +49,9 @@ 'columns' => array('uid' => 'uid'), ), ), + 'indexes' => array( + 'uid_module' => array('uid', 'module'), + ), ); $schema['role_permission'] = array( @@ -81,7 +84,7 @@ ), 'foreign keys' => array( 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -278,7 +281,7 @@ 'columns' => array('uid' => 'uid'), ), 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -356,11 +359,13 @@ '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 @@ -909,5 +914,14 @@ } /** + * Ensure there is a combined index on {authmap}.uid and {authmap}.module. + */ +function user_update_7019() { + // Check first in case it was already added manually. + if (!db_index_exists('authmap', 'uid_module')) { + db_add_index('authmap', 'uid_module', array('uid', 'module')); + } +} +/** * @} End of "addtogroup updates-7.x-extra". */ diff -Naur drupal-7.23/modules/user/user.js drupal-7.66/modules/user/user.js --- drupal-7.23/modules/user/user.js 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/user.js 2019-04-17 22:20:46.000000000 +0200 @@ -93,6 +93,8 @@ * Returns the estimated strength and the relevant output message. */ Drupal.evaluatePasswordStrength = function (password, translate) { + password = $.trim(password); + var weaknesses = 0, strength = 100, msg = []; var hasLowercase = /[a-z]+/.test(password); diff -Naur drupal-7.23/modules/user/user.module drupal-7.66/modules/user/user.module --- drupal-7.23/modules/user/user.module 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/user.module 2019-04-17 22:20:46.000000000 +0200 @@ -32,7 +32,7 @@ define('USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL', 2); /** - * Implement hook_help(). + * Implements hook_help(). */ function user_help($path, $arg) { global $user; @@ -418,13 +418,11 @@ * * @return * A fully-loaded $user object upon successful save or FALSE if the save failed. - * - * @todo D8: Drop $edit and fix user_save() to be consistent with others. */ function user_save($account, $edit = array(), $category = 'account') { $transaction = db_transaction(); try { - if (!empty($edit['pass'])) { + if (isset($edit['pass']) && strlen(trim($edit['pass'])) > 0) { // Allow alternate password hashing schemes. require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); $edit['pass'] = user_hash_password(trim($edit['pass'])); @@ -501,12 +499,17 @@ 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 +592,16 @@ 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(); } @@ -634,7 +637,7 @@ if (strpos($name, ' ') !== FALSE) { return t('The username cannot contain multiple spaces in a row.'); } - if (preg_match('/[^\x{80}-\x{F7} a-z0-9@_.\'-]/i', $name)) { + if (preg_match('/[^\x{80}-\x{F7} a-z0-9@+_.\'-]/i', $name)) { return t('The username contains an illegal character.'); } if (preg_match('/[\x{80}-\x{A0}' . // Non-printable ISO-8859-1 + NBSP @@ -686,7 +689,7 @@ $validators = array( 'file_validate_is_image' => array(), 'file_validate_image_resolution' => array(variable_get('user_picture_dimensions', '85x85')), - 'file_validate_size' => array(variable_get('user_picture_file_size', '30') * 1024), + 'file_validate_size' => array((int) variable_get('user_picture_file_size', '30') * 1024), ); // Save the file as a temporary file. @@ -717,10 +720,14 @@ // Loop the number of times specified by $length. for ($i = 0; $i < $length; $i++) { + do { + // Find a secure random number within the range needed. + $index = ord(drupal_random_bytes(1)); + } while ($index > $len); // Each iteration, pick a random character from the // allowable string and append it to the password: - $pass .= $allowable_characters[mt_rand(0, $len)]; + $pass .= $allowable_characters[$index]; } return $pass; @@ -733,8 +740,9 @@ * An array whose keys are the role IDs of interest, such as $user->roles. * * @return - * An array indexed by role ID. Each value is an array whose keys are the - * permission strings for the given role ID. + * If $roles is a non-empty array, an array indexed by role ID is returned. + * Each value is an array whose keys are the permission strings for the given + * role ID. If $roles is empty nothing is returned. */ function user_role_permissions($roles = array()) { $cache = &drupal_static(__FUNCTION__, array()); @@ -781,7 +789,7 @@ * (optional) The account to check, if not given use currently logged in user. * * @return - * Boolean TRUE if the current user has the requested permission. + * Boolean TRUE if the user has the requested permission. * * All permission checks in Drupal should go through this function. This * way, we guarantee consistent behavior, and ensure that the superuser @@ -839,6 +847,26 @@ } /** + * 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(). */ function user_permission() { @@ -928,6 +956,8 @@ */ function user_search_execute($keys = NULL, $conditions = NULL) { $find = array(); + // Escape for LIKE matching. + $keys = db_like($keys); // Replace wildcards with MySQL/PostgreSQL wildcards. $keys = preg_replace('!\*+!', '%', $keys); $query = db_select('users')->extend('PagerDefault'); @@ -937,13 +967,13 @@ // and they don't need to be restricted to only active users. $query->fields('users', array('mail')); $query->condition(db_or()-> - condition('name', '%' . db_like($keys) . '%', 'LIKE')-> - condition('mail', '%' . db_like($keys) . '%', 'LIKE')); + condition('name', '%' . $keys . '%', 'LIKE')-> + condition('mail', '%' . $keys . '%', 'LIKE')); } else { // Regular users can only search via usernames, and we do not show them // blocked accounts. - $query->condition('name', '%' . db_like($keys) . '%', 'LIKE') + $query->condition('name', '%' . $keys . '%', 'LIKE') ->condition('status', 1); } $uids = $query @@ -1058,13 +1088,16 @@ '#description' => t('To change the current user password, enter the new password in both fields.'), ); // To skip the current password field, the user must have logged in via a - // one-time link and have the token in the URL. - $pass_reset = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $account->uid]); + // one-time link and have the token in the URL. Store this in $form_state + // so it persists even on subsequent Ajax requests. + if (!isset($form_state['user_pass_reset'])) { + $form_state['user_pass_reset'] = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $account->uid]); + } $protected_values = array(); $current_pass_description = ''; // The user may only change their own password without their current // password if they logged in via a one-time login link. - if (!$pass_reset) { + if (!$form_state['user_pass_reset']) { $protected_values['mail'] = $form['account']['mail']['#title']; $protected_values['pass'] = t('Password'); $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.')))); @@ -1130,7 +1163,7 @@ $form['account']['roles'] = array( '#type' => 'checkboxes', '#title' => t('Roles'), - '#default_value' => (!$register && isset($account->roles) ? array_keys($account->roles) : array()), + '#default_value' => (!$register && !empty($account->roles) ? array_keys(array_filter($account->roles)) : array()), '#options' => $roles, '#access' => $roles && user_access('administer permissions'), DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated, @@ -1200,7 +1233,7 @@ // that prevent them from being empty if they are changed. if ((strlen(trim($form_state['values'][$key])) > 0) && ($form_state['values'][$key] != $account->$key)) { require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); - $current_pass_failed = empty($form_state['values']['current_pass']) || !user_check_password($form_state['values']['current_pass'], $account); + $current_pass_failed = strlen(trim($form_state['values']['current_pass'])) == 0 || !user_check_password($form_state['values']['current_pass'], $account); if ($current_pass_failed) { form_set_error('current_pass', t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => $name))); form_set_error($key); @@ -1276,10 +1309,12 @@ elseif (!empty($edit['picture_delete'])) { $edit['picture'] = NULL; } - // Prepare user roles. - if (isset($edit['roles'])) { - $edit['roles'] = array_filter($edit['roles']); - } + } + + // Filter out roles with empty values to avoid granting extra roles when + // processing custom form submissions. + if (isset($edit['roles'])) { + $edit['roles'] = array_filter($edit['roles']); } // Move account cancellation information into $user->data. @@ -1721,21 +1756,23 @@ $items['admin/people/create'] = array( 'title' => 'Add user', + 'page callback' => 'user_admin', 'page arguments' => array('create'), 'access arguments' => array('administer users'), 'type' => MENU_LOCAL_ACTION, + 'file' => 'user.admin.inc', ); // Administration pages. $items['admin/config/people'] = array( - 'title' => 'People', - 'description' => 'Configure user accounts.', - 'position' => 'left', - 'weight' => -20, - 'page callback' => 'system_admin_menu_block_page', - 'access arguments' => array('access administration pages'), - 'file' => 'system.admin.inc', - 'file path' => drupal_get_path('module', 'system'), + 'title' => 'People', + 'description' => 'Configure user accounts.', + 'position' => 'left', + 'weight' => -20, + 'page callback' => 'system_admin_menu_block_page', + 'access arguments' => array('access administration pages'), + 'file' => 'system.admin.inc', + 'file path' => drupal_get_path('module', 'system'), ); $items['admin/config/people/accounts'] = array( 'title' => 'Account settings', @@ -1881,13 +1918,13 @@ // for authenticated users. Authenticated users should see "My account", but // anonymous users should not see it at all. Therefore, invoke // user_translated_menu_link_alter() to conditionally hide the link. - if ($link['link_path'] == 'user' && $link['module'] == 'system') { + if ($link['link_path'] == 'user' && isset($link['module']) && $link['module'] == 'system') { $link['options']['alter'] = TRUE; } // Force the Logout link to appear on the top-level of 'user-menu' menu by // default (i.e., unless it has been customized). - if ($link['link_path'] == 'user/logout' && $link['module'] == 'system' && empty($link['customized'])) { + if ($link['link_path'] == 'user/logout' && isset($link['module']) && $link['module'] == 'system' && empty($link['customized'])) { $link['plid'] = 0; } } @@ -2118,7 +2155,7 @@ * A FAPI validate handler. Sets an error if supplied username has been blocked. */ function user_login_name_validate($form, &$form_state) { - if (isset($form_state['values']['name']) && user_is_blocked($form_state['values']['name'])) { + if (!empty($form_state['values']['name']) && user_is_blocked($form_state['values']['name'])) { // Blocked in user administration. form_set_error('name', t('The username %name has not been activated or is blocked.', array('%name' => $form_state['values']['name']))); } @@ -2131,7 +2168,7 @@ */ function user_login_authenticate_validate($form, &$form_state) { $password = trim($form_state['values']['pass']); - if (!empty($form_state['values']['name']) && !empty($password)) { + if (!empty($form_state['values']['name']) && strlen(trim($password)) > 0) { // Do not allow any login from the current user's IP if the limit has been // reached. Default is 50 failed attempts allowed in one hour. This is // independent of the per-user limit to catch attempts from one IP to log @@ -2195,7 +2232,11 @@ } } else { - form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password', array('query' => array('name' => $form_state['values']['name'])))))); + // Use $form_state['input']['name'] here to guarantee that we send + // exactly what the user typed in. $form_state['values']['name'] may have + // been modified by validation handlers that ran earlier than this one. + $query = isset($form_state['input']['name']) ? array('name' => $form_state['input']['name']) : array(); + form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password', array('query' => $query))))); watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name'])); } } @@ -2218,7 +2259,7 @@ */ function user_authenticate($name, $password) { $uid = FALSE; - if (!empty($name) && !empty($password)) { + if (!empty($name) && strlen(trim($password)) > 0) { $account = user_load_by_name($name); if ($account) { // Allow alternate password hashing schemes. @@ -2314,7 +2355,10 @@ * Generates a unique URL for a user to login and reset their password. * * @param object $account - * An object containing the user account. + * An object containing the user account, which must contain at least the + * following properties: + * - uid: The user ID number. + * - login: The UNIX timestamp of the user's last login. * * @return * A unique URL that provides a one-time log in for the user, from which @@ -2322,7 +2366,7 @@ */ function user_pass_reset_url($account) { $timestamp = REQUEST_TIME; - return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE)); + return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2331,9 +2375,9 @@ * @param object $account * The user account object, which must contain at least the following * properties: - * - uid: The user uid number. + * - uid: The user ID number. * - pass: The hashed user password string. - * - login: The user login name. + * - login: The UNIX timestamp of the user's last login. * * @return * A unique URL that may be used to confirm the cancellation of the user @@ -2344,7 +2388,7 @@ */ function user_cancel_url($account) { $timestamp = REQUEST_TIME; - return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE)); + return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2355,21 +2399,42 @@ * order to validate the URL, the same hash can be generated again, from the * same information, and compared to the hash value from the URL. The URL * normally contains both the time stamp and the numeric user ID. The login - * name and hashed password are retrieved from the database as necessary. For a - * usage example, see user_cancel_url() and user_cancel_confirm(). + * timestamp and hashed password are retrieved from the database as necessary. + * For a usage example, see user_cancel_url() and user_cancel_confirm(). * - * @param $password + * @param string $password * The hashed user account password value. - * @param $timestamp - * A unix timestamp. - * @param $login - * The user account login name. + * @param int $timestamp + * 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); } /** @@ -2434,7 +2499,9 @@ } /** - * Last batch processing step for cancelling a user account. + * Implements callback_batch_operation(). + * + * Last step for cancelling a user account. * * Since batch and session API require a valid user account, the actual * cancellation of a user account needs to happen last. @@ -2482,6 +2549,8 @@ } /** + * Implements callback_batch_finished(). + * * Finished batch processing callback for cancelling a user account. * * @see user_cancel() @@ -2625,12 +2694,7 @@ $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); @@ -2834,7 +2898,7 @@ * An associative array of token replacement values. If the 'user' element * exists, it must contain a user account object with the following * properties: - * - login: The account login name. + * - login: The UNIX timestamp of the user's last login. * - pass: The hashed account login password. * @param $options * Unused parameter required by the token_replace() function. @@ -2990,6 +3054,11 @@ $role = user_role_load_by_name($role); } + // If this is the administrator role, delete the user_admin_role variable. + if ($role->rid == variable_get('user_admin_role')) { + variable_del('user_admin_role'); + } + db_delete('role') ->condition('rid', $role->rid) ->execute(); @@ -3605,12 +3674,7 @@ ); $element['#attached']['js'][] = drupal_get_path('module', 'user') . '/user.js'; - // Ensure settings are only added once per page. - static $already_added = FALSE; - if (!$already_added) { - $already_added = TRUE; - $element['#attached']['js'][] = array('data' => $js_settings, 'type' => 'setting'); - } + $element['#attached']['js'][] = array('data' => $js_settings, 'type' => 'setting'); return $element; } @@ -3670,7 +3734,14 @@ } /** - * Blocks the current user. + * Blocks a specific user or the current user, if one is not specified. + * + * @param $entity + * (optional) An entity object; if it is provided and it has a uid property, + * the user with that ID is blocked. + * @param $context + * (optional) An associative array; if no user ID is found in $entity, the + * 'uid' element of this array determines the user to block. * * @ingroup actions */ @@ -3758,8 +3829,8 @@ // 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 -Naur drupal-7.23/modules/user/user.pages.inc drupal-7.66/modules/user/user.pages.inc --- drupal-7.23/modules/user/user.pages.inc 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/user.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -44,6 +44,12 @@ $form['name']['#value'] = $user->mail; $form['mail'] = array( '#prefix' => '

                ', + // As of https://www.drupal.org/node/889772 the user no longer must log + // out (if they are still logged in when using the password reset link, + // they will be logged out automatically then), but this text is kept as + // is to avoid breaking translations as well as to encourage the user to + // log out manually at a time of their own choosing (when it will not + // interrupt anything else they may have been in the middle of doing). '#markup' => t('Password reset instructions will be mailed to %email. You must log out to use the password reset link in the e-mail.', array('%email' => $user->mail)), '#suffix' => '

                ', ); @@ -54,6 +60,11 @@ return $form; } +/** + * Form validation handler for user_pass(). + * + * @see user_pass_submit() + */ function user_pass_validate($form, &$form_state) { $name = trim($form_state['values']['name']); // Try to load by email. @@ -72,6 +83,11 @@ } } +/** + * Form submission handler for user_pass(). + * + * @see user_pass_validate() + */ function user_pass_submit($form, &$form_state) { global $language; @@ -96,22 +112,33 @@ // When processing the one-time login link, we have to make sure that a user // isn't already logged in. if ($user->uid) { - // The existing user is already logged in. + // The existing user is already logged in. Log them out and reload the + // current page so the password reset process can continue. if ($user->uid == $uid) { - drupal_set_message(t('You are logged in as %user. Change your password.', array('%user' => $user->name, '!user_edit' => url("user/$user->uid/edit")))); + // Preserve the current destination (if any) and ensure the redirect goes + // back to the current page; any custom destination set in + // hook_user_logout() and intended for regular logouts would not be + // appropriate here. + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + } + user_logout_current_user(); + unset($_GET['destination']); + drupal_goto(current_path(), array('query' => drupal_get_query_parameters() + $destination)); } // A different user is already logged in on the computer. else { $reset_link_account = user_load($uid); if (!empty($reset_link_account)) { drupal_set_message(t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please logout and try using the link again.', - array('%other_user' => $user->name, '%resetting_user' => $reset_link_account->name, '!logout' => url('user/logout')))); + array('%other_user' => $user->name, '%resetting_user' => $reset_link_account->name, '!logout' => url('user/logout'))), 'warning'); } else { // Invalid one-time link specifies an unknown user. - drupal_set_message(t('The one-time login link you clicked is invalid.')); + drupal_set_message(t('The one-time login link you clicked is invalid.'), 'error'); } + drupal_goto(); } - drupal_goto(); } else { // Time out, in seconds, until login URL expires. Defaults to 24 hours = @@ -123,10 +150,10 @@ if ($timestamp <= $current && $account = reset($users)) { // No time out for first time login. if ($account->login && $current - $timestamp > $timeout) { - 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_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.'), 'error'); 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. @@ -137,7 +164,7 @@ watchdog('user', 'User %name used one-time login link at time %timestamp.', array('%name' => $account->name, '%timestamp' => $timestamp)); drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); // Let the user's password be changed without the current password check. - $token = drupal_hash_base64(drupal_random_bytes(55)); + $token = drupal_random_key(); $_SESSION['pass_reset_' . $user->uid] = $token; drupal_goto('user/' . $user->uid . '/edit', array('query' => array('pass-reset-token' => $token))); } @@ -151,7 +178,7 @@ } } else { - drupal_set_message(t('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.')); + drupal_set_message(t('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.'), 'error'); drupal_goto('user/password'); } } @@ -159,6 +186,7 @@ // Deny access, no more clues. // Everything will be in the watchdog's URL for the administrator to check. drupal_access_denied(); + drupal_exit(); } } } @@ -167,6 +195,14 @@ * Menu callback; logs the current user out, and redirects to the home page. */ function user_logout() { + user_logout_current_user(); + drupal_goto(); +} + +/** + * Logs the current user out. + */ +function user_logout_current_user() { global $user; watchdog('user', 'Session closed for %name.', array('%name' => $user->name)); @@ -175,15 +211,16 @@ // Destroy the current session, and reset $user to the anonymous user. session_destroy(); - - drupal_goto(); } /** * 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 */ @@ -290,14 +327,18 @@ } /** - * Validation function for the user account and profile editing form. + * Form validation handler for user_profile_form(). + * + * @see user_profile_form_submit() */ function user_profile_form_validate($form, &$form_state) { entity_form_field_validate('user', $form, $form_state); } /** - * Submit function for the user account and profile editing form. + * Form submission handler for user_profile_form(). + * + * @see user_profile_form_validate() */ function user_profile_form_submit($form, &$form_state) { $account = $form_state['user']; @@ -355,7 +396,6 @@ $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( @@ -519,7 +559,7 @@ // 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), ); @@ -530,18 +570,24 @@ batch_process(''); } else { - drupal_set_message(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.')); + drupal_set_message(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'error'); drupal_goto("user/$account->uid/cancel"); } } - drupal_access_denied(); + return MENU_ACCESS_DENIED; } /** - * Access callback for path /user. + * Page callback: Displays the user page. * * Displays user profile if user is logged in, or login form for anonymous * users. + * + * @return + * A render array for either a user profile or a login form. + * + * @see user_view_page() + * @see user_login() */ function user_page() { global $user; diff -Naur drupal-7.23/modules/user/user.test drupal-7.66/modules/user/user.test --- drupal-7.23/modules/user/user.test 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/modules/user/user.test 2019-04-17 22:20:46.000000000 +0200 @@ -276,6 +276,7 @@ 'foo@example.com' => array('Valid username', 'assertNull'), 'foo@-example.com' => array('Valid username', 'assertNull'), // invalid domains are allowed in usernames 'þòøÇߪř€' => array('Valid username', 'assertNull'), + 'foo+bar' => array('Valid username', 'assertNull'), // '+' symbol is allowed 'ᚠᛇᚻ᛫ᛒᛦᚦ' => array('Valid UTF8 username', 'assertNull'), // runes ' foo' => array('Invalid username that starts with a space', 'assertNotNull'), 'foo ' => array('Invalid username that ends with a space', 'assertNotNull'), @@ -466,6 +467,19 @@ } /** + * Retrieves password reset email and extracts the login link. + */ + public function getResetURL() { + // Assume the most recent email. + $_emails = $this->drupalGetMails(); + $email = end($_emails); + $urls = array(); + preg_match('#.+user/reset/.+#', $email['body'], $urls); + + return $urls[0]; + } + + /** * Tests password reset functionality. */ function testUserPasswordReset() { @@ -478,6 +492,77 @@ $this->drupalPost('user/password', $edit, t('E-mail new password')); // Confirm the password reset. $this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.'); + + // Create an image field to enable an Ajax request on the user profile page. + $field = array( + 'field_name' => 'field_avatar', + 'type' => 'image', + 'settings' => array(), + 'cardinality' => 1, + ); + field_create_field($field); + + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => 'user', + 'label' => 'Avatar', + 'bundle' => 'user', + 'required' => FALSE, + 'settings' => array(), + 'widget' => array( + 'type' => 'image_image', + 'settings' => array(), + ), + ); + field_create_instance($instance); + + $resetURL = $this->getResetURL(); + $this->drupalGet($resetURL); + + // Check successful login. + $this->drupalPost(NULL, NULL, t('Log in')); + + // Make sure the Ajax request from uploading a file does not invalidate the + // reset token. + $image = current($this->drupalGetTestFiles('image')); + $edit = array( + 'files[field_avatar_und_0]' => drupal_realpath($image->uri), + ); + $this->drupalPostAJAX(NULL, $edit, 'field_avatar_und_0_upload_button'); + + // Change the forgotten password. + $password = user_password(); + $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.'); + } + + /** + * Test user password reset while logged in. + */ + function testUserPasswordResetLoggedIn() { + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + // Make sure the test account has a valid password. + user_save($account, array('pass' => user_password())); + + // Generate one time login link. + $reset_url = user_pass_reset_url($account); + $this->drupalGet($reset_url); + + $this->assertText('Reset password'); + $this->drupalPost(NULL, NULL, t('Log in')); + + $this->assertText('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.'); + + $pass = user_password(); + $edit = array( + 'pass[pass1]' => $pass, + 'pass[pass2]' => $pass, + ); + $this->drupalPost(NULL, $edit, t('Save')); + + $this->assertText('The changes have been saved.'); } /** @@ -498,7 +583,7 @@ // 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 +604,74 @@ $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 +711,7 @@ // 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 +784,14 @@ // 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 +828,7 @@ $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 +866,7 @@ $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 +916,7 @@ $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 +980,7 @@ $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 +1280,17 @@ $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.'); } } @@ -1422,7 +1586,13 @@ // Setup date/time settings for Los Angeles time. variable_set('date_default_timezone', 'America/Los_Angeles'); variable_set('configurable_timezones', 1); - variable_set('date_format_medium', 'Y-m-d H:i T'); + + // Override the 'medium' date format, which is the default for node + // creation time. Since we are testing time zones with Daylight Saving + // Time, and need to future proof against changes to the zoneinfo database, + // we choose the 'I' format placeholder instead of a human-readable zone + // name. With 'I', a 1 means the date is in DST, and 0 if not. + variable_set('date_format_medium', 'Y-m-d H:i I'); // Create a user account and login. $web_user = $this->drupalCreateUser(); @@ -1440,11 +1610,11 @@ // Confirm date format and time zone. $this->drupalGet("node/$node1->nid"); - $this->assertText('2007-03-09 21:00 PST', 'Date should be PST.'); + $this->assertText('2007-03-09 21:00 0', 'Date should be PST.'); $this->drupalGet("node/$node2->nid"); - $this->assertText('2007-03-11 01:00 PST', 'Date should be PST.'); + $this->assertText('2007-03-11 01:00 0', 'Date should be PST.'); $this->drupalGet("node/$node3->nid"); - $this->assertText('2007-03-20 21:00 PDT', 'Date should be PDT.'); + $this->assertText('2007-03-20 21:00 1', 'Date should be PDT.'); // Change user time zone to Santiago time. $edit = array(); @@ -1455,11 +1625,11 @@ // Confirm date format and time zone. $this->drupalGet("node/$node1->nid"); - $this->assertText('2007-03-10 02:00 CLST', 'Date should be Chile summer time; five hours ahead of PST.'); + $this->assertText('2007-03-10 02:00 1', 'Date should be Chile summer time; five hours ahead of PST.'); $this->drupalGet("node/$node2->nid"); - $this->assertText('2007-03-11 05:00 CLT', 'Date should be Chile time; four hours ahead of PST'); + $this->assertText('2007-03-11 05:00 0', 'Date should be Chile time; four hours ahead of PST'); $this->drupalGet("node/$node3->nid"); - $this->assertText('2007-03-21 00:00 CLT', 'Date should be Chile time; three hours ahead of PDT.'); + $this->assertText('2007-03-21 00:00 0', 'Date should be Chile time; three hours ahead of PDT.'); } } @@ -1770,6 +1940,19 @@ $this->drupalGet('admin/people'); $this->assertText($edit['name'], 'User found in list of users'); } + + // Test that the password '0' is considered a password. + $name = $this->randomName(); + $edit = array( + 'name' => $name, + 'mail' => $name . '@example.com', + 'pass[pass1]' => 0, + 'pass[pass2]' => 0, + 'notify' => FALSE, + ); + $this->drupalPost('admin/people/create', $edit, t('Create new account')); + $this->assertText(t('Created a new user account for @name. No e-mail has been sent.', array('@name' => $edit['name'])), 'User created with password 0'); + $this->assertNoText('Password field is required'); } } @@ -1847,6 +2030,74 @@ $this->drupalLogin($user1); $this->drupalLogout(); } + + /** + * Tests setting the password to "0". + */ + public function testUserWith0Password() { + $admin = $this->drupalCreateUser(array('administer users')); + $this->drupalLogin($admin); + // Create a regular user. + $user1 = $this->drupalCreateUser(array()); + + $edit = array('pass[pass1]' => '0', 'pass[pass2]' => '0'); + $this->drupalPost("user/" . $user1->uid . "/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + + $this->drupalLogout(); + $user1->pass_raw = '0'; + $this->drupalLogin($user1); + $this->drupalLogout(); + } +} + +/** + * Tests editing a user account with and without a form rebuild. + */ +class UserEditRebuildTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'User edit with form rebuild', + 'description' => 'Test user edit page when a form rebuild is triggered.', + 'group' => 'User', + ); + } + + function setUp() { + parent::setUp('user_form_test'); + } + + /** + * Test user edit page when the form is set to rebuild. + */ + function testUserEditFormRebuild() { + $user1 = $this->drupalCreateUser(array('change own username')); + $this->drupalLogin($user1); + + $roles = array_keys($user1->roles); + // Save the user form twice. + $edit = array(); + $edit['current_pass'] = $user1->pass_raw; + $this->drupalPost("user/$user1->uid/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $saved_user1 = entity_load_unchanged('user', $user1->uid); + $this->assertEqual(count($roles), count($saved_user1->roles), 'Count of user roles in database matches original count.'); + $diff = array_diff(array_keys($saved_user1->roles), $roles); + $this->assertTrue(empty($diff), format_string('User roles in database match original: @roles', array('@roles' => implode(', ', $saved_user1->roles)))); + // Set variable that causes the form to be rebuilt in user_form_test.module. + variable_set('user_form_test_user_profile_form_rebuild', TRUE); + $this->drupalPost("user/$user1->uid/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $saved_user1 = entity_load_unchanged('user', $user1->uid); + $this->assertEqual(count($roles), count($saved_user1->roles), 'Count of user roles in database matches original count.'); + $diff = array_diff(array_keys($saved_user1->roles), $roles); + $this->assertTrue(empty($diff), format_string('User roles in database match original: @roles', array('@roles' => implode(', ', $saved_user1->roles)))); + } } /** @@ -2016,12 +2267,16 @@ $this->assertFalse(user_role_load_by_name($old_name), 'The role can no longer be retrieved from the database using its old name.'); $this->assertTrue(is_object(user_role_load_by_name($role_name)), 'The role can be retrieved from the database using its new name.'); - // Test deleting a role. + // Test deleting the default administrator role. + $role_name = 'administrator'; + $role = user_role_load_by_name($role_name); $this->drupalPost("admin/people/permissions/roles/edit/{$role->rid}", NULL, t('Delete role')); $this->drupalPost(NULL, NULL, t('Delete')); $this->assertText(t('The role has been deleted.'), 'The role has been deleted'); $this->assertNoLinkByHref("admin/people/permissions/roles/edit/{$role->rid}", 'Role edit link removed.'); $this->assertFalse(user_role_load_by_name($role_name), 'A deleted role can no longer be loaded.'); + // Make sure this role is no longer configured as the administrator role. + $this->assertNull(variable_get('user_admin_role'), 'The administrator role is no longer configured as the administrator role.'); // Make sure that the system-defined roles cannot be edited via the user // interface. @@ -2132,7 +2387,13 @@ } function testUserSearch() { + // Verify that a user without 'administer users' permission cannot search + // for users by email address. Additionally, ensure that the username has a + // plus sign to ensure searching works with that. $user1 = $this->drupalCreateUser(array('access user profiles', 'search content', 'use advanced search')); + $edit['name'] = 'foo+bar'; + $edit['mail'] = $edit['name'] . '@example.com'; + user_save($user1, $edit); $this->drupalLogin($user1); $keys = $user1->mail; $edit = array('keys' => $keys); @@ -2147,6 +2408,20 @@ $this->drupalPost('search/user/', $edit, t('Search')); $this->assertText($keys); + // Verify that wildcard search works. + $keys = $user1->name; + $keys = substr($keys, 0, 2) . '*' . substr($keys, 4, 2); + $edit = array('keys' => $keys); + $this->drupalPost('search/user/', $edit, t('Search')); + $this->assertText($user1->name, 'Search for username wildcard resulted in user name on page for administrative user.'); + + // Verify that wildcard search works for email. + $keys = $user1->mail; + $keys = substr($keys, 0, 2) . '*' . substr($keys, 4, 2); + $edit = array('keys' => $keys); + $this->drupalPost('search/user/', $edit, t('Search')); + $this->assertText($user1->name, 'Search for email wildcard resulted in user name on page for administrative user.'); + // Create a blocked user. $blocked_user = $this->drupalCreateUser(); $edit = array('status' => 0); diff -Naur drupal-7.23/profiles/README.txt drupal-7.66/profiles/README.txt --- drupal-7.23/profiles/README.txt 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/profiles/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,28 @@ +Installation profiles define additional steps that run after the base +installation provided by Drupal core when Drupal is first installed. + +WHAT TO PLACE IN THIS DIRECTORY? +-------------------------------- + +Place downloaded and custom installation profiles in this directory. +Installation profiles are generally provided as part of a Drupal distribution. +They only impact the installation of your site. They do not have any effect on +an already running site. + +DOWNLOAD ADDITIONAL DISTRIBUTIONS +--------------------------------- + +Contributed distributions from the Drupal community may be downloaded at +https://www.drupal.org/project/project_distribution. + +MULTISITE CONFIGURATION +----------------------- + +In multisite configurations, installation profiles found in this directory are +available to all sites during their initial site installation. + +MORE INFORMATION +---------------- + +Refer to the "Installation profiles" section of the README.txt in the Drupal +root directory for further information on extending Drupal with custom profiles. diff -Naur drupal-7.23/profiles/minimal/minimal.info drupal-7.66/profiles/minimal/minimal.info --- drupal-7.23/profiles/minimal/minimal.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/profiles/minimal/minimal.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ dependencies[] = block dependencies[] = dblog -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/profiles/standard/standard.info drupal-7.66/profiles/standard/standard.info --- drupal-7.23/profiles/standard/standard.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/profiles/standard/standard.info 2019-04-17 22:39:36.000000000 +0200 @@ -24,8 +24,7 @@ dependencies[] = file dependencies[] = rdf -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/profiles/standard/standard.install drupal-7.66/profiles/standard/standard.install --- drupal-7.23/profiles/standard/standard.install 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/profiles/standard/standard.install 2019-04-17 22:20:46.000000000 +0200 @@ -275,13 +275,10 @@ // Create a default vocabulary named "Tags", enabled for the 'article' content type. $description = st('Use tags to group articles on similar topics into categories.'); - $help = st('Enter a comma-separated list of words to describe your content.'); $vocabulary = (object) array( 'name' => st('Tags'), 'description' => $description, 'machine_name' => 'tags', - 'help' => $help, - ); taxonomy_vocabulary_save($vocabulary); @@ -301,12 +298,13 @@ ); field_create_field($field); + $help = st('Enter a comma-separated list of words to describe your content.'); $instance = array( 'field_name' => 'field_' . $vocabulary->machine_name, 'entity_type' => 'node', 'label' => 'Tags', 'bundle' => 'article', - 'description' => $vocabulary->help, + 'description' => $help, 'widget' => array( 'type' => 'taxonomy_autocomplete', 'weight' => -4, diff -Naur drupal-7.23/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info drupal-7.66/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info --- drupal-7.23/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ hidden = TRUE files[] = drupal_system_listing_compatible_test.test -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info drupal-7.66/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info --- drupal-7.23/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -8,8 +8,7 @@ core = 6.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/profiles/testing/testing.info drupal-7.66/profiles/testing/testing.info --- drupal-7.23/profiles/testing/testing.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/profiles/testing/testing.info 2019-04-17 22:39:36.000000000 +0200 @@ -4,8 +4,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/robots.txt drupal-7.66/robots.txt --- drupal-7.23/robots.txt 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/robots.txt 2019-04-17 22:20:46.000000000 +0200 @@ -11,13 +11,43 @@ # 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 +# CSS, JS, Images +Allow: /misc/*.css$ +Allow: /misc/*.css? +Allow: /misc/*.js$ +Allow: /misc/*.js? +Allow: /misc/*.gif +Allow: /misc/*.jpg +Allow: /misc/*.jpeg +Allow: /misc/*.png +Allow: /modules/*.css$ +Allow: /modules/*.css? +Allow: /modules/*.js$ +Allow: /modules/*.js? +Allow: /modules/*.gif +Allow: /modules/*.jpg +Allow: /modules/*.jpeg +Allow: /modules/*.png +Allow: /profiles/*.css$ +Allow: /profiles/*.css? +Allow: /profiles/*.js$ +Allow: /profiles/*.js? +Allow: /profiles/*.gif +Allow: /profiles/*.jpg +Allow: /profiles/*.jpeg +Allow: /profiles/*.png +Allow: /themes/*.css$ +Allow: /themes/*.css? +Allow: /themes/*.js$ +Allow: /themes/*.js? +Allow: /themes/*.gif +Allow: /themes/*.jpg +Allow: /themes/*.jpeg +Allow: /themes/*.png # Directories Disallow: /includes/ Disallow: /misc/ diff -Naur drupal-7.23/scripts/generate-d6-content.sh drupal-7.66/scripts/generate-d6-content.sh --- drupal-7.23/scripts/generate-d6-content.sh 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/scripts/generate-d6-content.sh 2019-04-17 22:20:46.000000000 +0200 @@ -67,6 +67,7 @@ ++$voc_id; $vocabulary['name'] = "vocabulary $voc_id (i=$i)"; $vocabulary['description'] = "description of ". $vocabulary['name']; + $vocabulary['help'] = "help for ". $vocabulary['name']; $vocabulary['nodes'] = $i > 11 ? array('page' => TRUE) : array(); $vocabulary['multiple'] = $multiple[$i % 12]; $vocabulary['required'] = $required[$i % 12]; diff -Naur drupal-7.23/scripts/password-hash.sh drupal-7.66/scripts/password-hash.sh --- drupal-7.23/scripts/password-hash.sh 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/scripts/password-hash.sh 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/php +#!/usr/bin/env php useDefaults(array('test_id'))->execute(); // Execute tests. -simpletest_script_execute_batch($test_id, simpletest_script_get_test_list()); +$status = simpletest_script_execute_batch($test_id, simpletest_script_get_test_list()); // Retrieve the last database prefix used for testing and the last test class // that was run from. Use the information to read the lgo file in case any @@ -100,7 +104,7 @@ simpletest_clean_results_table($test_id); // Test complete, exit. -exit; +exit($status); /** * Print help text. @@ -142,6 +146,8 @@ --file Run tests identified by specific file names, instead of group names. Specify the path and the extension (i.e. 'modules/user/user.test'). + --directory Run all tests found within the specified file directory. + --xml If provided, test results will be written as xml files to this path. @@ -190,6 +196,7 @@ 'all' => FALSE, 'class' => FALSE, 'file' => FALSE, + 'directory' => '', 'color' => FALSE, 'verbose' => FALSE, 'test_names' => array(), @@ -222,7 +229,7 @@ else { // Argument not found in list. simpletest_script_print_error("Unknown argument '$arg'."); - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } } else { @@ -235,7 +242,7 @@ // Validate the concurrency argument if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) { simpletest_script_print_error("--concurrency must be a strictly positive integer."); - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } return array($args, $count); @@ -265,7 +272,7 @@ else { simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.'); simpletest_script_help(); - exit(); + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } // Get URL from arguments. @@ -310,6 +317,8 @@ function simpletest_script_execute_batch($test_id, $test_classes) { global $args; + $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS; + // Multi-process execution. $children = array(); while (!empty($test_classes) || !empty($children)) { @@ -325,7 +334,7 @@ if (!is_resource($process)) { echo "Unable to fork test process. Aborting.\n"; - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } // Register our new child. @@ -345,13 +354,22 @@ if (empty($status['running'])) { // The child exited, unregister it. proc_close($child['process']); - if ($status['exitcode']) { + if ($status['exitcode'] == SIMPLETEST_SCRIPT_EXIT_FAILURE) { + if ($status['exitcode'] > $total_status) { + $total_status = $status['exitcode']; + } + } + elseif ($status['exitcode']) { + $total_status = $status['exitcode']; echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n"; } + + // Remove this child. unset($children[$cid]); } } } + return $total_status; } /** @@ -374,11 +392,14 @@ simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status)); // Finished, kill this runner. - exit(0); + if ($had_fails || $had_exceptions) { + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); + } + exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); } catch (Exception $e) { echo (string) $e; - exit(1); + exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); } } @@ -419,9 +440,20 @@ 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(SIMPLETEST_SCRIPT_EXIT_FAILURE); } } } @@ -440,13 +472,61 @@ } } } + elseif ($args['directory']) { + // Extract test case class names from specified directory. + // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php + // Since we do not want to hard-code too many structural file/directory + // assumptions about PSR-0/4 files and directories, we check for the + // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in + // its path. + // Ignore anything from third party vendors, and ignore template files used in tests. + // And any api.php files. + $ignore = array('nomask' => '/vendor|\.tpl\.php|\.api\.php/'); + $files = array(); + if ($args['directory'][0] === '/') { + $directory = $args['directory']; + } + else { + $directory = DRUPAL_ROOT . "/" . $args['directory']; + } + $file_list = file_scan_directory($directory, '/\.php|\.test$/', $ignore); + foreach ($file_list as $file) { + // '/Tests/' can be contained anywhere in the file's path (there can be + // sub-directories below /Tests), but must be contained literally. + // Case-insensitive to match all Simpletest and PHPUnit tests: + // ./lib/Drupal/foo/Tests/Bar/Baz.php + // ./foo/src/Tests/Bar/Baz.php + // ./foo/tests/Drupal/foo/Tests/FooTest.php + // ./foo/tests/src/FooTest.php + // $file->filename doesn't give us a directory, so we use $file->uri + // Strip the drupal root directory and trailing slash off the URI + $filename = substr($file->uri, strlen(DRUPAL_ROOT)+1); + if (stripos($filename, '/Tests/')) { + $files[drupal_realpath($filename)] = 1; + } else if (stripos($filename, '.test')){ + $files[drupal_realpath($filename)] = 1; + } + } + + // Check for valid class names. + foreach ($all_tests as $class_name) { + $refclass = new ReflectionClass($class_name); + $classfile = $refclass->getFileName(); + if (isset($files[$classfile])) { + $test_list[] = $class_name; + } + } + } else { // 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(SIMPLETEST_SCRIPT_EXIT_FAILURE); } } } @@ -454,7 +534,7 @@ if (empty($test_list)) { simpletest_script_print_error('No valid tests were specified.'); - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } return $test_list; } @@ -674,3 +754,37 @@ } 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 -Naur drupal-7.23/sites/all/libraries/README.txt drupal-7.66/sites/all/libraries/README.txt --- drupal-7.23/sites/all/libraries/README.txt 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/sites/all/libraries/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,2 @@ +This directory should be used to place downloaded and custom libraries (such as +JavaScript libraries) which are used by contributed or custom modules. diff -Naur drupal-7.23/sites/all/modules/README.txt drupal-7.66/sites/all/modules/README.txt --- drupal-7.23/sites/all/modules/README.txt 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/sites/all/modules/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,16 +1,37 @@ -Place downloaded and custom modules that extend your site functionality beyond -Drupal core in this directory to ensure clean separation from core modules and -to facilitate safe, self-contained code updates. Contributed modules from the -Drupal community may be downloaded at http://drupal.org/project/modules. +Modules extend your site functionality beyond Drupal core. -It is safe to organize modules into subdirectories, such as "contrib" for +WHAT TO PLACE IN THIS DIRECTORY? +-------------------------------- + +Placing downloaded and custom modules in this directory separates downloaded and +custom modules from Drupal core's modules. This allows Drupal core to be updated +without overwriting these files. + +DOWNLOAD ADDITIONAL MODULES +--------------------------- + +Contributed modules from the Drupal community may be downloaded at +https://www.drupal.org/project/project_module. + +ORGANIZING MODULES IN THIS DIRECTORY +------------------------------------ + +You may create subdirectories in this directory, to organize your added modules, +without breaking the site. Some common subdirectories include "contrib" for contributed modules, and "custom" for custom modules. Note that if you move a module to a subdirectory after it has been enabled, you may need to clear the -Drupal cache so that it can be found. +Drupal cache so it can be found. (Alternatively, you can disable the module +before moving it and then re-enable it after the move.) + +MULTISITE CONFIGURATION +----------------------- + +In multisite configurations, modules found in this directory are available to +all sites. Alternatively, the sites/your_site_name/modules directory pattern +may be used to restrict modules to a specific site instance. -In multisite configuration, modules found in this directory are available to -all sites. Alternatively, the sites/your_site_name/modules directory pattern may -be used to restrict modules to a specific site instance. +MORE INFORMATION +---------------- Refer to the "Developing for Drupal" section of the README.txt in the Drupal root directory for further information on extending Drupal with custom modules. diff -Naur drupal-7.23/sites/all/themes/README.txt drupal-7.66/sites/all/themes/README.txt --- drupal-7.23/sites/all/themes/README.txt 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/sites/all/themes/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,14 +1,29 @@ -Place downloaded and custom themes that modify your site's appearance in this -directory to ensure clean separation from Drupal core and to facilitate safe, -self-contained code updates. Contributed themes from the Drupal community may -be downloaded at http://drupal.org/project/themes. - -It is safe to organize themes into subdirectories and is recommended to use -Drupal's sub-theme functionality to ensure easy maintenance and upgrades. - -In multisite configuration, themes found in this directory are available to -all sites. Alternatively, the sites/your_site_name/themes directory pattern may -be used to restrict themes to a specific site instance. +Themes allow you to change the look and feel of your Drupal site. You can use +themes contributed by others or create your own. -Refer to the "Appearance" section of the README.txt in the Drupal root -directory for further information on theming. +WHAT TO PLACE IN THIS DIRECTORY? +-------------------------------- + +Placing downloaded and custom themes in this directory separates downloaded and +custom themes from Drupal core's themes. This allows Drupal core to be updated +without overwriting these files. + +DOWNLOAD ADDITIONAL THEMES +-------------------------- + +Contributed themes from the Drupal community may be downloaded at +https://www.drupal.org/project/project_theme. + +MULTISITE CONFIGURATION +----------------------- + +In multisite configurations, themes found in this directory are available to +all sites. Alternatively, the sites/your_site_name/themes directory pattern +may be used to restrict themes to a specific site instance. + +MORE INFORMATION +----------------- + +Refer to the "Appearance" section of the README.txt in the Drupal root directory +for further information on customizing the appearance of Drupal with custom +themes. diff -Naur drupal-7.23/sites/default/default.settings.php drupal-7.66/sites/default/default.settings.php --- drupal-7.23/sites/default/default.settings.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/sites/default/default.settings.php 2019-04-17 22:20:46.000000000 +0200 @@ -83,11 +83,13 @@ * webserver. For most other drivers, you must specify a * username, password, host, and database name. * - * Some database engines support transactions. In order to enable - * transaction support for a given database, set the 'transactions' key - * to TRUE. To disable it, set it to FALSE. Note that the default value - * varies by driver. For MySQL, the default is FALSE since MyISAM tables - * do not support transactions. + * Transaction support is enabled by default for all drivers that support it, + * including MySQL. To explicitly disable it, set the 'transactions' key to + * FALSE. + * Note that some configurations of MySQL, such as the MyISAM engine, don't + * support it and will proceed silently even if enabled. If you experience + * transaction related crashes with such configuration, set the 'transactions' + * key to FALSE. * * For each database, you may optionally specify multiple "target" databases. * A target database allows Drupal to try to send certain queries to a @@ -124,6 +126,38 @@ * ); * @endcode * + * For handling full UTF-8 in MySQL, including multi-byte characters such as + * emojis, Asian symbols, and mathematical symbols, you may set the collation + * and charset to "utf8mb4" prior to running install.php: + * @code + * $databases['default']['default'] = array( + * 'driver' => 'mysql', + * 'database' => 'databasename', + * 'username' => 'username', + * 'password' => 'password', + * 'host' => 'localhost', + * 'charset' => 'utf8mb4', + * 'collation' => 'utf8mb4_general_ci', + * ); + * @endcode + * When using this setting on an existing installation, ensure that all existing + * tables have been converted to the utf8mb4 charset, for example by using the + * utf8mb4_convert contributed project available at + * https://www.drupal.org/project/utf8mb4_convert, so as to prevent mixing data + * with different charsets. + * Note this should only be used when all of the following conditions are met: + * - In order to allow for large indexes, MySQL must be set up with the + * following my.cnf settings: + * [mysqld] + * innodb_large_prefix=true + * innodb_file_format=barracuda + * innodb_file_per_table=true + * These settings are available as of MySQL 5.5.14, and are defaults in + * MySQL 5.7.7 and up. + * - The PHP MySQL driver must support the utf8mb4 charset (libmysqlclient + * 5.5.3 and up, as well as mysqlnd 5.0.9 and up). + * - The MySQL server must support the utf8mb4 charset (5.5.3 and up). + * * You can optionally set prefixes for some or all database table names * by using the 'prefix' setting. If a prefix is specified, the table * name will be prepended with its value. Be sure to use valid database @@ -271,7 +305,7 @@ * * To see what PHP settings are possible, including whether they can be set at * runtime (by using ini_set()), read the PHP documentation: - * http://www.php.net/manual/en/ini.list.php + * http://www.php.net/manual/ini.list.php * See drupal_environment_initialize() in includes/bootstrap.inc for required * runtime settings and the .htaccess file for non-runtime settings. Settings * defined there should not be duplicated here so as to avoid conflict issues. @@ -307,7 +341,7 @@ * output filter may not have sufficient memory to process it. If you * experience this issue, you may wish to uncomment the following two lines * and increase the limits of these variables. For more information, see - * http://php.net/manual/en/pcre.configuration.php. + * http://php.net/manual/pcre.configuration.php. */ # ini_set('pcre.backtrack_limit', 200000); # ini_set('pcre.recursion_limit', 200000); @@ -433,6 +467,35 @@ # $conf['js_gzip_compression'] = FALSE; /** + * Block caching: + * + * Block caching may not be compatible with node access modules depending on + * how the original block cache policy is defined by the module that provides + * the block. By default, Drupal therefore disables block caching when one or + * more modules implement hook_node_grants(). If you consider block caching to + * be safe on your site and want to bypass this restriction, uncomment the line + * below. + */ +# $conf['block_cache_bypass_node_grants'] = TRUE; + +/** + * Expiration of cache_form entries: + * + * Drupal's Form API stores details of forms in cache_form and these entries are + * kept for at least 6 hours by default. Expired entries are cleared by cron. + * Busy sites can encounter problems with the cache_form table becoming very + * large. It's possible to mitigate this by setting a shorter expiration for + * cached forms. In some cases it may be desirable to set a longer cache + * expiration, for example to prolong cache_form entries for Ajax forms in + * cached HTML. + * + * @see form_set_cache() + * @see system_cron() + * @see ajax_get_form() + */ +# $conf['form_cache_expiration'] = 21600; + +/** * String overrides: * * To override specific strings on your site with or without enabling the Locale @@ -480,6 +543,7 @@ * specific pattern: * - 404_fast_paths_exclude: A regular expression to match paths to exclude, * such as images generated by image styles, or dynamically-resized images. + * The default pattern provided below also excludes the private file system. * If you need to add more paths, you can add '|path' to the expression. * - 404_fast_paths: A regular expression to match paths that should return a * simple 404 page, rather than the fully themed 404 page. If you don't have @@ -488,7 +552,7 @@ * * Add leading hash signs if you would like to disable this functionality. */ -$conf['404_fast_paths_exclude'] = '/\/(?:styles)\//'; +$conf['404_fast_paths_exclude'] = '/\/(?:styles)|(?:system\/files)\//'; $conf['404_fast_paths'] = '/\.(?:txt|png|gif|jpe?g|css|js|ico|swf|flv|cgi|bat|pl|dll|exe|asp)$/i'; $conf['404_fast_html'] = '404 Not Found

                Not Found

                The requested URL "@path" was not found on this server.

                '; @@ -503,10 +567,10 @@ * server response time when loading 404 error pages and prevents the 404 error * from being logged in the Drupal system log. In order to prevent valid pages * such as image styles and other generated content that may match the - * '404_fast_html' regular expression from returning 404 errors, it is necessary - * to add them to the '404_fast_paths_exclude' regular expression above. Make - * sure that you understand the effects of this feature before uncommenting the - * line below. + * '404_fast_paths' regular expression from returning 404 errors, it is + * necessary to add them to the '404_fast_paths_exclude' regular expression + * above. Make sure that you understand the effects of this feature before + * uncommenting the line below. */ # drupal_fast_404(); @@ -551,3 +615,47 @@ * Remove the leading hash signs to disable. */ # $conf['allow_authorize_operations'] = FALSE; + +/** + * Theme debugging: + * + * When debugging is enabled: + * - The markup of each template is surrounded by HTML comments that contain + * theming information, such as template file name suggestions. + * - Note that this debugging markup will cause automated tests that directly + * check rendered HTML to fail. + * + * For more information about debugging theme templates, see + * https://www.drupal.org/node/223440#theme-debug. + * + * Not recommended in production environments. + * + * Remove the leading hash sign to enable. + */ +# $conf['theme_debug'] = TRUE; + +/** + * CSS identifier double underscores allowance: + * + * To allow CSS identifiers to contain double underscores (.example__selector) + * for Drupal's BEM-style naming standards, uncomment the line below. + * Note that if you change this value in existing sites, existing page styles + * may be broken. + * + * @see drupal_clean_css_identifier() + */ +# $conf['allow_css_double_underscores'] = TRUE; + +/** + * The default list of directories that will be ignored by Drupal's file API. + * + * By default ignore node_modules and bower_components folders to avoid issues + * with common frontend tools and recursive scanning of directories looking for + * extensions. + * + * @see file_scan_directory() + */ +$conf['file_scan_ignore_directories'] = array( + 'node_modules', + 'bower_components', +); diff -Naur drupal-7.23/themes/bartik/bartik.info drupal-7.66/themes/bartik/bartik.info --- drupal-7.23/themes/bartik/bartik.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/themes/bartik/bartik.info 2019-04-17 22:39:36.000000000 +0200 @@ -34,8 +34,7 @@ settings[shortcut_module_link] = 0 -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/themes/bartik/css/style-rtl.css drupal-7.66/themes/bartik/css/style-rtl.css --- drupal-7.23/themes/bartik/css/style-rtl.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/themes/bartik/css/style-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -225,10 +225,10 @@ /* 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 -Naur drupal-7.23/themes/bartik/css/style.css drupal-7.66/themes/bartik/css/style.css --- drupal-7.23/themes/bartik/css/style.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/themes/bartik/css/style.css 2019-04-17 22:20:46.000000000 +0200 @@ -53,7 +53,7 @@ samp, var { padding: 0 0.4em; - font-size: 0.77em; + font-size: 0.857em; font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", "Nimbus Mono L", "DejaVu Sans Mono", monospace, "Courier New"; } code { @@ -704,7 +704,6 @@ } .comment .submitted .comment-permalink { font-size: 0.786em; - text-transform: lowercase; } .comment .content { font-size: 0.929em; @@ -1326,14 +1325,6 @@ 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 -Naur drupal-7.23/themes/garland/garland.info drupal-7.66/themes/garland/garland.info --- drupal-7.23/themes/garland/garland.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/themes/garland/garland.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ stylesheets[print][] = print.css settings[garland_width] = fluid -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/themes/garland/template.php drupal-7.66/themes/garland/template.php --- drupal-7.23/themes/garland/template.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/themes/garland/template.php 2019-04-17 22:20:46.000000000 +0200 @@ -19,22 +19,22 @@ /** * Override or insert variables into the maintenance page template. */ -function garland_preprocess_maintenance_page(&$vars) { +function garland_preprocess_maintenance_page(&$variables) { // While markup for normal pages is split into page.tpl.php and html.tpl.php, // the markup for the maintenance page is all in the single // maintenance-page.tpl.php template. So, to have what's done in // garland_preprocess_html() also happen on the maintenance page, it has to be // called here. - garland_preprocess_html($vars); + garland_preprocess_html($variables); } /** * Override or insert variables into the html template. */ -function garland_preprocess_html(&$vars) { +function garland_preprocess_html(&$variables) { // Toggle fixed or fluid width. if (theme_get_setting('garland_width') == 'fluid') { - $vars['classes_array'][] = 'fluid-width'; + $variables['classes_array'][] = 'fluid-width'; } // Add conditional CSS for IE6. drupal_add_css(path_to_theme() . '/fix-ie.css', array('group' => CSS_THEME, 'browsers' => array('IE' => 'lt IE 7', '!IE' => FALSE), 'preprocess' => FALSE)); @@ -43,27 +43,27 @@ /** * Override or insert variables into the html template. */ -function garland_process_html(&$vars) { +function garland_process_html(&$variables) { // Hook into color.module if (module_exists('color')) { - _color_html_alter($vars); + _color_html_alter($variables); } } /** * Override or insert variables into the page template. */ -function garland_preprocess_page(&$vars) { +function garland_preprocess_page(&$variables) { // Move secondary tabs into a separate variable. - $vars['tabs2'] = array( + $variables['tabs2'] = array( '#theme' => 'menu_local_tasks', - '#secondary' => $vars['tabs']['#secondary'], + '#secondary' => $variables['tabs']['#secondary'], ); - unset($vars['tabs']['#secondary']); + unset($variables['tabs']['#secondary']); - if (isset($vars['main_menu'])) { - $vars['primary_nav'] = theme('links__system_main_menu', array( - 'links' => $vars['main_menu'], + if (isset($variables['main_menu'])) { + $variables['primary_nav'] = theme('links__system_main_menu', array( + 'links' => $variables['main_menu'], 'attributes' => array( 'class' => array('links', 'inline', 'main-menu'), ), @@ -75,11 +75,11 @@ )); } else { - $vars['primary_nav'] = FALSE; + $variables['primary_nav'] = FALSE; } - if (isset($vars['secondary_menu'])) { - $vars['secondary_nav'] = theme('links__system_secondary_menu', array( - 'links' => $vars['secondary_menu'], + if (isset($variables['secondary_menu'])) { + $variables['secondary_nav'] = theme('links__system_secondary_menu', array( + 'links' => $variables['secondary_menu'], 'attributes' => array( 'class' => array('links', 'inline', 'secondary-menu'), ), @@ -91,66 +91,66 @@ )); } else { - $vars['secondary_nav'] = FALSE; + $variables['secondary_nav'] = FALSE; } // Prepare header. $site_fields = array(); - if (!empty($vars['site_name'])) { - $site_fields[] = $vars['site_name']; + if (!empty($variables['site_name'])) { + $site_fields[] = $variables['site_name']; } - if (!empty($vars['site_slogan'])) { - $site_fields[] = $vars['site_slogan']; + if (!empty($variables['site_slogan'])) { + $site_fields[] = $variables['site_slogan']; } - $vars['site_title'] = implode(' ', $site_fields); + $variables['site_title'] = implode(' ', $site_fields); if (!empty($site_fields)) { $site_fields[0] = '' . $site_fields[0] . ''; } - $vars['site_html'] = implode(' ', $site_fields); + $variables['site_html'] = implode(' ', $site_fields); // Set a variable for the site name title and logo alt attributes text. - $slogan_text = $vars['site_slogan']; - $site_name_text = $vars['site_name']; - $vars['site_name_and_slogan'] = $site_name_text . ' ' . $slogan_text; + $slogan_text = $variables['site_slogan']; + $site_name_text = $variables['site_name']; + $variables['site_name_and_slogan'] = $site_name_text . ' ' . $slogan_text; } /** * Override or insert variables into the node template. */ -function garland_preprocess_node(&$vars) { - $vars['submitted'] = $vars['date'] . ' — ' . $vars['name']; +function garland_preprocess_node(&$variables) { + $variables['submitted'] = $variables['date'] . ' — ' . $variables['name']; } /** * Override or insert variables into the comment template. */ -function garland_preprocess_comment(&$vars) { - $vars['submitted'] = $vars['created'] . ' — ' . $vars['author']; +function garland_preprocess_comment(&$variables) { + $variables['submitted'] = $variables['created'] . ' — ' . $variables['author']; } /** * Override or insert variables into the block template. */ -function garland_preprocess_block(&$vars) { - $vars['title_attributes_array']['class'][] = 'title'; - $vars['classes_array'][] = 'clearfix'; +function garland_preprocess_block(&$variables) { + $variables['title_attributes_array']['class'][] = 'title'; + $variables['classes_array'][] = 'clearfix'; } /** * Override or insert variables into the page template. */ -function garland_process_page(&$vars) { +function garland_process_page(&$variables) { // Hook into color.module if (module_exists('color')) { - _color_page_alter($vars); + _color_page_alter($variables); } } /** * Override or insert variables into the region template. */ -function garland_preprocess_region(&$vars) { - if ($vars['region'] == 'header') { - $vars['classes_array'][] = 'clearfix'; +function garland_preprocess_region(&$variables) { + if ($variables['region'] == 'header') { + $variables['classes_array'][] = 'clearfix'; } } diff -Naur drupal-7.23/themes/seven/seven.info drupal-7.66/themes/seven/seven.info --- drupal-7.23/themes/seven/seven.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/themes/seven/seven.info 2019-04-17 22:39:36.000000000 +0200 @@ -13,8 +13,7 @@ regions[sidebar_first] = First sidebar regions_hidden[] = sidebar_first -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/themes/seven/style.css drupal-7.66/themes/seven/style.css --- drupal-7.23/themes/seven/style.css 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/themes/seven/style.css 2019-04-17 22:20:46.000000000 +0200 @@ -475,6 +475,12 @@ border-color: #bebfb9; padding: 3px 10px; } +/** + * Force browsers to calculate the width of a 'select all' TH element. + */ +table th.select-all { + width: 1px; +} table th.active { background: #bdbeb9; } @@ -703,12 +709,7 @@ 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 -Naur drupal-7.23/themes/stark/stark.info drupal-7.66/themes/stark/stark.info --- drupal-7.23/themes/stark/stark.info 2013-08-08 04:17:18.000000000 +0200 +++ drupal-7.66/themes/stark/stark.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x stylesheets[all][] = layout.css -; Information added by drupal.org packaging script on 2013-08-08 -version = "7.23" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1375928238" - +datestamp = "1555533576" diff -Naur drupal-7.23/update.php drupal-7.66/update.php --- drupal-7.23/update.php 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/update.php 2019-04-17 22:20:46.000000000 +0200 @@ -178,7 +178,8 @@ $output = '

                Updates were attempted. If you see no failures below, you may proceed happily back to your site. Otherwise, you may need to update your database manually.' . $log_message . '

                '; } else { - list($module, $version) = array_pop(reset($_SESSION['updates_remaining'])); + $updates_remaining = reset($_SESSION['updates_remaining']); + list($module, $version) = array_pop($updates_remaining); $output = '

                The update process was aborted prematurely while running update #' . $version . ' in ' . $module . '.module.' . $log_message; if (module_exists('dblog')) { $output .= ' You may need to check the watchdog database table manually.'; @@ -467,13 +468,13 @@ // update.php ops. case 'selection': - if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) { + if (isset($_GET['token']) && drupal_valid_token($_GET['token'], 'update')) { $output = update_selection_page(); break; } case 'Apply pending updates': - if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) { + if (isset($_GET['token']) && drupal_valid_token($_GET['token'], 'update')) { // Generate absolute URLs for the batch processing (using $base_root), // since the batch API will pass them to url() which does not handle // update.php correctly by default. diff -Naur drupal-7.23/web.config drupal-7.66/web.config --- drupal-7.23/web.config 2013-08-08 04:04:26.000000000 +0200 +++ drupal-7.66/web.config 2019-04-17 22:20:46.000000000 +0200 @@ -6,7 +6,7 @@ - +